activeentity 0.0.1.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +42 -0
  3. data/README.md +145 -0
  4. data/Rakefile +29 -0
  5. data/lib/active_entity.rb +73 -0
  6. data/lib/active_entity/aggregations.rb +276 -0
  7. data/lib/active_entity/associations.rb +146 -0
  8. data/lib/active_entity/associations/embedded/association.rb +134 -0
  9. data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
  10. data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
  11. data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
  12. data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
  13. data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
  14. data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
  15. data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
  16. data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
  17. data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
  18. data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
  19. data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
  20. data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
  21. data/lib/active_entity/attribute_assignment.rb +85 -0
  22. data/lib/active_entity/attribute_decorators.rb +90 -0
  23. data/lib/active_entity/attribute_methods.rb +330 -0
  24. data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
  26. data/lib/active_entity/attribute_methods/query.rb +35 -0
  27. data/lib/active_entity/attribute_methods/read.rb +47 -0
  28. data/lib/active_entity/attribute_methods/serialization.rb +90 -0
  29. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
  30. data/lib/active_entity/attribute_methods/write.rb +63 -0
  31. data/lib/active_entity/attributes.rb +165 -0
  32. data/lib/active_entity/base.rb +303 -0
  33. data/lib/active_entity/coders/json.rb +15 -0
  34. data/lib/active_entity/coders/yaml_column.rb +50 -0
  35. data/lib/active_entity/core.rb +281 -0
  36. data/lib/active_entity/define_callbacks.rb +17 -0
  37. data/lib/active_entity/enum.rb +234 -0
  38. data/lib/active_entity/errors.rb +80 -0
  39. data/lib/active_entity/gem_version.rb +17 -0
  40. data/lib/active_entity/inheritance.rb +278 -0
  41. data/lib/active_entity/integration.rb +78 -0
  42. data/lib/active_entity/locale/en.yml +45 -0
  43. data/lib/active_entity/model_schema.rb +115 -0
  44. data/lib/active_entity/nested_attributes.rb +592 -0
  45. data/lib/active_entity/readonly_attributes.rb +47 -0
  46. data/lib/active_entity/reflection.rb +441 -0
  47. data/lib/active_entity/serialization.rb +25 -0
  48. data/lib/active_entity/store.rb +242 -0
  49. data/lib/active_entity/translation.rb +24 -0
  50. data/lib/active_entity/type.rb +73 -0
  51. data/lib/active_entity/type/date.rb +9 -0
  52. data/lib/active_entity/type/date_time.rb +9 -0
  53. data/lib/active_entity/type/decimal_without_scale.rb +15 -0
  54. data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
  55. data/lib/active_entity/type/internal/timezone.rb +17 -0
  56. data/lib/active_entity/type/json.rb +30 -0
  57. data/lib/active_entity/type/modifiers/array.rb +72 -0
  58. data/lib/active_entity/type/registry.rb +92 -0
  59. data/lib/active_entity/type/serialized.rb +71 -0
  60. data/lib/active_entity/type/text.rb +11 -0
  61. data/lib/active_entity/type/time.rb +21 -0
  62. data/lib/active_entity/type/type_map.rb +62 -0
  63. data/lib/active_entity/type/unsigned_integer.rb +17 -0
  64. data/lib/active_entity/validate_embedded_association.rb +305 -0
  65. data/lib/active_entity/validations.rb +50 -0
  66. data/lib/active_entity/validations/absence.rb +25 -0
  67. data/lib/active_entity/validations/associated.rb +60 -0
  68. data/lib/active_entity/validations/length.rb +26 -0
  69. data/lib/active_entity/validations/presence.rb +68 -0
  70. data/lib/active_entity/validations/subset.rb +76 -0
  71. data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
  72. data/lib/active_entity/version.rb +10 -0
  73. data/lib/tasks/active_entity_tasks.rake +6 -0
  74. metadata +155 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class Json < ActiveModel::Type::Value
6
+ include ActiveModel::Type::Helpers::Mutable
7
+
8
+ def type
9
+ :json
10
+ end
11
+
12
+ def deserialize(value)
13
+ return value unless value.is_a?(::String)
14
+ ActiveSupport::JSON.decode(value) rescue nil
15
+ end
16
+
17
+ def serialize(value)
18
+ ActiveSupport::JSON.encode(value) unless value.nil?
19
+ end
20
+
21
+ def changed_in_place?(raw_old_value, new_value)
22
+ deserialize(raw_old_value) != new_value
23
+ end
24
+
25
+ def accessor
26
+ ActiveEntity::Store::StringKeyedHashAccessor
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ module Modifiers
6
+ class Array < ActiveModel::Type::Value # :nodoc:
7
+ include ActiveModel::Type::Helpers::Mutable
8
+
9
+ attr_reader :subtype, :delimiter
10
+ delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
11
+
12
+ def initialize(subtype, delimiter = ",")
13
+ @subtype = subtype
14
+ @delimiter = delimiter
15
+ end
16
+
17
+ def deserialize(value)
18
+ case value
19
+ when ::String
20
+ type_cast_array(value.split(@delimiter), :deserialize)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def cast(value)
27
+ if value.is_a?(::String)
28
+ value = value.split(@delimiter)
29
+ end
30
+ type_cast_array(value, :cast)
31
+ end
32
+
33
+ def serialize(value)
34
+ if value.is_a?(::Array)
35
+ casted_values = type_cast_array(value, :serialize)
36
+ casted_values.join(@delimiter)
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def ==(other)
43
+ other.is_a?(Array) &&
44
+ subtype == other.subtype &&
45
+ delimiter == other.delimiter
46
+ end
47
+
48
+ def map(value, &block)
49
+ value.map(&block)
50
+ end
51
+
52
+ def changed_in_place?(raw_old_value, new_value)
53
+ deserialize(raw_old_value) != new_value
54
+ end
55
+
56
+ def force_equality?(value)
57
+ value.is_a?(::Array)
58
+ end
59
+
60
+ private
61
+
62
+ def type_cast_array(value, method)
63
+ if value.is_a?(::Array)
64
+ value.map { |item| type_cast_array(item, method) }
65
+ else
66
+ @subtype.public_send(method, value)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/type/registry"
4
+
5
+ module ActiveEntity
6
+ # :stopdoc:
7
+ module Type
8
+ class Registry < ActiveModel::Type::Registry
9
+ def add_modifier(options, klass)
10
+ registrations << DecorationRegistration.new(options, klass)
11
+ end
12
+
13
+ private
14
+
15
+ def registration_klass
16
+ Registration
17
+ end
18
+
19
+ def find_registration(symbol, *args)
20
+ registrations
21
+ .select { |registration| registration.matches?(symbol, *args) }
22
+ .max
23
+ end
24
+ end
25
+
26
+ class Registration
27
+ def initialize(name, block, override: nil)
28
+ @name = name
29
+ @block = block
30
+ @override = override
31
+ end
32
+
33
+ def call(_registry, *args, **kwargs)
34
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
35
+ block.call(*args, **kwargs)
36
+ else
37
+ block.call(*args)
38
+ end
39
+ end
40
+
41
+ def matches?(type_name, *args, **kwargs)
42
+ type_name == name
43
+ end
44
+
45
+ def <=>(other)
46
+ priority <=> other.priority
47
+ end
48
+
49
+ protected
50
+
51
+ attr_reader :name, :block, :override
52
+
53
+ def priority
54
+ override ? 1 : 0
55
+ end
56
+ end
57
+
58
+ class DecorationRegistration < Registration
59
+ def initialize(options, klass)
60
+ @options = options
61
+ @klass = klass
62
+ end
63
+
64
+ def call(registry, *args, **kwargs)
65
+ subtype = registry.lookup(*args, **kwargs.except(*options.keys))
66
+ klass.new(subtype)
67
+ end
68
+
69
+ def matches?(*args, **kwargs)
70
+ matches_options?(**kwargs)
71
+ end
72
+
73
+ def priority
74
+ super | 4
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :options, :klass
80
+
81
+ def matches_options?(**kwargs)
82
+ options.all? do |key, value|
83
+ kwargs[key] == value
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ class TypeConflictError < StandardError
90
+ end
91
+ # :startdoc:
92
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
6
+ undef to_yaml if method_defined?(:to_yaml)
7
+
8
+ include ActiveModel::Type::Helpers::Mutable
9
+
10
+ attr_reader :subtype, :coder
11
+
12
+ def initialize(subtype, coder)
13
+ @subtype = subtype
14
+ @coder = coder
15
+ super(subtype)
16
+ end
17
+
18
+ def deserialize(value)
19
+ if default_value?(value)
20
+ value
21
+ else
22
+ coder.load(super)
23
+ end
24
+ end
25
+
26
+ def serialize(value)
27
+ return if value.nil?
28
+ unless default_value?(value)
29
+ super coder.dump(value)
30
+ end
31
+ end
32
+
33
+ def inspect
34
+ Kernel.instance_method(:inspect).bind(self).call
35
+ end
36
+
37
+ def changed_in_place?(raw_old_value, value)
38
+ return false if value.nil?
39
+ raw_new_value = encoded(value)
40
+ raw_old_value.nil? != raw_new_value.nil? ||
41
+ subtype.changed_in_place?(raw_old_value, raw_new_value)
42
+ end
43
+
44
+ def accessor
45
+ ActiveEntity::Store::IndifferentHashAccessor
46
+ end
47
+
48
+ def assert_valid_value(value)
49
+ if coder.respond_to?(:assert_valid_value)
50
+ coder.assert_valid_value(value, action: "serialize")
51
+ end
52
+ end
53
+
54
+ def force_equality?(value)
55
+ coder.respond_to?(:object_class) && value.is_a?(coder.object_class)
56
+ end
57
+
58
+ private
59
+
60
+ def default_value?(value)
61
+ value == coder.load(nil)
62
+ end
63
+
64
+ def encoded(value)
65
+ unless default_value?(value)
66
+ coder.dump(value)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class Text < ActiveModel::Type::String # :nodoc:
6
+ def type
7
+ :text
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class Time < ActiveModel::Type::Time
6
+ include Internal::Timezone
7
+
8
+ class Value < DelegateClass(::Time) # :nodoc:
9
+ end
10
+
11
+ def serialize(value)
12
+ case value = super
13
+ when ::Time
14
+ Value.new(value)
15
+ else
16
+ value
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module ActiveEntity
6
+ module Type
7
+ class TypeMap # :nodoc:
8
+ def initialize
9
+ @mapping = {}
10
+ @cache = Concurrent::Map.new do |h, key|
11
+ h.fetch_or_store(key, Concurrent::Map.new)
12
+ end
13
+ end
14
+
15
+ def lookup(lookup_key, *args)
16
+ fetch(lookup_key, *args) { Type.default_value }
17
+ end
18
+
19
+ def fetch(lookup_key, *args, &block)
20
+ @cache[lookup_key].fetch_or_store(args) do
21
+ perform_fetch(lookup_key, *args, &block)
22
+ end
23
+ end
24
+
25
+ def register_type(key, value = nil, &block)
26
+ raise ::ArgumentError unless value || block
27
+ @cache.clear
28
+
29
+ if block
30
+ @mapping[key] = block
31
+ else
32
+ @mapping[key] = proc { value }
33
+ end
34
+ end
35
+
36
+ def alias_type(key, target_key)
37
+ register_type(key) do |sql_type, *args|
38
+ metadata = sql_type[/\(.*\)/, 0]
39
+ lookup("#{target_key}#{metadata}", *args)
40
+ end
41
+ end
42
+
43
+ def clear
44
+ @mapping.clear
45
+ end
46
+
47
+ private
48
+
49
+ def perform_fetch(lookup_key, *args)
50
+ matching_pair = @mapping.reverse_each.detect do |key, _|
51
+ key === lookup_key
52
+ end
53
+
54
+ if matching_pair
55
+ matching_pair.last.call(lookup_key, *args)
56
+ else
57
+ yield lookup_key, *args
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Type
5
+ class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
6
+ private
7
+
8
+ def max_value
9
+ super * 2
10
+ end
11
+
12
+ def min_value
13
+ 0
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ # = Active Entity Autosave Association
5
+ #
6
+ # AutosaveAssociation is a module that takes care of automatically saving
7
+ # associated records when their parent is saved. In addition to saving, it
8
+ # also destroys any associated records that were marked for destruction.
9
+ # (See #mark_for_destruction and #marked_for_destruction?).
10
+ #
11
+ # Saving of the parent, its associations, and the destruction of marked
12
+ # associations, all happen inside a transaction. This should never leave the
13
+ # database in an inconsistent state.
14
+ #
15
+ # If validations for any of the associations fail, their error messages will
16
+ # be applied to the parent.
17
+ #
18
+ # Note that it also means that associations marked for destruction won't
19
+ # be destroyed directly. They will however still be marked for destruction.
20
+ #
21
+ # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
22
+ # When the <tt>:autosave</tt> option is not present then new association records are
23
+ # saved but the updated association records are not saved.
24
+ #
25
+ # == Validation
26
+ #
27
+ # Child records are validated unless <tt>:validate</tt> is +false+.
28
+ #
29
+ # == Callbacks
30
+ #
31
+ # Association with autosave option defines several callbacks on your
32
+ # model (before_save, after_create, after_update). Please note that
33
+ # callbacks are executed in the order they were defined in
34
+ # model. You should avoid modifying the association content, before
35
+ # autosave callbacks are executed. Placing your callbacks after
36
+ # associations is usually a good practice.
37
+ #
38
+ # === One-to-one Example
39
+ #
40
+ # class Post < ActiveEntity::Base
41
+ # has_one :author, autosave: true
42
+ # end
43
+ #
44
+ # Saving changes to the parent and its associated model can now be performed
45
+ # automatically _and_ atomically:
46
+ #
47
+ # post = Post.find(1)
48
+ # post.title # => "The current global position of migrating ducks"
49
+ # post.author.name # => "alloy"
50
+ #
51
+ # post.title = "On the migration of ducks"
52
+ # post.author.name = "Eloy Duran"
53
+ #
54
+ # post.save
55
+ # post.reload
56
+ # post.title # => "On the migration of ducks"
57
+ # post.author.name # => "Eloy Duran"
58
+ #
59
+ # Destroying an associated model, as part of the parent's save action, is as
60
+ # simple as marking it for destruction:
61
+ #
62
+ # post.author.mark_for_destruction
63
+ # post.author.marked_for_destruction? # => true
64
+ #
65
+ # Note that the model is _not_ yet removed from the database:
66
+ #
67
+ # id = post.author.id
68
+ # Author.find_by(id: id).nil? # => false
69
+ #
70
+ # post.save
71
+ # post.reload.author # => nil
72
+ #
73
+ # Now it _is_ removed from the database:
74
+ #
75
+ # Author.find_by(id: id).nil? # => true
76
+ #
77
+ # === One-to-many Example
78
+ #
79
+ # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
80
+ #
81
+ # class Post < ActiveEntity::Base
82
+ # has_many :comments # :autosave option is not declared
83
+ # end
84
+ #
85
+ # post = Post.new(title: 'ruby rocks')
86
+ # post.comments.build(body: 'hello world')
87
+ # post.save # => saves both post and comment
88
+ #
89
+ # post = Post.create(title: 'ruby rocks')
90
+ # post.comments.build(body: 'hello world')
91
+ # post.save # => saves both post and comment
92
+ #
93
+ # post = Post.create(title: 'ruby rocks')
94
+ # post.comments.create(body: 'hello world')
95
+ # post.save # => saves both post and comment
96
+ #
97
+ # When <tt>:autosave</tt> is true all children are saved, no matter whether they
98
+ # are new records or not:
99
+ #
100
+ # class Post < ActiveEntity::Base
101
+ # has_many :comments, autosave: true
102
+ # end
103
+ #
104
+ # post = Post.create(title: 'ruby rocks')
105
+ # post.comments.create(body: 'hello world')
106
+ # post.comments[0].body = 'hi everyone'
107
+ # post.comments.build(body: "good morning.")
108
+ # post.title += "!"
109
+ # post.save # => saves both post and comments.
110
+ #
111
+ # Destroying one of the associated models as part of the parent's save action
112
+ # is as simple as marking it for destruction:
113
+ #
114
+ # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
115
+ # post.comments[1].mark_for_destruction
116
+ # post.comments[1].marked_for_destruction? # => true
117
+ # post.comments.length # => 2
118
+ #
119
+ # Note that the model is _not_ yet removed from the database:
120
+ #
121
+ # id = post.comments.last.id
122
+ # Comment.find_by(id: id).nil? # => false
123
+ #
124
+ # post.save
125
+ # post.reload.comments.length # => 1
126
+ #
127
+ # Now it _is_ removed from the database:
128
+ #
129
+ # Comment.find_by(id: id).nil? # => true
130
+ module ValidateEmbeddedAssociation
131
+ extend ActiveSupport::Concern
132
+
133
+ module AssociationBuilderExtension #:nodoc:
134
+ def self.build(model, reflection)
135
+ model.send(:add_embedded_associations_validation_callbacks, reflection)
136
+ end
137
+
138
+ def self.valid_options
139
+ []
140
+ end
141
+ end
142
+
143
+ included do
144
+ Associations::Embedded::Builder::Association.extensions << AssociationBuilderExtension
145
+
146
+ unless respond_to?(:index_nested_attribute_errors)
147
+ mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false
148
+ end
149
+ end
150
+
151
+ module ClassMethods # :nodoc:
152
+ private
153
+
154
+ def define_non_cyclic_method(name, &block)
155
+ return if instance_methods(false).include?(name)
156
+ define_method(name) do |*args|
157
+ result = true; @_already_called ||= {}
158
+ # Loop prevention for validation of associations
159
+ unless @_already_called[name]
160
+ begin
161
+ @_already_called[name] = true
162
+ result = instance_eval(&block)
163
+ ensure
164
+ @_already_called[name] = false
165
+ end
166
+ end
167
+
168
+ result
169
+ end
170
+ end
171
+
172
+ # Adds validation and save callbacks for the association as specified by
173
+ # the +reflection+.
174
+ #
175
+ # For performance reasons, we don't check whether to validate at runtime.
176
+ # However the validation and callback methods are lazy and those methods
177
+ # get created when they are invoked for the very first time. However,
178
+ # this can change, for instance, when using nested attributes, which is
179
+ # called _after_ the association has been defined. Since we don't want
180
+ # the callbacks to get defined multiple times, there are guards that
181
+ # check if the save or validation methods have already been defined
182
+ # before actually defining them.
183
+ def add_embedded_associations_validation_callbacks(reflection)
184
+ define_embedded_associations_validation_callbacks(reflection)
185
+ end
186
+
187
+ def define_embedded_associations_validation_callbacks(reflection)
188
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
189
+ if reflection.validate? && !method_defined?(validation_method)
190
+ if reflection.collection?
191
+ method = :validate_collection_association
192
+ else
193
+ method = :validate_single_association
194
+ end
195
+
196
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
197
+ validate validation_method
198
+ after_validation :_ensure_no_duplicate_errors
199
+ end
200
+ end
201
+ end
202
+
203
+ # Marks this record to be destroyed as part of the parent's save transaction.
204
+ # This does _not_ actually destroy the record instantly, rather child record will be destroyed
205
+ # when <tt>parent.save</tt> is called.
206
+ #
207
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
208
+ def mark_for_destruction
209
+ @marked_for_destruction = true
210
+ end
211
+
212
+ # Returns whether or not this record will be destroyed as part of the parent's save transaction.
213
+ #
214
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
215
+ def marked_for_destruction?
216
+ @marked_for_destruction
217
+ end
218
+
219
+ # Records the association that is being destroyed and destroying this
220
+ # record in the process.
221
+ def destroyed_by_association=(reflection)
222
+ @destroyed_by_association = reflection
223
+ end
224
+
225
+ # Returns the association for the parent being destroyed.
226
+ #
227
+ # Used to avoid updating the counter cache unnecessarily.
228
+ def destroyed_by_association
229
+ @destroyed_by_association
230
+ end
231
+
232
+ private
233
+
234
+ # Returns the record for an association collection that should be validated
235
+ # or saved. If +autosave+ is +false+ only new records will be returned,
236
+ # unless the parent is/was a new record itself.
237
+ def associated_records_to_validate(association)
238
+ association&.target
239
+ end
240
+
241
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
242
+ # turned on for the association.
243
+ def validate_single_association(reflection)
244
+ association = association_instance_get(reflection.name)
245
+ record = association&.reader
246
+ association_valid?(reflection, record) if record
247
+ end
248
+
249
+ # Validate the associated records if <tt>:validate</tt> or
250
+ # <tt>:autosave</tt> is turned on for the association specified by
251
+ # +reflection+.
252
+ def validate_collection_association(reflection)
253
+ if association = association_instance_get(reflection.name)
254
+ if records = associated_records_to_validate(association)
255
+ records.each_with_index { |record, index| association_valid?(reflection, record, index) }
256
+ end
257
+ end
258
+ end
259
+
260
+ # Returns whether or not the association is valid and applies any errors to
261
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
262
+ # enabled records if they're marked_for_destruction? or destroyed.
263
+ def association_valid?(reflection, record, index = nil)
264
+ return true if record.marked_for_destruction?
265
+
266
+ context = validation_context
267
+
268
+ unless valid = record.valid?(context)
269
+ indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveEntity::Base.index_nested_attribute_errors)
270
+
271
+ record.errors.each do |attribute, message|
272
+ attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
273
+ errors[attribute] << message
274
+ errors[attribute].uniq!
275
+ end
276
+
277
+ record.errors.details.each_key do |attribute|
278
+ reflection_attribute =
279
+ normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
280
+
281
+ record.errors.details[attribute].each do |error|
282
+ errors.details[reflection_attribute] << error
283
+ errors.details[reflection_attribute].uniq!
284
+ end
285
+ end
286
+ end
287
+
288
+ valid
289
+ end
290
+
291
+ def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
292
+ if indexed_attribute
293
+ "#{reflection.name}[#{index}].#{attribute}"
294
+ else
295
+ "#{reflection.name}.#{attribute}"
296
+ end
297
+ end
298
+
299
+ def _ensure_no_duplicate_errors
300
+ errors.messages.each_key do |attribute|
301
+ errors[attribute].uniq!
302
+ end
303
+ end
304
+ end
305
+ end