ripple 1.0.0.beta → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/.gitignore +10 -6
  2. data/Gemfile +6 -15
  3. data/Gemfile.rails30 +3 -0
  4. data/Gemfile.rails31 +3 -0
  5. data/Gemfile.rails32 +3 -0
  6. data/Guardfile +3 -1
  7. data/LICENSE +16 -0
  8. data/README.markdown +173 -0
  9. data/RELEASE_NOTES.textile +286 -0
  10. data/Rakefile +19 -0
  11. data/lib/rails/generators/ripple/configuration/templates/ripple.yml +1 -0
  12. data/lib/rails/generators/ripple/model/model_generator.rb +1 -1
  13. data/lib/rails/generators/ripple/model/templates/{model.rb → model.rb.erb} +0 -0
  14. data/lib/rails/generators/ripple/observer/observer_generator.rb +1 -1
  15. data/lib/rails/generators/ripple/observer/templates/{observer.rb → observer.rb.erb} +0 -2
  16. data/lib/rails/generators/ripple/test/templates/cucumber.rb.erb +7 -0
  17. data/lib/rails/generators/ripple/test/test_generator.rb +17 -13
  18. data/lib/rails/generators/ripple_generator.rb +1 -0
  19. data/lib/ripple/associations.rb +65 -55
  20. data/lib/ripple/associations/embedded.rb +1 -1
  21. data/lib/ripple/associations/linked.rb +1 -1
  22. data/lib/ripple/associations/many.rb +1 -1
  23. data/lib/ripple/associations/many_embedded_proxy.rb +3 -2
  24. data/lib/ripple/associations/many_linked_proxy.rb +1 -1
  25. data/lib/ripple/associations/many_reference_proxy.rb +7 -5
  26. data/lib/ripple/associations/proxy.rb +2 -2
  27. data/lib/ripple/attribute_methods.rb +69 -61
  28. data/lib/ripple/attribute_methods/dirty.rb +2 -2
  29. data/lib/ripple/attribute_methods/read.rb +4 -2
  30. data/lib/ripple/callbacks.rb +23 -26
  31. data/lib/ripple/conflict/basic_resolver.rb +6 -2
  32. data/lib/ripple/conflict/document_hooks.rb +26 -0
  33. data/lib/ripple/conflict/resolver.rb +10 -2
  34. data/lib/ripple/conflict/test_helper.rb +3 -2
  35. data/lib/ripple/conversion.rb +1 -0
  36. data/lib/ripple/core_ext.rb +1 -0
  37. data/lib/ripple/core_ext/casting.rb +2 -0
  38. data/lib/ripple/core_ext/indexes.rb +89 -0
  39. data/lib/ripple/document.rb +23 -22
  40. data/lib/ripple/document/key.rb +12 -14
  41. data/lib/ripple/document/persistence.rb +99 -84
  42. data/lib/ripple/embedded_document.rb +9 -10
  43. data/lib/ripple/embedded_document/persistence.rb +42 -44
  44. data/lib/ripple/i18n.rb +4 -1
  45. data/lib/ripple/indexes.rb +151 -0
  46. data/lib/ripple/locale/en.yml +4 -0
  47. data/lib/ripple/locale/fr.yml +24 -0
  48. data/lib/ripple/nested_attributes.rb +92 -90
  49. data/lib/ripple/properties.rb +2 -1
  50. data/lib/ripple/railtie.rb +9 -0
  51. data/lib/ripple/railties/ripple.rake +32 -15
  52. data/lib/ripple/serialization.rb +50 -52
  53. data/lib/ripple/test_server.rb +1 -2
  54. data/lib/ripple/timestamps.rb +6 -8
  55. data/lib/ripple/validations.rb +19 -21
  56. data/lib/ripple/version.rb +1 -1
  57. data/ripple.gemspec +6 -5
  58. data/spec/generators/ripple/configuration_generator_spec.rb +9 -0
  59. data/spec/generators/ripple/js_generator_spec.rb +14 -0
  60. data/spec/generators/ripple/model_generator_spec.rb +64 -0
  61. data/spec/generators/ripple/observer_generator_spec.rb +20 -0
  62. data/spec/generators/ripple/test_generator_spec.rb +116 -0
  63. data/spec/generators/ripple_generator_spec.rb +11 -0
  64. data/spec/integration/ripple/conflict_resolution_spec.rb +35 -4
  65. data/spec/integration/ripple/indexes_spec.rb +47 -0
  66. data/spec/ripple/associations/many_embedded_proxy_spec.rb +50 -60
  67. data/spec/ripple/associations/many_linked_proxy_spec.rb +2 -2
  68. data/spec/ripple/associations/many_reference_proxy_spec.rb +1 -1
  69. data/spec/ripple/associations_spec.rb +16 -7
  70. data/spec/ripple/attribute_methods_spec.rb +43 -2
  71. data/spec/ripple/callbacks_spec.rb +120 -101
  72. data/spec/ripple/conversion_spec.rb +5 -13
  73. data/spec/ripple/core_ext_spec.rb +93 -15
  74. data/spec/ripple/finders_spec.rb +0 -2
  75. data/spec/ripple/indexes_spec.rb +111 -0
  76. data/spec/ripple/observable_spec.rb +1 -2
  77. data/spec/ripple/persistence_spec.rb +55 -32
  78. data/spec/ripple/properties_spec.rb +1 -1
  79. data/spec/ripple/ripple_spec.rb +5 -5
  80. data/spec/ripple/timestamps_spec.rb +9 -2
  81. data/spec/ripple/validations_spec.rb +50 -52
  82. data/spec/spec_helper.rb +9 -2
  83. data/spec/support/generator_setup.rb +26 -0
  84. data/spec/support/models.rb +1 -0
  85. data/spec/support/models/box.rb +1 -0
  86. data/spec/support/models/clock.rb +1 -1
  87. data/spec/support/models/indexer.rb +26 -0
  88. data/spec/support/models/post.rb +3 -2
  89. data/spec/support/models/widget.rb +2 -0
  90. data/spec/support/search.rb +2 -2
  91. data/spec/support/test_server.rb +23 -11
  92. data/spec/support/test_server.yml.example +1 -1
  93. metadata +159 -135
  94. data/spec/support/mocks.rb +0 -4
@@ -28,6 +28,7 @@ module Ripple
28
28
  include Persistence
29
29
  extend Ripple::Properties
30
30
  include Ripple::AttributeMethods
31
+ include Ripple::Indexes
31
32
  include Ripple::Timestamps
32
33
  include Ripple::Validations
33
34
  include Ripple::Associations
@@ -46,19 +47,17 @@ module Ripple
46
47
  end
47
48
  end
48
49
 
49
- module InstanceMethods
50
- def ==(other)
51
- self.class == other.class &&
50
+ def ==(other)
51
+ self.class == other.class &&
52
52
  _parent_document == other._parent_document &&
53
53
  serializable_hash == other.serializable_hash
54
- end
55
- alias eql? ==
54
+ end
55
+ alias eql? ==
56
56
 
57
- def hash
58
- parent = [_parent_document.class]
59
- parent << [_parent_document.key] if _parent_document.respond_to?(:key)
60
- [ self.class, parent, serializable_hash ].hash
61
- end
57
+ def hash
58
+ hash = self.class.hash ^ _parent_document.class.hash ^ serializable_hash.to_s.hash
59
+ hash ^= _parent_document.key.hash if _parent_document.respond_to?(:key)
60
+ hash
62
61
  end
63
62
  end
64
63
  end
@@ -24,53 +24,51 @@ module Ripple
24
24
  end
25
25
  end
26
26
 
27
- module InstanceMethods
28
- # The parent document to this embedded document. This may be a
29
- # {Ripple::Document} or another {Ripple::EmbeddedDocument}.
30
- attr_accessor :_parent_document
31
-
32
- # Whether the root document is unsaved.
33
- def new?
34
- if _root_document
35
- _root_document.new?
36
- else
37
- true
38
- end
39
- end
40
-
41
- # Sets this embedded documents attributes and saves the root document.
42
- def update_attributes(attrs)
43
- self.attributes = attrs
44
- save
27
+ # The parent document to this embedded document. This may be a
28
+ # {Ripple::Document} or another {Ripple::EmbeddedDocument}.
29
+ attr_accessor :_parent_document
30
+
31
+ # Whether the root document is unsaved.
32
+ def new?
33
+ if _root_document
34
+ _root_document.new?
35
+ else
36
+ true
45
37
  end
46
-
47
- # Updates this embedded document's attribute and saves the
48
- # root document, skipping validations.
49
- def update_attribute(attribute, value)
50
- send("#{attribute}=", value)
51
- save(:validate => false)
52
- end
53
-
54
- # Saves this embedded document by delegating to the root document.
55
- def save(*args)
56
- if _root_document
57
- run_save_callbacks do
58
- _root_document.save(*args)
59
- end
60
- else
61
- raise NoRootDocument.new(self, :save)
38
+ end
39
+
40
+ # Sets this embedded documents attributes and saves the root document.
41
+ def update_attributes(attrs)
42
+ self.attributes = attrs
43
+ save
44
+ end
45
+
46
+ # Updates this embedded document's attribute and saves the
47
+ # root document, skipping validations.
48
+ def update_attribute(attribute, value)
49
+ send("#{attribute}=", value)
50
+ save(:validate => false)
51
+ end
52
+
53
+ # Saves this embedded document by delegating to the root document.
54
+ def save(*args)
55
+ if _root_document
56
+ run_save_callbacks do
57
+ _root_document.save(*args)
62
58
  end
59
+ else
60
+ raise NoRootDocument.new(self, :save)
63
61
  end
64
-
65
- # @private
66
- def attributes_for_persistence
67
- raw_attributes.merge("_type" => self.class.name)
68
- end
69
-
70
- # The root {Ripple::Document} to which this embedded document belongs.
71
- def _root_document
72
- @_parent_document.try(:_root_document)
73
- end
62
+ end
63
+
64
+ # @private
65
+ def attributes_for_persistence
66
+ raw_attributes.merge("_type" => self.class.name)
67
+ end
68
+
69
+ # The root {Ripple::Document} to which this embedded document belongs.
70
+ def _root_document
71
+ @_parent_document.try(:_root_document)
74
72
  end
75
73
  end
76
74
  end
data/lib/ripple/i18n.rb CHANGED
@@ -1,2 +1,5 @@
1
1
  require 'active_support/i18n'
2
- I18n.load_path << File.expand_path("../locale/en.yml", __FILE__)
2
+
3
+ Dir.glob(File.expand_path("../locale/*.yml", __FILE__)).each do |locale_file|
4
+ I18n.load_path << locale_file
5
+ end
@@ -0,0 +1,151 @@
1
+ require 'ripple/translation'
2
+ require 'active_support/concern'
3
+
4
+ module Ripple
5
+ # Adds secondary-indexes to {Document} properties.
6
+ module Indexes
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+
11
+ def inherited(subclass)
12
+ super
13
+ subclass.indexes = indexes.dup
14
+ end
15
+
16
+ # Indexes defined on the document.
17
+ def indexes
18
+ @indexes ||= {}.with_indifferent_access
19
+ end
20
+
21
+ def indexes=(idx)
22
+ @indexes = idx
23
+ end
24
+
25
+ def property(key, type, options={})
26
+ if indexed = options.delete(:index)
27
+ indexes[key] = Index.new(key, type, indexed)
28
+ end
29
+ super
30
+ end
31
+
32
+ def index(key, type, &block)
33
+ if block_given?
34
+ indexes[key] = Index.new(key, type, &block)
35
+ else
36
+ indexes[key] = Index.new(key, type)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Returns indexes in a form suitable for persisting to Riak.
42
+ # @return [Hash] indexes for this document
43
+ def indexes_for_persistence(prefix = '')
44
+ Hash.new {|h,k| h[k] = Set.new }.tap do |indexes|
45
+ # Add embedded associations' indexes
46
+ self.class.embedded_associations.each do |association|
47
+ documents = instance_variable_get(association.ivar)
48
+ unless documents.nil?
49
+ Array(documents).each do |doc|
50
+ embedded_indexes = doc.indexes_for_persistence("#{prefix}#{association.name}_")
51
+ indexes.merge!(embedded_indexes) do |_,original,new|
52
+ original.merge new
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Add this document's indexes
59
+ self.class.indexes.each do |key, index|
60
+ if index.block
61
+ index_value = index.to_index_value instance_exec(&index.block)
62
+ else
63
+ index_value = index.to_index_value send(key)
64
+ end
65
+ index_value = Set[index_value] unless index_value.is_a?(Enumerable) && !index_value.is_a?(String)
66
+ indexes[prefix + index.index_key].merge index_value
67
+ end
68
+ end
69
+ end
70
+
71
+ # Modifies the persistence chain to set indexes on the internal
72
+ # {Riak::RObject} before saving.
73
+ module DocumentMethods
74
+ extend ActiveSupport::Concern
75
+ def update_robject
76
+ robject.indexes = indexes_for_persistence
77
+ super
78
+ end
79
+
80
+ module ClassMethods
81
+ # Search for a document using an indexed column
82
+ # @param [Symbol] name of the index
83
+ # @param [String, Integer, Range] query to search for
84
+ def find_by_index(index_name, query)
85
+ if ["$bucket", "$key"].include?(index_name.to_s)
86
+ self.find(Ripple.client.get_index(self.bucket.name, index_name.to_s, query))
87
+ else
88
+ idx = self.indexes[index_name]
89
+ raise ArgumentError, t('index_undefined', :property => index_name, :type => self.name) if idx.nil?
90
+ self.find(Ripple.client.get_index(self.bucket.name, idx.index_key, query))
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Represents a Secondary Index on a Document
98
+ class Index
99
+ include Translation
100
+ attr_reader :key, :type, :block
101
+
102
+ # Creates an index for a Document
103
+ # @param [Symbol] key the attribute key
104
+ # @param [Class] property_type the type of the associated property
105
+ # @param ['bin', 'int', String, Integer] index_type if given, the
106
+ # type of index
107
+ # @yield a block that returns the value of the index
108
+ def initialize(key, property_type, index_type=true, &block)
109
+ @key, @type, @index, @block = key, property_type, index_type, block
110
+ end
111
+
112
+
113
+ # The key under which a value will be indexed
114
+ def index_key
115
+ "#{key}_#{index_type}"
116
+ end
117
+
118
+ # Converts an attribute to a value appropriate for storing in a
119
+ # secondary index.
120
+ # @param [Object] value a value of type {#type}
121
+ # @return [String, Integer, Set] a value appropriate for storing
122
+ # in a secondary index
123
+ def to_index_value(value)
124
+ value.to_ripple_index(index_type)
125
+ end
126
+
127
+ # @return ["bin", "int", nil] the type of index used for this property
128
+ # @raise [ArgumentError] if the type cannot be automatically determined
129
+ def index_type
130
+ @index_type ||= case @index
131
+ when /^bin|int$/
132
+ @index
133
+ when Class
134
+ determine_index_type(@index)
135
+ else
136
+ determine_index_type(@type)
137
+ end
138
+ end
139
+
140
+ private
141
+ def determine_index_type(itype)
142
+ if String == itype || itype < String
143
+ 'bin'
144
+ elsif [Integer, Time, Date, ActiveSupport::TimeWithZone].any? {|t| t == itype || itype < t }
145
+ 'int'
146
+ else
147
+ raise ArgumentError, t('index_type_unknown', :property => @key, :type => itype.name)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -10,13 +10,17 @@ en:
10
10
  no_key: "Couldn't find document without a key"
11
11
  one_key: "Couldn't find document with key: %{key}"
12
12
  many_keys: "Couldn't find documents with keys: %{keys}"
13
+ index_type_unknown: "Cannot determine index type for property '%{property}' of type '%{type}'."
14
+ index_undefined: "No index has been defined for property '%{property}' of type '%{type}'."
13
15
  invalid_association_value: "Invalid value %{value} for association %{name} of type %{klass} on %{owner}"
14
16
  many_association_validation_error: "are invalid (%{association_errors})"
15
17
  many_key_association: "You cannot use a many association using :key"
18
+ mass_assignment_roles_unsupported: "Roles for mass assignment are not supported for Rails 3.0"
16
19
  missing_configuration: "You are missing your ripple configuration file that should be at %{file}"
17
20
  no_root_document: "You cannot call %{method} on %{doc} without a root document"
18
21
  one_association_validation_error: "is invalid (%{association_errors})"
19
22
  property_type_mismatch: "Cannot cast %{value} into a %{class}"
23
+ stored_key_requires_property: "Missing property: %{name}. Stored-key associations require a property on the owner document."
20
24
  undefined_property: "Undefined property :%{prop} for class '%{class}'"
21
25
  unexpected_conflicts: "Conflict cannot be resolved for %{conflicts} (for %{document})"
22
26
 
@@ -0,0 +1,24 @@
1
+ fr:
2
+ ripple:
3
+ around_callbacks_not_supported: "Les callbacks around_%{type} ne sont pas utilisables sur les documents embarqués. Voir https://github.com/seancribbs/ripple/issues/188 pour plus d'information."
4
+ associated_document_error_summary: "Pour %{doc_type} %{doc_id}: %{errors}"
5
+ attribute_hash: "La valeur des attributs doit être un Hash"
6
+ cluster_not_created: "Le cluster pour l'environnement actuel n'a pas été créé. Executez d'abord `rake riak:create`"
7
+ conflict_handler_not_implemented: "Aucun handler on_conflict n'a été implémenté pour %{document}"
8
+ document_invalid: "La validation a échoué : %{errors}"
9
+ document_not_found:
10
+ no_key: "Document introuvable sans clé."
11
+ one_key: "Document introuvable avec la clé %{key}"
12
+ many_keys: "Documents introuvables avec les clés : %{keys}"
13
+ index_type_unknown: "Impossible de déterminer le type d'index pour la propriété '%{property}' of type '%{type}'."
14
+ invalid_association_value: "Valeur invalide %{value} pour l'association %{name} de type %{klass} sur %{owner}"
15
+ many_association_validation_error: "sont invalides (%{association_errors})"
16
+ many_key_association: "Vous ne pouvez pas utiliser une association `many` avec :key"
17
+ mass_assignment_roles_unsupported: "Les rôles pour les assignations de masse ne sont pas supportés avec Rails 3.0"
18
+ missing_configuration: "Votre fichier de configuration est introuvable. Il devrait être ici : %{file}"
19
+ no_root_document: "Vous ne pouvez appeler %{method} sur %{doc} sans document racine."
20
+ one_association_validation_error: "est invalide (%{association_errors})"
21
+ property_type_mismatch: "Ne peut caster %{value} en %{class}"
22
+ undefined_property: "Propriété indéfinie :%{prop} pour la classe '%{class}'"
23
+ unexpected_conflicts: "Le conflit ne peut être résolu : %{conflicts} (pour %{document})"
24
+
@@ -1,5 +1,9 @@
1
1
  require 'active_support/concern'
2
- require 'active_support/core_ext/class/inheritable_attributes'
2
+ if ActiveSupport::VERSION::STRING < '3.2'
3
+ require 'active_support/core_ext/class/inheritable_attributes'
4
+ else
5
+ require 'active_support/core_ext/class/attribute'
6
+ end
3
7
 
4
8
  module Ripple
5
9
  module NestedAttributes #:nodoc:
@@ -137,107 +141,105 @@ module Ripple
137
141
  end
138
142
  end
139
143
 
140
- module InstanceMethods
141
-
142
- protected
143
-
144
- def autosave
145
- @autosave_nested_attributes_for ||= {}
146
- end
147
-
148
- def marked_for_destruction
149
- @marked_for_destruction ||= {}
150
- end
151
-
152
- private
153
144
 
154
- def save_nested_attributes_for_one_association(association_name)
155
- send(association_name).save
145
+ protected
146
+
147
+ def autosave
148
+ @autosave_nested_attributes_for ||= {}
149
+ end
150
+
151
+ def marked_for_destruction
152
+ @marked_for_destruction ||= {}
153
+ end
154
+
155
+ private
156
+
157
+ def save_nested_attributes_for_one_association(association_name)
158
+ send(association_name).save
159
+ end
160
+
161
+ def save_nested_attributes_for_many_association(association_name)
162
+ send(association_name).map(&:save)
163
+ end
164
+
165
+ def destroy_marked_for_destruction
166
+ self.marked_for_destruction.each_pair do |association_name, resources|
167
+ resources.map(&:destroy)
168
+ send(association_name).reload
156
169
  end
157
-
158
- def save_nested_attributes_for_many_association(association_name)
159
- send(association_name).map(&:save)
170
+ end
171
+
172
+ def destroy_nested_many_association(association_name)
173
+ send(association_name).map(&:destroy)
174
+ end
175
+
176
+ def assign_nested_attributes_for_one_association(association_name, attributes)
177
+ association = self.class.associations[association_name]
178
+ if association.embedded?
179
+ assign_nested_attributes_for_one_embedded_association(association_name, attributes)
180
+ else
181
+ self.autosave[association_name] = true
182
+ assign_nested_attributes_for_one_linked_association(association_name, attributes)
160
183
  end
161
-
162
- def destroy_marked_for_destruction
163
- self.marked_for_destruction.each_pair do |association_name, resources|
164
- resources.map(&:destroy)
165
- send(association_name).reload
184
+ end
185
+
186
+ def assign_nested_attributes_for_one_embedded_association(association_name, attributes)
187
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
188
+ end
189
+
190
+ def assign_nested_attributes_for_one_linked_association(association_name, attributes)
191
+ attributes = attributes.stringify_keys
192
+ options = nested_attributes_options[association_name]
193
+
194
+ if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
195
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
196
+ else
197
+ if ((existing_record = send(association_name)).key.to_s == attributes[key_attr.to_s].to_s)
198
+ assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
199
+ else
200
+ raise ArgumentError, "Attempting to update a child that isn't already associated to the parent."
166
201
  end
167
202
  end
168
-
169
- def destroy_nested_many_association(association_name)
170
- send(association_name).map(&:destroy)
203
+ end
204
+
205
+ def assign_nested_attributes_for_many_association(association_name, attributes_collection)
206
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
207
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
171
208
  end
172
-
173
- def assign_nested_attributes_for_one_association(association_name, attributes)
174
- association = self.class.associations[association_name]
175
- if association.embedded?
176
- assign_nested_attributes_for_one_embedded_association(association_name, attributes)
177
- else
178
- self.autosave[association_name] = true
179
- assign_nested_attributes_for_one_linked_association(association_name, attributes)
180
- end
209
+
210
+ if attributes_collection.is_a? Hash
211
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
181
212
  end
182
-
183
- def assign_nested_attributes_for_one_embedded_association(association_name, attributes)
184
- send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
213
+
214
+ association = self.class.associations[association_name]
215
+ if association.embedded?
216
+ assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
217
+ else
218
+ self.autosave[association_name] = true
219
+ assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
185
220
  end
186
-
187
- def assign_nested_attributes_for_one_linked_association(association_name, attributes)
221
+ end
222
+
223
+ def assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
224
+ options = nested_attributes_options[association_name]
225
+ send(:"#{association_name}=", []) # Clobber existing
226
+ attributes_collection.each do |attributes|
188
227
  attributes = attributes.stringify_keys
189
- options = nested_attributes_options[association_name]
190
-
191
- if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
228
+ if !reject_new_record?(association_name, attributes)
192
229
  send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
193
- else
194
- if ((existing_record = send(association_name)).key.to_s == attributes[key_attr.to_s].to_s)
195
- assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
196
- else
197
- raise ArgumentError, "Attempting to update a child that isn't already associated to the parent."
198
- end
199
- end
200
- end
201
-
202
- def assign_nested_attributes_for_many_association(association_name, attributes_collection)
203
- unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
204
- raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
205
- end
206
-
207
- if attributes_collection.is_a? Hash
208
- attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
209
- end
210
-
211
- association = self.class.associations[association_name]
212
- if association.embedded?
213
- assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
214
- else
215
- self.autosave[association_name] = true
216
- assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
217
- end
218
- end
219
-
220
- def assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
221
- options = nested_attributes_options[association_name]
222
- send(:"#{association_name}=", []) # Clobber existing
223
- attributes_collection.each do |attributes|
224
- attributes = attributes.stringify_keys
225
- if !reject_new_record?(association_name, attributes)
226
- send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
227
- end
228
230
  end
229
231
  end
230
-
231
- def assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
232
- options = nested_attributes_options[association_name]
233
- attributes_collection.each do |attributes|
234
- attributes = attributes.stringify_keys
235
-
236
- if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
237
- send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
238
- elsif existing_record = send(association_name).detect { |record| record.key.to_s == attributes[key_attr.to_s].to_s }
239
- assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
240
- end
232
+ end
233
+
234
+ def assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
235
+ options = nested_attributes_options[association_name]
236
+ attributes_collection.each do |attributes|
237
+ attributes = attributes.stringify_keys
238
+
239
+ if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
240
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
241
+ elsif existing_record = send(association_name).detect { |record| record.key.to_s == attributes[key_attr.to_s].to_s }
242
+ assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
241
243
  end
242
244
  end
243
245
  end