granite-form 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +6 -13
  5. data/lib/granite/form/model/associations/nested_attributes.rb +2 -2
  6. data/lib/granite/form/model/associations/reflections/embeds_many.rb +1 -1
  7. data/lib/granite/form/model/associations/reflections/embeds_one.rb +11 -1
  8. data/lib/granite/form/model/associations/reflections/references_one.rb +2 -0
  9. data/lib/granite/form/model/associations/reflections/singular.rb +0 -14
  10. data/lib/granite/form/model/attributes/attribute.rb +3 -21
  11. data/lib/granite/form/model/attributes/base.rb +5 -23
  12. data/lib/granite/form/model/attributes/reference_many.rb +1 -1
  13. data/lib/granite/form/model/attributes/reference_one.rb +1 -1
  14. data/lib/granite/form/model/attributes/reflections/attribute.rb +4 -4
  15. data/lib/granite/form/model/attributes/reflections/base/build_type_definition.rb +38 -0
  16. data/lib/granite/form/model/attributes/reflections/base.rb +12 -10
  17. data/lib/granite/form/model/attributes/reflections/collection/build_type_definition.rb +19 -0
  18. data/lib/granite/form/model/attributes/reflections/dictionary/build_type_definition.rb +19 -0
  19. data/lib/granite/form/model/attributes/reflections/dictionary.rb +0 -3
  20. data/lib/granite/form/model/attributes/reflections/represents/build_type_definition.rb +73 -0
  21. data/lib/granite/form/model/attributes/reflections/represents.rb +10 -2
  22. data/lib/granite/form/model/attributes/represents.rb +22 -37
  23. data/lib/granite/form/model/attributes.rb +10 -2
  24. data/lib/granite/form/model/representation.rb +1 -0
  25. data/lib/granite/form/model/validations.rb +6 -0
  26. data/lib/granite/form/model.rb +1 -1
  27. data/lib/granite/form/types/active_support/time_zone.rb +2 -0
  28. data/lib/granite/form/types/array.rb +2 -0
  29. data/lib/granite/form/types/big_decimal.rb +2 -0
  30. data/lib/granite/form/types/boolean.rb +2 -0
  31. data/lib/granite/form/types/collection.rb +11 -0
  32. data/lib/granite/form/types/date.rb +2 -0
  33. data/lib/granite/form/types/date_time.rb +2 -0
  34. data/lib/granite/form/types/dictionary.rb +23 -0
  35. data/lib/granite/form/types/float.rb +2 -0
  36. data/lib/granite/form/types/has_subtype.rb +18 -0
  37. data/lib/granite/form/types/hash_with_action_controller_parameters.rb +2 -0
  38. data/lib/granite/form/types/integer.rb +2 -0
  39. data/lib/granite/form/types/object.rb +28 -0
  40. data/lib/granite/form/types/string.rb +2 -0
  41. data/lib/granite/form/types/time.rb +2 -0
  42. data/lib/granite/form/types/uuid.rb +2 -0
  43. data/lib/granite/form/types.rb +3 -0
  44. data/lib/granite/form/util.rb +55 -0
  45. data/lib/granite/form/version.rb +1 -1
  46. data/lib/granite/form.rb +1 -0
  47. data/spec/granite/form/model/associations/references_many_spec.rb +1 -1
  48. data/spec/granite/form/model/associations/references_one_spec.rb +4 -4
  49. data/spec/granite/form/model/attributes/attribute_spec.rb +0 -29
  50. data/spec/granite/form/model/attributes/reflections/attribute_spec.rb +0 -9
  51. data/spec/granite/form/model/attributes/reflections/base/build_type_definition_spec.rb +27 -0
  52. data/spec/granite/form/model/attributes/reflections/base_spec.rb +16 -10
  53. data/spec/granite/form/model/attributes/reflections/collection/build_type_definition_spec.rb +24 -0
  54. data/spec/granite/form/model/attributes/reflections/dictionary/build_type_definition_spec.rb +24 -0
  55. data/spec/granite/form/model/attributes/reflections/dictionary_spec.rb +0 -6
  56. data/spec/granite/form/model/attributes/reflections/represents/build_type_definition_spec.rb +129 -0
  57. data/spec/granite/form/model/attributes/reflections/represents_spec.rb +43 -20
  58. data/spec/granite/form/model/attributes/represents_spec.rb +78 -55
  59. data/spec/granite/form/model/attributes_spec.rb +84 -23
  60. data/spec/granite/form/model/dirty_spec.rb +0 -6
  61. data/spec/granite/form/model/representation_spec.rb +4 -7
  62. data/spec/granite/form/model/validations_spec.rb +28 -1
  63. data/spec/granite/form/types/collection_spec.rb +22 -0
  64. data/spec/granite/form/types/dictionary_spec.rb +32 -0
  65. data/spec/granite/form/types/has_subtype_spec.rb +20 -0
  66. data/spec/granite/form/types/object_spec.rb +50 -4
  67. data/spec/granite/form/util_spec.rb +108 -0
  68. data/spec/support/active_record.rb +3 -0
  69. metadata +26 -15
  70. data/lib/granite/form/model/attributes/collection.rb +0 -19
  71. data/lib/granite/form/model/attributes/dictionary.rb +0 -28
  72. data/lib/granite/form/model/attributes/localized.rb +0 -44
  73. data/lib/granite/form/model/attributes/reflections/localized.rb +0 -45
  74. data/lib/granite/form/model/localization.rb +0 -26
  75. data/spec/granite/form/model/attributes/collection_spec.rb +0 -72
  76. data/spec/granite/form/model/attributes/dictionary_spec.rb +0 -100
  77. data/spec/granite/form/model/attributes/localized_spec.rb +0 -103
  78. data/spec/granite/form/model/attributes/reflections/localized_spec.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 848ee19f938a86a22a506585a7a4960bbb192b977fdc61173b05057c0381ec79
4
- data.tar.gz: 9c024906d4e6c7c8a93db7bcfe005dbffa956e4239a0078d642b3604c4226c0a
3
+ metadata.gz: 908865a58fa79739294c85bb506d878596a28782c02a9a2825ca1fb7fd128972
4
+ data.tar.gz: ff9b6cbed34774e3dedb0b33c28b75694b1575051adb989248ca5695254e9586
5
5
  SHA512:
6
- metadata.gz: e35f2d05bd0adaabdb00df1799b9997d6f01818f00271ee24cc628b9e5edf859ab4f15e1ee3e56f697485d89a2f1c05ebeff42816f53b4293c82854e3f4e247e
7
- data.tar.gz: df23355e592e78a1d998ace8a8010e188360ce99173fe69623bf3b03da18286f3476d509620b89f91fcaa5df299a20b32906cb3c0ae95353b75eb2c694afe409
6
+ metadata.gz: 92d055b121d30a9644521227782908a0b84ef0847c16ce60fafec47cb17c5c2988e8a071e199ed5506dd0783bba335b6a399a519c095867658e310680dd96f10
7
+ data.tar.gz: 4b4cf5f23530ad12408f498df6eed11e53438dd0a6f110ae04c10568934f041dbb64b946ee42352b7bfec83119a5827b652c8fddd62e745fa0f2835448674ce6
data/.github/CODEOWNERS CHANGED
@@ -1 +1 @@
1
- * @toptal/coresmiths-team
1
+ * @toptal/portals-experience-be
data/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # master
2
2
 
3
3
  ## Next
4
+ ## v0.4.0
5
+
6
+ * [BREAKING] Drop support for taking `model` as first argument in default/readonly/enum/normalize. This means that `default: -> (model) { model.other_field}` is no longer supported and should be replaced with `default: -> { other_field }`.
7
+ * Add support for evaluating `Symbol` for readonly/enum/normalize. If symbol is passed in one of those options, method with that name will be called when evaluating the value.
8
+ * [BREAKING] Remove `localized` attribute type.
9
+ * [BREAKING] Change the behavior of `default` and `normalize` for `collection` & `dictionary`. Instead of acting per element they will now act on the attribute as a whole.
10
+ * E.g. `collection :numbers, default: [1, 2, 3]` will not set the default for the whole collection of `numbers` rather than each element in `numbers`.
11
+
4
12
  ## v0.3.0
5
13
 
6
14
  - [BREAKING] Stop automatically saving `references_one`/`references_many` when applying changes.
data/README.md CHANGED
@@ -172,9 +172,10 @@ It is possible to provide default values for attributes and they will act in the
172
172
 
173
173
  ```ruby
174
174
  attribute :check, Boolean, default: false # Simply false by default
175
- attribute :today, Date, default: ->{ Time.zone.now.to_date } # Dynamic default value
176
- attribute :today_wday, Integer, default: ->{ today.wday } # Default is evaluated in instance context
177
- attribute :today_wday, Integer, default: ->(instance) { instance.today.wday } # The same as previous, but instance provided explicitly
175
+ attribute :wday, Integer, default: ->{ today.wday } # Default evaluated in instance context
176
+ def calculate_today
177
+ Time.zone.now.today
178
+ end
178
179
  ```
179
180
 
180
181
  ##### Enums
@@ -201,8 +202,8 @@ attribute :title, String, normalizers: [->(value) { value.strip }, trim: {length
201
202
 
202
203
  ```ruby
203
204
  attribute :name, String, readonly: true # Readonly forever
204
- attribute :name, String, readonly: -> { true } # Conditionally readonly
205
- attribute :name, String, readonly: ->(instance) { instance.subject.present? } # Explicit instance
205
+ attribute :name, String, readonly: :name_changed? # Conditional with calling method
206
+ attribute :name, String, readonly: -> { subject.present? } # Conditional with lambda
206
207
  ```
207
208
 
208
209
  #### Collection
@@ -251,14 +252,6 @@ end
251
252
 
252
253
  The keys list might be restricted with the `:keys` option. Default and enum modifiers are applied on each value, normalizers are applied on the hash.
253
254
 
254
- #### Localized
255
-
256
- `localized` is similar to how `Globalize 3` attributes work.
257
-
258
- ```ruby
259
- localized :title, String
260
- ```
261
-
262
255
  #### Represents
263
256
 
264
257
  `represents` provides an easy way to expose model attributes through an interface.
@@ -74,7 +74,7 @@ module Granite
74
74
  primary_attribute_name = primary_name_for(association.reflection.klass)
75
75
  if existing_record
76
76
  primary_attribute = existing_record.attribute(primary_attribute_name)
77
- primary_attribute_value = primary_attribute.type_definition.ensure_type(attributes[primary_attribute_name]) if primary_attribute
77
+ primary_attribute_value = primary_attribute.type_definition.prepare(attributes[primary_attribute_name]) if primary_attribute
78
78
  end
79
79
 
80
80
  if existing_record && (!primary_attribute || options[:update_only] || existing_record.primary_attribute == primary_attribute_value)
@@ -124,7 +124,7 @@ module Granite
124
124
  else
125
125
  existing_record = association.target.detect do |record|
126
126
  primary_attribute_value = record.attribute(primary_attribute_name)
127
- .type_definition.ensure_type(attributes[primary_attribute_name])
127
+ .type_definition.prepare(attributes[primary_attribute_name])
128
128
  record.primary_attribute == primary_attribute_value
129
129
  end
130
130
  if existing_record
@@ -5,7 +5,7 @@ module Granite
5
5
  module Reflections
6
6
  class EmbedsMany < EmbedsAny
7
7
  def self.build(target, generated_methods, name, options = {}, &block)
8
- target.add_attribute(Granite::Form::Model::Attributes::Reflections::Base, name) if target < Granite::Form::Model::Attributes
8
+ target.add_attribute(Granite::Form::Model::Attributes::Reflections::Base, name, type: Object) if target < Granite::Form::Model::Attributes
9
9
  options[:validate] = true unless options.key?(:validate)
10
10
  super
11
11
  end
@@ -7,10 +7,20 @@ module Granite
7
7
  include Singular
8
8
 
9
9
  def self.build(target, generated_methods, name, options = {}, &block)
10
- target.add_attribute(Granite::Form::Model::Attributes::Reflections::Base, name) if target < Granite::Form::Model::Attributes
10
+ target.add_attribute(Granite::Form::Model::Attributes::Reflections::Base, name, type: Object) if target < Granite::Form::Model::Attributes
11
11
  options[:validate] = true unless options.key?(:validate)
12
12
  super
13
13
  end
14
+
15
+ def self.generate_methods(name, target)
16
+ super
17
+
18
+ target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def build_#{name} attributes = {}
20
+ association(:#{name}).build(attributes)
21
+ end
22
+ RUBY
23
+ end
14
24
  end
15
25
  end
16
26
  end
@@ -7,6 +7,8 @@ module Granite
7
7
  module Associations
8
8
  module Reflections
9
9
  class ReferencesOne < ReferencesAny
10
+ include Singular
11
+
10
12
  def self.build(target, generated_methods, name, *args, &block)
11
13
  reflection = super
12
14
 
@@ -4,20 +4,6 @@ module Granite
4
4
  module Associations
5
5
  module Reflections
6
6
  module Singular
7
- extend ActiveSupport::Concern
8
-
9
- module ClassMethods
10
- def generate_methods(name, target)
11
- super
12
-
13
- target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
- def build_#{name} attributes = {}
15
- association(:#{name}).build(attributes)
16
- end
17
- RUBY
18
- end
19
- end
20
-
21
7
  def collection?
22
8
  false
23
9
  end
@@ -14,7 +14,7 @@ module Granite
14
14
 
15
15
  def read
16
16
  variable_cache(:value) do
17
- normalize(enumerize(type_definition.ensure_type(read_before_type_cast)))
17
+ normalize(type_definition.prepare(read_before_type_cast))
18
18
  end
19
19
  end
20
20
 
@@ -25,31 +25,13 @@ module Granite
25
25
  end
26
26
 
27
27
  def default
28
- defaultizer.is_a?(Proc) ? evaluate(&defaultizer) : defaultizer
28
+ owner.evaluate_if_proc(defaultizer)
29
29
  end
30
30
 
31
31
  def defaultize(value, default_value = nil)
32
32
  !defaultizer.nil? && value.nil? ? default_value || default : value
33
33
  end
34
34
 
35
- def enum
36
- source = enumerizer.is_a?(Proc) ? evaluate(&enumerizer) : enumerizer
37
-
38
- case source
39
- when Range
40
- source.to_a
41
- when Set
42
- source
43
- else
44
- Array.wrap(source)
45
- end.to_set
46
- end
47
-
48
- def enumerize(value)
49
- set = enum if enumerizer
50
- value if !set || (set.none? || set.include?(value))
51
- end
52
-
53
35
  def normalize(value)
54
36
  if normalizers.none?
55
37
  value
@@ -57,7 +39,7 @@ module Granite
57
39
  normalizers.inject(value) do |val, normalizer|
58
40
  case normalizer
59
41
  when Proc
60
- evaluate(val, &normalizer)
42
+ owner.evaluate(normalizer, val)
61
43
  when Hash
62
44
  normalizer.inject(val) do |v, (name, options)|
63
45
  Granite::Form.normalizer(name).call(v, options, self)
@@ -3,13 +3,12 @@ module Granite
3
3
  module Model
4
4
  module Attributes
5
5
  class Base
6
- attr_reader :owner, :reflection
6
+ attr_reader :type_definition
7
+ delegate :type, :reflection, :owner, :enum, to: :type_definition
7
8
  delegate :name, :readonly, to: :reflection
8
- delegate :type, to: :type_definition
9
9
 
10
- def initialize(reflection, owner)
11
- @reflection = reflection
12
- @owner = owner
10
+ def initialize(type_definition)
11
+ @type_definition = type_definition
13
12
  @origin = :default
14
13
  end
15
14
 
@@ -53,11 +52,7 @@ module Granite
53
52
  end
54
53
 
55
54
  def readonly?
56
- !!(readonly.is_a?(Proc) ? evaluate(&readonly) : readonly)
57
- end
58
-
59
- def type_definition
60
- @type_definition ||= build_type_definition(reflection.type)
55
+ !!owner.evaluate(readonly)
61
56
  end
62
57
 
63
58
  def inspect_attribute
@@ -96,19 +91,6 @@ module Granite
96
91
 
97
92
  private
98
93
 
99
- def build_type_definition(type)
100
- Granite::Form.type_for(type).new(type, reflection, owner)
101
- end
102
-
103
- def evaluate(*args, &block)
104
- if block.arity >= 0 && block.arity <= args.length
105
- owner.instance_exec(*args.first(block.arity), &block)
106
- else
107
- args = block.arity.negative? ? args : args.first(block.arity)
108
- yield(*args, owner)
109
- end
110
- end
111
-
112
94
  def remove_variable(*names)
113
95
  names.flatten.each do |name|
114
96
  name = :"@#{name}"
@@ -5,7 +5,7 @@ module Granite
5
5
  class ReferenceMany < ReferenceOne
6
6
  def type_casted_value
7
7
  variable_cache(:value) do
8
- read_before_type_cast.map { |id| type_definition.ensure_type(id) }
8
+ read_before_type_cast.map { |id| type_definition.prepare(id) }
9
9
  end
10
10
  end
11
11
 
@@ -24,7 +24,7 @@ module Granite
24
24
 
25
25
  def type_casted_value
26
26
  variable_cache(:value) do
27
- type_definition.ensure_type(read_before_type_cast)
27
+ type_definition.prepare(read_before_type_cast)
28
28
  end
29
29
  end
30
30
 
@@ -4,6 +4,10 @@ module Granite
4
4
  module Attributes
5
5
  module Reflections
6
6
  class Attribute < Base
7
+ def self.attribute_class
8
+ Granite::Form::Model::Attributes::Attribute
9
+ end
10
+
7
11
  def self.generate_methods(name, target)
8
12
  target.class_eval <<-RUBY, __FILE__, __LINE__ + 1
9
13
  def #{name}
@@ -40,10 +44,6 @@ module Granite
40
44
  @defaultizer ||= options[:default]
41
45
  end
42
46
 
43
- def enumerizer
44
- @enumerizer ||= options[:enum] || options[:in]
45
- end
46
-
47
47
  def normalizers
48
48
  @normalizers ||= Array.wrap(options[:normalize] || options[:normalizer] || options[:normalizers])
49
49
  end
@@ -0,0 +1,38 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Base
7
+ class BuildTypeDefinition
8
+ attr_reader :owner, :reflection
9
+ delegate :name, to: :reflection
10
+
11
+ def initialize(owner, reflection)
12
+ @owner = owner
13
+ @reflection = reflection
14
+ end
15
+
16
+ def call
17
+ raise "Type is not specified for `#{name}`" if type.nil?
18
+
19
+ type_definition_for(type)
20
+ end
21
+
22
+ private
23
+
24
+ def type
25
+ reflection.options[:type]
26
+ end
27
+
28
+ def type_definition_for(type)
29
+ type = type.to_s.camelize.constantize unless type.is_a?(Module)
30
+ Granite::Form.type_for(type).new(type, reflection, owner)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -28,24 +28,26 @@ module Granite
28
28
  end
29
29
 
30
30
  def build_attribute(owner, raw_value = Granite::Form::UNDEFINED)
31
- attribute = self.class.attribute_class.new(self, owner)
31
+ type_definition = self.class::BuildTypeDefinition.new(owner, self).call
32
+ attribute = self.class.attribute_class.new(type_definition)
32
33
  attribute.write_value(raw_value, origin: :persistence) unless raw_value == Granite::Form::UNDEFINED
33
34
  attribute
34
35
  end
35
36
 
36
37
  def type
37
- @type ||= case options[:type]
38
- when Class, Module
39
- options[:type]
40
- when nil
41
- raise "Type is not specified for `#{name}`"
42
- else
43
- options[:type].to_s.camelize.constantize
44
- end
38
+ options[:type]
45
39
  end
46
40
 
47
41
  def readonly
48
- @readonly ||= options[:readonly]
42
+ options[:readonly]
43
+ end
44
+
45
+ def enum
46
+ options[:enum] || options[:in]
47
+ end
48
+
49
+ def keys
50
+ @keys ||= Array.wrap(options[:keys]).map(&:to_s)
49
51
  end
50
52
 
51
53
  def inspect_reflection
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Granite
4
+ module Form
5
+ module Model
6
+ module Attributes
7
+ module Reflections
8
+ class Collection
9
+ class BuildTypeDefinition < Base::BuildTypeDefinition
10
+ def call
11
+ Types::Collection.new(super)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Granite
4
+ module Form
5
+ module Model
6
+ module Attributes
7
+ module Reflections
8
+ class Dictionary
9
+ class BuildTypeDefinition < Base::BuildTypeDefinition
10
+ def call
11
+ Types::Dictionary.new(super)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -4,9 +4,6 @@ module Granite
4
4
  module Attributes
5
5
  module Reflections
6
6
  class Dictionary < Attribute
7
- def keys
8
- @keys ||= Array.wrap(options[:keys]).map(&:to_s)
9
- end
10
7
  end
11
8
  end
12
9
  end
@@ -0,0 +1,73 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Attributes
5
+ module Reflections
6
+ class Represents
7
+ class BuildTypeDefinition < Base::BuildTypeDefinition
8
+ GRANITE_COLLECTION_TYPES = [Granite::Form::Model::Attributes::ReferenceMany].freeze
9
+ TYPES = {
10
+ 'ActiveRecord::Enum::EnumType' => String,
11
+ 'ActiveRecord::Type::Serialized' => Object
12
+ }.freeze
13
+
14
+ def call
15
+ if type.present?
16
+ super
17
+ else
18
+ granite_form_type || active_record_type || type_definition_for(Object)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def reference
25
+ owner.__send__(reflection.reference)
26
+ end
27
+
28
+ def granite_form_type
29
+ return nil unless reference.is_a?(Model)
30
+
31
+ reference_attribute = reference.attribute(name)
32
+ return nil if reference_attribute.nil?
33
+
34
+ type_definition = reference_attribute.type_definition.build_duplicate(reflection, owner)
35
+ if GRANITE_COLLECTION_TYPES.any? { |klass| reference_attribute.is_a? klass }
36
+ Types::Collection.new(type_definition)
37
+ else
38
+ type_definition
39
+ end
40
+ end
41
+
42
+ def active_record_type
43
+ return nil unless reference.respond_to?(:type_for_attribute)
44
+
45
+ attribute_type = reference.type_for_attribute(active_model_attribute_name.to_s)
46
+
47
+ attribute_type_name = attribute_type.class.to_s
48
+ if TYPES.key?(attribute_type_name)
49
+ type_definition_for(TYPES[attribute_type_name])
50
+ elsif attribute_type.respond_to?(:subtype)
51
+ Types::Collection.new(convert_active_model_type_to_definition(attribute_type.subtype))
52
+ else
53
+ convert_active_model_type_to_definition(attribute_type)
54
+ end
55
+ end
56
+
57
+ def active_model_attribute_name
58
+ aliases = reference.class.try(:attribute_aliases) || {}
59
+ aliases.fetch(name.to_s, name)
60
+ end
61
+
62
+ def convert_active_model_type_to_definition(attribute_type)
63
+ type = attribute_type.try(:value_class) ||
64
+ Associations::PersistenceAdapters::ActiveRecord::TYPES[attribute_type.type&.to_sym]
65
+ type_definition_for(type) if type
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -4,14 +4,22 @@ module Granite
4
4
  module Attributes
5
5
  module Reflections
6
6
  class Represents < Attribute
7
+ def self.attribute_class
8
+ Attributes::Represents
9
+ end
10
+
7
11
  def self.build(target, generated_methods, name, *args, &block)
8
12
  options = args.extract_options!
9
13
 
10
14
  reference = target.reflect_on_association(options[:of]) if target.respond_to?(:reflect_on_association)
11
15
  reference ||= target.reflect_on_attribute(options[:of]) if target.respond_to?(:reflect_on_attribute)
12
16
  options[:of] = reference.name if reference
13
- validates_nested = target.respond_to?(:validates_nested) && !target.validates_nested?(options[:of])
14
- target.validates_nested(options[:of]) if validates_nested
17
+
18
+ if options.fetch(:validate_reference, true)
19
+ validates_nested = target.respond_to?(:validates_nested) && !target.validates_nested?(options[:of])
20
+ target.validates_nested(options[:of]) if validates_nested
21
+ target.validates_presence_of(options[:of]) unless target.validates_presence?(options[:of])
22
+ end
15
23
 
16
24
  super(target, generated_methods, name, *args, options, &block)
17
25
  end
@@ -5,60 +5,45 @@ module Granite
5
5
  class Represents < Attribute
6
6
  delegate :reader, :reader_before_type_cast, :writer, to: :reflection
7
7
 
8
- def write(value)
9
- return if readonly?
10
- pollute do
11
- reset
12
- reference.send(writer, value)
13
- end
14
- end
15
-
16
- def reset
8
+ def initialize(*_args)
17
9
  super
18
- remove_variable(:cached_value, :cached_value_before_type_cast)
10
+
11
+ set_default_value
12
+ set_default_value_before_type_cast
19
13
  end
20
14
 
21
- def read
22
- reset if cached_value != read_value
23
- variable_cache(:value) do
24
- normalize(enumerize(defaultize(cached_value, read_before_type_cast)))
25
- end
15
+ def sync
16
+ reference.public_send(writer, read) if reference.respond_to?(writer)
26
17
  end
27
18
 
28
- def read_before_type_cast
29
- reset if cached_value_before_type_cast != read_value_before_type_cast
30
- variable_cache(:value_before_type_cast) do
31
- defaultize(cached_value_before_type_cast)
19
+ def changed?
20
+ if reflection.options.key?(:default)
21
+ reference.public_send(reader) != read
22
+ else
23
+ owner.public_send("#{name}_changed?")
32
24
  end
33
25
  end
34
26
 
35
27
  private
36
28
 
37
29
  def reference
38
- owner.send(reflection.reference)
30
+ owner.__send__(reflection.reference)
39
31
  end
40
32
 
41
- def read_value
42
- ref = reference
43
- ref.public_send(reader) if ref
44
- end
45
-
46
- def cached_value
47
- variable_cache(:cached_value) { read_value }
48
- end
33
+ def set_default_value
34
+ return unless reference.respond_to?(reader)
49
35
 
50
- def read_value_before_type_cast
51
- ref = reference
52
- return unless ref
53
- if ref.respond_to?(reader_before_type_cast)
54
- ref.public_send(reader_before_type_cast)
55
- else
56
- ref.public_send(reader)
36
+ variable_cache(:value) do
37
+ normalize(type_definition.prepare(defaultize(reference.public_send(reader))))
57
38
  end
58
39
  end
59
40
 
60
- def cached_value_before_type_cast
61
- variable_cache(:cached_value_before_type_cast) { read_value_before_type_cast }
41
+ def set_default_value_before_type_cast
42
+ return unless reference.respond_to?(reader_before_type_cast)
43
+
44
+ variable_cache(:value_before_type_cast) do
45
+ defaultize(reference.public_send(reader_before_type_cast))
46
+ end
62
47
  end
63
48
  end
64
49
  end
@@ -1,12 +1,13 @@
1
1
  require 'granite/form/model/attributes/reflections/base'
2
+ require 'granite/form/model/attributes/reflections/base/build_type_definition'
2
3
  require 'granite/form/model/attributes/reflections/attribute'
3
4
  require 'granite/form/model/attributes/reflections/collection'
5
+ require 'granite/form/model/attributes/reflections/collection/build_type_definition'
4
6
  require 'granite/form/model/attributes/reflections/dictionary'
7
+ require 'granite/form/model/attributes/reflections/dictionary/build_type_definition'
5
8
 
6
9
  require 'granite/form/model/attributes/base'
7
10
  require 'granite/form/model/attributes/attribute'
8
- require 'granite/form/model/attributes/collection'
9
- require 'granite/form/model/attributes/dictionary'
10
11
 
11
12
  module Granite
12
13
  module Form
@@ -178,6 +179,13 @@ module Granite
178
179
 
179
180
  alias_method :attributes=, :assign_attributes
180
181
 
182
+ def sync_attributes
183
+ attribute_names.each do |name|
184
+ attr = attribute(name)
185
+ attr.try(:sync) if attr.try(:changed?)
186
+ end
187
+ end
188
+
181
189
  def inspect
182
190
  "#<#{self.class.send(:original_inspect)} #{attributes_for_inspect.presence || '(no attributes)'}>"
183
191
  end
@@ -1,5 +1,6 @@
1
1
  require 'active_model/version'
2
2
  require 'granite/form/model/attributes/reflections/represents'
3
+ require 'granite/form/model/attributes/reflections/represents/build_type_definition'
3
4
  require 'granite/form/model/attributes/represents'
4
5
 
5
6
  module Granite
@@ -12,6 +12,12 @@ module Granite
12
12
  alias_method :validate, :valid?
13
13
  end
14
14
 
15
+ class_methods do
16
+ def validates_presence?(attr)
17
+ _validators[attr.to_sym].grep(ActiveModel::Validations::PresenceValidator).present?
18
+ end
19
+ end
20
+
15
21
  def validate!(context = nil)
16
22
  valid?(context) || raise_validation_error
17
23
  end