granite-form 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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