granite-form 0.1.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 (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.github/workflows/ci.yml +35 -0
  4. data/.github/workflows/main.yml +29 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +64 -0
  8. data/.rubocop_todo.yml +48 -0
  9. data/Appraisals +8 -0
  10. data/CHANGELOG.md +73 -0
  11. data/Gemfile +8 -0
  12. data/Guardfile +77 -0
  13. data/LICENSE +22 -0
  14. data/README.md +429 -0
  15. data/Rakefile +6 -0
  16. data/gemfiles/rails.4.2.gemfile +15 -0
  17. data/gemfiles/rails.5.0.gemfile +15 -0
  18. data/gemfiles/rails.5.1.gemfile +15 -0
  19. data/gemfiles/rails.5.2.gemfile +15 -0
  20. data/gemfiles/rails.6.0.gemfile +14 -0
  21. data/gemfiles/rails.6.1.gemfile +14 -0
  22. data/gemfiles/rails.7.0.gemfile +14 -0
  23. data/granite-form.gemspec +31 -0
  24. data/lib/granite/form/active_record/associations.rb +57 -0
  25. data/lib/granite/form/active_record/nested_attributes.rb +20 -0
  26. data/lib/granite/form/base.rb +15 -0
  27. data/lib/granite/form/config.rb +42 -0
  28. data/lib/granite/form/errors.rb +111 -0
  29. data/lib/granite/form/extensions.rb +36 -0
  30. data/lib/granite/form/model/associations/base.rb +97 -0
  31. data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
  32. data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
  33. data/lib/granite/form/model/associations/embeds_any.rb +19 -0
  34. data/lib/granite/form/model/associations/embeds_many.rb +152 -0
  35. data/lib/granite/form/model/associations/embeds_one.rb +112 -0
  36. data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
  37. data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
  38. data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
  39. data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
  40. data/lib/granite/form/model/associations/references_any.rb +43 -0
  41. data/lib/granite/form/model/associations/references_many.rb +113 -0
  42. data/lib/granite/form/model/associations/references_one.rb +88 -0
  43. data/lib/granite/form/model/associations/reflections/base.rb +92 -0
  44. data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
  45. data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
  46. data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
  47. data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
  48. data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
  49. data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
  50. data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
  51. data/lib/granite/form/model/associations/validations.rb +41 -0
  52. data/lib/granite/form/model/associations.rb +120 -0
  53. data/lib/granite/form/model/attributes/attribute.rb +75 -0
  54. data/lib/granite/form/model/attributes/base.rb +134 -0
  55. data/lib/granite/form/model/attributes/collection.rb +19 -0
  56. data/lib/granite/form/model/attributes/dictionary.rb +28 -0
  57. data/lib/granite/form/model/attributes/localized.rb +44 -0
  58. data/lib/granite/form/model/attributes/reference_many.rb +21 -0
  59. data/lib/granite/form/model/attributes/reference_one.rb +52 -0
  60. data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
  61. data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
  62. data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
  63. data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
  64. data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
  65. data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
  66. data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
  67. data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
  68. data/lib/granite/form/model/attributes/represents.rb +67 -0
  69. data/lib/granite/form/model/attributes.rb +204 -0
  70. data/lib/granite/form/model/callbacks.rb +72 -0
  71. data/lib/granite/form/model/conventions.rb +40 -0
  72. data/lib/granite/form/model/dirty.rb +84 -0
  73. data/lib/granite/form/model/lifecycle.rb +309 -0
  74. data/lib/granite/form/model/localization.rb +26 -0
  75. data/lib/granite/form/model/persistence.rb +59 -0
  76. data/lib/granite/form/model/primary.rb +59 -0
  77. data/lib/granite/form/model/representation.rb +101 -0
  78. data/lib/granite/form/model/scopes.rb +118 -0
  79. data/lib/granite/form/model/validations/associated.rb +22 -0
  80. data/lib/granite/form/model/validations/nested.rb +56 -0
  81. data/lib/granite/form/model/validations.rb +29 -0
  82. data/lib/granite/form/model.rb +33 -0
  83. data/lib/granite/form/railtie.rb +9 -0
  84. data/lib/granite/form/undefined_class.rb +11 -0
  85. data/lib/granite/form/version.rb +5 -0
  86. data/lib/granite/form.rb +163 -0
  87. data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
  88. data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
  89. data/spec/lib/granite/form/config_spec.rb +66 -0
  90. data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
  91. data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
  92. data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
  93. data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  94. data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
  95. data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
  96. data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
  97. data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
  98. data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
  99. data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
  100. data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
  101. data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
  102. data/spec/lib/granite/form/model/associations_spec.rb +198 -0
  103. data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
  104. data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
  105. data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
  106. data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
  107. data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
  108. data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
  109. data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
  110. data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
  111. data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
  112. data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
  113. data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
  114. data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
  115. data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
  116. data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
  117. data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
  118. data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
  119. data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
  120. data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
  121. data/spec/lib/granite/form/model/primary_spec.rb +84 -0
  122. data/spec/lib/granite/form/model/representation_spec.rb +139 -0
  123. data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
  124. data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
  125. data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
  126. data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
  127. data/spec/lib/granite/form/model/validations_spec.rb +31 -0
  128. data/spec/lib/granite/form/model_spec.rb +10 -0
  129. data/spec/lib/granite/form_spec.rb +11 -0
  130. data/spec/shared/nested_attribute_examples.rb +332 -0
  131. data/spec/spec_helper.rb +50 -0
  132. data/spec/support/model_helpers.rb +10 -0
  133. data/spec/support/muffle_helper.rb +7 -0
  134. metadata +403 -0
@@ -0,0 +1,215 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ module NestedAttributes
6
+ extend ActiveSupport::Concern
7
+
8
+ DESTROY_ATTRIBUTE = '_destroy'.freeze
9
+
10
+ included do
11
+ class_attribute :nested_attributes_options, instance_writer: false
12
+ self.nested_attributes_options = {}
13
+
14
+ extend NestedAttributesMethodsExtension
15
+ prepend PrependMethods
16
+ end
17
+
18
+ module PrependMethods
19
+ def assign_attributes(attrs)
20
+ if self.class.nested_attributes_options.present?
21
+ attrs = attrs.to_unsafe_hash if attrs.respond_to?(:to_unsafe_hash)
22
+ attrs = attrs.stringify_keys
23
+
24
+ nested_attrs = self.class.nested_attributes_options.keys
25
+ .each_with_object({}) do |association_name, result|
26
+ name = "#{association_name}_attributes"
27
+ result[name] = attrs.delete(name) if attrs.key?(name)
28
+ end
29
+
30
+ super(attrs.merge!(nested_attrs))
31
+ else
32
+ super(attrs)
33
+ end
34
+ end
35
+
36
+ alias_method :attributes=, :assign_attributes
37
+ end
38
+
39
+ class NestedAttributesMethods
40
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == DESTROY_ATTRIBUTE || value.blank? } }
41
+
42
+ def self.accepts_nested_attributes_for(klass, *attr_names)
43
+ options = {allow_destroy: false, update_only: false}
44
+ options.update(attr_names.extract_options!)
45
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
46
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
47
+
48
+ NestedAttributesMethodsExtension.ensure_extended!(klass)
49
+
50
+ attr_names.each do |association_name|
51
+ reflection = klass.reflect_on_association(association_name)
52
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" unless reflection
53
+ klass.nested_attributes_options = klass.nested_attributes_options.merge(association_name.to_sym => options)
54
+
55
+ should_validate_nested = klass.respond_to?(:validates_nested) && !klass.validates_nested?(association_name)
56
+ klass.validates_nested(association_name) if should_validate_nested
57
+
58
+ type = (reflection.collection? ? :collection : :one_to_one)
59
+ klass.nested_attributes_methods_module.class_eval <<-METHOD, __FILE__, __LINE__ + 1
60
+ def #{association_name}_attributes=(attributes)
61
+ Granite::Form::Model::Associations::NestedAttributes::NestedAttributesMethods
62
+ .assign_nested_attributes_for_#{type}_association(self, :#{association_name}, attributes)
63
+ end
64
+ METHOD
65
+ end
66
+ end
67
+
68
+ def self.assign_nested_attributes_for_one_to_one_association(object, association_name, attributes)
69
+ options = object.nested_attributes_options[association_name]
70
+ attributes = attributes.with_indifferent_access
71
+
72
+ association = object.association(association_name)
73
+ existing_record = association.target
74
+ primary_attribute_name = primary_name_for(association.reflection.klass)
75
+ if existing_record
76
+ primary_attribute = existing_record.attribute(primary_attribute_name)
77
+ primary_attribute_value = primary_attribute.typecast(attributes[primary_attribute_name]) if primary_attribute
78
+ end
79
+
80
+ if existing_record && (!primary_attribute || options[:update_only] || existing_record.primary_attribute == primary_attribute_value)
81
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(object, association_name, attributes)
82
+ elsif attributes[primary_attribute_name].present?
83
+ raise Granite::Form::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
84
+ elsif !reject_new_object?(object, association_name, attributes, options)
85
+ assignable_attributes = attributes.except(*unassignable_keys(object))
86
+
87
+ if existing_record && !existing_record.persisted?
88
+ existing_record.assign_attributes(assignable_attributes)
89
+ else
90
+ association.build(assignable_attributes)
91
+ end
92
+ end
93
+ end
94
+
95
+ def self.assign_nested_attributes_for_collection_association(object, association_name, attributes_collection)
96
+ options = object.nested_attributes_options[association_name]
97
+
98
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
99
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
100
+ end
101
+
102
+ check_record_limit!(options[:limit], attributes_collection)
103
+
104
+ association = object.association(association_name)
105
+ primary_attribute_name = primary_name_for(association.reflection.klass)
106
+
107
+ raise Granite::Form::UndefinedPrimaryAttribute.new(object.class, association_name) unless primary_attribute_name
108
+
109
+ if attributes_collection.is_a? Hash
110
+ keys = attributes_collection.keys
111
+ attributes_collection = if keys.include?(primary_attribute_name) || keys.include?(primary_attribute_name.to_sym)
112
+ [attributes_collection]
113
+ else
114
+ attributes_collection.values
115
+ end
116
+ end
117
+
118
+ attributes_collection.each do |attributes|
119
+ attributes = attributes.with_indifferent_access
120
+
121
+ if attributes[primary_attribute_name].blank?
122
+ association.build(attributes.except(*unassignable_keys(object))) unless reject_new_object?(object, association_name, attributes, options)
123
+ else
124
+ existing_record = association.target.detect do |record|
125
+ primary_attribute_value = record.attribute(primary_attribute_name)
126
+ .typecast(attributes[primary_attribute_name])
127
+ record.primary_attribute == primary_attribute_value
128
+ end
129
+ if existing_record
130
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(object, association_name, attributes)
131
+ elsif association.reflection.embedded?
132
+ unless reject_new_object?(object, association_name, attributes, options)
133
+ association.reflection.klass.with_sanitize(false) do
134
+ association.build(attributes.except(DESTROY_ATTRIBUTE))
135
+ end
136
+ end
137
+ else
138
+ raise Granite::Form::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ def self.check_record_limit!(limit, attributes_collection)
145
+ limit = case limit
146
+ when Symbol
147
+ send(limit)
148
+ when Proc
149
+ limit.call
150
+ else
151
+ limit
152
+ end
153
+
154
+ return unless limit && attributes_collection.size > limit
155
+
156
+ raise Granite::Form::TooManyObjects.new(limit, attributes_collection.size)
157
+ end
158
+
159
+ def self.assign_to_or_mark_for_destruction(object, attributes, allow_destroy)
160
+ object.assign_attributes(attributes.except(*unassignable_keys(object)))
161
+ object.mark_for_destruction if destroy_flag?(attributes) && allow_destroy
162
+ end
163
+
164
+ def self.destroy_flag?(hash)
165
+ Granite::Form.typecaster(Boolean).call(hash[DESTROY_ATTRIBUTE])
166
+ end
167
+
168
+ def self.reject_new_object?(object, association_name, attributes, options)
169
+ options[:update_only] || destroy_flag?(attributes) || call_reject_if(object, association_name, attributes)
170
+ end
171
+
172
+ def self.call_reject_if(object, association_name, attributes)
173
+ return false if destroy_flag?(attributes)
174
+ case callback = object.nested_attributes_options[association_name][:reject_if]
175
+ when Symbol
176
+ method(callback).arity.zero? ? send(callback) : send(callback, attributes)
177
+ when Proc
178
+ callback.call(attributes)
179
+ end
180
+ end
181
+
182
+ def self.unassignable_keys(object)
183
+ [primary_name_for(object.class), DESTROY_ATTRIBUTE].compact
184
+ end
185
+
186
+ def self.primary_name_for(klass)
187
+ klass < Granite::Form::Model ? klass.primary_name : 'id'
188
+ end
189
+ end
190
+
191
+ module ClassMethods
192
+ def accepts_nested_attributes_for(*attr_names)
193
+ NestedAttributesMethods.accepts_nested_attributes_for self, *attr_names
194
+ end
195
+ end
196
+
197
+ module NestedAttributesMethodsExtension
198
+ def self.ensure_extended!(klass)
199
+ return if klass.singleton_class.ancestors.include?(self)
200
+ klass.extend(self)
201
+ end
202
+
203
+ def nested_attributes_methods_module
204
+ @nested_attributes_methods_module ||= begin
205
+ mod = const_set(:NestedAttributesMethods, Module.new)
206
+ include(mod)
207
+ mod
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,33 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ module PersistenceAdapters
6
+ class ActiveRecord < Base
7
+ class ReferencedProxy < Granite::Form::Model::Associations::Collection::Proxy
8
+ # You can't create data directly through ActiveRecord::Relation
9
+ METHODS_EXCLUDED_FROM_DELEGATION = %w[build create create!].map(&:to_sym).freeze
10
+
11
+ attr_reader :association
12
+ delegate :scope, to: :@association
13
+
14
+ def method_missing(method, *args, &block)
15
+ delegate_to_scope?(method) ? scope.send(method, *args, &block) : super
16
+ end
17
+
18
+ def respond_to_missing?(method, include_private = false)
19
+ delegate_to_scope?(method) || super
20
+ end
21
+
22
+ private
23
+
24
+ def delegate_to_scope?(method)
25
+ METHODS_EXCLUDED_FROM_DELEGATION.exclude?(method) && scope.respond_to?(method)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ require 'granite/form/model/associations/persistence_adapters/active_record/referenced_proxy'
2
+
3
+ module Granite
4
+ module Form
5
+ module Model
6
+ module Associations
7
+ module PersistenceAdapters
8
+ class ActiveRecord < Base
9
+ TYPES = {
10
+ integer: Integer,
11
+ float: Float,
12
+ decimal: BigDecimal,
13
+ datetime: Time,
14
+ timestamp: Time,
15
+ time: Time,
16
+ date: Date,
17
+ text: String,
18
+ string: String,
19
+ binary: String,
20
+ boolean: Boolean
21
+ }.freeze
22
+
23
+ alias_method :data_type, :data_source
24
+
25
+ def build(attributes)
26
+ data_source.new(attributes)
27
+ end
28
+
29
+ def persist(object, raise_error: false)
30
+ raise_error ? object.save! : object.save
31
+ end
32
+
33
+ def scope(owner, source)
34
+ scope = data_source.unscoped
35
+
36
+ if scope_proc
37
+ scope = if scope_proc.arity.zero?
38
+ scope.instance_exec(&scope_proc)
39
+ else
40
+ scope.instance_exec(owner, &scope_proc)
41
+ end
42
+ end
43
+
44
+ scope.where(primary_key => source)
45
+ end
46
+
47
+ def identify(object)
48
+ object[primary_key] if object
49
+ end
50
+
51
+ def primary_key
52
+ @primary_key ||= :id
53
+ end
54
+
55
+ def primary_key_type
56
+ column = data_source.columns_hash[primary_key.to_s]
57
+ TYPES[column.type]
58
+ end
59
+
60
+ def referenced_proxy(association)
61
+ ReferencedProxy.new(association)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,55 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ module PersistenceAdapters
6
+ class Base
7
+ attr_reader :data_source, :primary_key, :scope_proc
8
+
9
+ def initialize(data_source, primary_key, scope_proc = nil)
10
+ @data_source = data_source
11
+ @primary_key = primary_key
12
+ @scope_proc = scope_proc
13
+ end
14
+
15
+ def build(_attributes)
16
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Build new instance of data object by attributes'
17
+ end
18
+
19
+ def persist(_object, *)
20
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Build new instance of data object by attributes'
21
+ end
22
+
23
+ def scope(_owner, _source)
24
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Better to be Enumerable'
25
+ end
26
+
27
+ def find_one(owner, identificator)
28
+ scope(owner, identificator).first
29
+ end
30
+
31
+ def find_all(owner, identificators)
32
+ scope(owner, identificators).to_a
33
+ end
34
+
35
+ def identify(_object)
36
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Field to be used as primary_key for object'
37
+ end
38
+
39
+ def data_type
40
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Type of data object for type_check'
41
+ end
42
+
43
+ def primary_key_type
44
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Ruby data type'
45
+ end
46
+
47
+ def referenced_proxy
48
+ raise NotImplementedError, 'Should be implemented in inhereted adapter. Object to manage proxying of methods to scope.'
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ class ReferencesAny < Base
6
+ def scope(source = read_source)
7
+ reflection.persistence_adapter.scope(owner, source)
8
+ end
9
+
10
+ private
11
+
12
+ def read_source
13
+ attribute.read_before_type_cast
14
+ end
15
+
16
+ def write_source(value)
17
+ attribute.write_value value
18
+ end
19
+
20
+ def attribute
21
+ @attribute ||= owner.attribute(reflection.reference_key)
22
+ end
23
+
24
+ def build_object(attributes)
25
+ reflection.persistence_adapter.build(attributes)
26
+ end
27
+
28
+ def persist_object(object, **options)
29
+ reflection.persistence_adapter.persist(object, **options)
30
+ end
31
+
32
+ def matches_type?(object)
33
+ object.is_a?(reflection.persistence_adapter.data_type)
34
+ end
35
+
36
+ def raise_type_mismatch(object)
37
+ raise AssociationTypeMismatch.new(reflection.persistence_adapter.data_type, object.class)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,113 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ class ReferencesMany < ReferencesAny
6
+ def build(attributes = {})
7
+ append([build_object(attributes)]).last
8
+ end
9
+
10
+ def create(attributes = {})
11
+ object = build(attributes)
12
+ persist_object(object)
13
+ object
14
+ end
15
+
16
+ def create!(attributes = {})
17
+ object = build(attributes)
18
+ persist_object(object, raise_error: true)
19
+ object
20
+ end
21
+
22
+ def apply_changes
23
+ target.all? do |object|
24
+ if object
25
+ if object.marked_for_destruction? && reflection.autosave?
26
+ object.destroy
27
+ elsif object.new_record? || (reflection.autosave? && object.changed?)
28
+ persist_object(object)
29
+ else
30
+ true
31
+ end
32
+ else
33
+ true
34
+ end
35
+ end
36
+ end
37
+
38
+ def target=(object)
39
+ loaded!
40
+ @target = object.to_a
41
+ end
42
+
43
+ def load_target
44
+ source = read_source
45
+ source.present? ? reflection.persistence_adapter.find_all(owner, source) : default
46
+ end
47
+
48
+ def default
49
+ return [] if evar_loaded?
50
+
51
+ default = Array.wrap(reflection.default(owner))
52
+
53
+ return [] unless default
54
+
55
+ if default.all? { |object| object.is_a?(reflection.persistence_adapter.data_type) }
56
+ default
57
+ elsif default.all? { |object| object.is_a?(Hash) }
58
+ default.map { |attributes| build_object(attributes) }
59
+ else
60
+ reflection.persistence_adapter.find_all(owner, default)
61
+ end || []
62
+ end
63
+
64
+ def reader(force_reload = false)
65
+ reload if force_reload
66
+ @proxy ||= reflection.persistence_adapter.referenced_proxy(self)
67
+ end
68
+
69
+ def replace(objects)
70
+ loaded!
71
+ transaction do
72
+ clear
73
+ append objects
74
+ end
75
+ end
76
+
77
+ alias_method :writer, :replace
78
+
79
+ def concat(*objects)
80
+ append objects.flatten
81
+ reader
82
+ end
83
+
84
+ def clear
85
+ attribute.pollute do
86
+ write_source([])
87
+ end
88
+ reload.empty?
89
+ end
90
+
91
+ def identify
92
+ target.map { |obj| reflection.persistence_adapter.identify(obj) }
93
+ end
94
+
95
+ private
96
+
97
+ def append(objects)
98
+ attribute.pollute do
99
+ objects.each do |object|
100
+ next if target.include?(object)
101
+ raise_type_mismatch(object) unless matches_type?(object)
102
+
103
+ target.push(object)
104
+ write_source(identify)
105
+ end
106
+ end
107
+ target
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,88 @@
1
+ module Granite
2
+ module Form
3
+ module Model
4
+ module Associations
5
+ class ReferencesOne < ReferencesAny
6
+ def build(attributes = {})
7
+ replace(build_object(attributes))
8
+ end
9
+
10
+ def create(attributes = {})
11
+ persist_object(build(attributes))
12
+ target
13
+ end
14
+
15
+ def create!(attributes = {})
16
+ persist_object(build(attributes), raise_error: true)
17
+ target
18
+ end
19
+
20
+ def apply_changes
21
+ if target
22
+ if target.marked_for_destruction? && reflection.autosave?
23
+ target.destroy
24
+ elsif target.new_record? || (reflection.autosave? && target.changed?)
25
+ persist_object(target)
26
+ else
27
+ true
28
+ end
29
+ else
30
+ true
31
+ end
32
+ end
33
+
34
+ def target=(object)
35
+ loaded!
36
+ @target = object
37
+ end
38
+
39
+ def load_target
40
+ source = read_source
41
+ source ? reflection.persistence_adapter.find_one(owner, source) : default
42
+ end
43
+
44
+ def default
45
+ return if evar_loaded?
46
+
47
+ default = reflection.default(owner)
48
+
49
+ return unless default
50
+
51
+ case default
52
+ when reflection.persistence_adapter.data_type
53
+ default
54
+ when Hash
55
+ build_object(default)
56
+ else
57
+ reflection.persistence_adapter.find_one(owner, default)
58
+ end
59
+ end
60
+
61
+ def reader(force_reload = false)
62
+ reset if force_reload
63
+ target
64
+ end
65
+
66
+ def replace(object)
67
+ raise_type_mismatch(object) unless object.nil? || matches_type?(object)
68
+
69
+ transaction do
70
+ attribute.pollute do
71
+ self.target = object
72
+ write_source identify
73
+ end
74
+ end
75
+
76
+ target
77
+ end
78
+
79
+ alias_method :writer, :replace
80
+
81
+ def identify
82
+ reflection.persistence_adapter.identify(target)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end