activeentity 0.0.1.beta1

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