active_data 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rspec +0 -1
  4. data/.rvmrc +1 -1
  5. data/.travis.yml +13 -6
  6. data/Appraisals +7 -0
  7. data/Gemfile +1 -5
  8. data/Guardfile +68 -15
  9. data/README.md +144 -2
  10. data/active_data.gemspec +19 -11
  11. data/gemfiles/rails.4.0.gemfile +14 -0
  12. data/gemfiles/rails.4.1.gemfile +14 -0
  13. data/gemfiles/rails.4.2.gemfile +14 -0
  14. data/gemfiles/rails.5.0.gemfile +14 -0
  15. data/lib/active_data.rb +120 -3
  16. data/lib/active_data/active_record/associations.rb +50 -0
  17. data/lib/active_data/active_record/nested_attributes.rb +24 -0
  18. data/lib/active_data/config.rb +40 -0
  19. data/lib/active_data/errors.rb +93 -0
  20. data/lib/active_data/extensions.rb +33 -0
  21. data/lib/active_data/model.rb +16 -74
  22. data/lib/active_data/model/associations.rb +84 -15
  23. data/lib/active_data/model/associations/base.rb +79 -0
  24. data/lib/active_data/model/associations/collection/embedded.rb +12 -0
  25. data/lib/active_data/model/associations/collection/proxy.rb +32 -0
  26. data/lib/active_data/model/associations/collection/referenced.rb +26 -0
  27. data/lib/active_data/model/associations/embeds_many.rb +124 -18
  28. data/lib/active_data/model/associations/embeds_one.rb +90 -15
  29. data/lib/active_data/model/associations/nested_attributes.rb +180 -0
  30. data/lib/active_data/model/associations/references_many.rb +96 -0
  31. data/lib/active_data/model/associations/references_one.rb +83 -0
  32. data/lib/active_data/model/associations/reflections/base.rb +100 -0
  33. data/lib/active_data/model/associations/reflections/embeds_many.rb +25 -0
  34. data/lib/active_data/model/associations/reflections/embeds_one.rb +49 -0
  35. data/lib/active_data/model/associations/reflections/reference_reflection.rb +45 -0
  36. data/lib/active_data/model/associations/reflections/references_many.rb +28 -0
  37. data/lib/active_data/model/associations/reflections/references_one.rb +28 -0
  38. data/lib/active_data/model/associations/validations.rb +63 -0
  39. data/lib/active_data/model/attributes.rb +247 -0
  40. data/lib/active_data/model/attributes/attribute.rb +73 -0
  41. data/lib/active_data/model/attributes/base.rb +116 -0
  42. data/lib/active_data/model/attributes/collection.rb +17 -0
  43. data/lib/active_data/model/attributes/dictionary.rb +26 -0
  44. data/lib/active_data/model/attributes/localized.rb +42 -0
  45. data/lib/active_data/model/attributes/reference_many.rb +21 -0
  46. data/lib/active_data/model/attributes/reference_one.rb +42 -0
  47. data/lib/active_data/model/attributes/reflections/attribute.rb +55 -0
  48. data/lib/active_data/model/attributes/reflections/base.rb +62 -0
  49. data/lib/active_data/model/attributes/reflections/collection.rb +10 -0
  50. data/lib/active_data/model/attributes/reflections/dictionary.rb +13 -0
  51. data/lib/active_data/model/attributes/reflections/localized.rb +43 -0
  52. data/lib/active_data/model/attributes/reflections/reference_many.rb +10 -0
  53. data/lib/active_data/model/attributes/reflections/reference_one.rb +58 -0
  54. data/lib/active_data/model/attributes/reflections/represents.rb +55 -0
  55. data/lib/active_data/model/attributes/represents.rb +64 -0
  56. data/lib/active_data/model/callbacks.rb +71 -0
  57. data/lib/active_data/model/conventions.rb +35 -0
  58. data/lib/active_data/model/dirty.rb +77 -0
  59. data/lib/active_data/model/lifecycle.rb +307 -0
  60. data/lib/active_data/model/localization.rb +21 -0
  61. data/lib/active_data/model/persistence.rb +57 -0
  62. data/lib/active_data/model/primary.rb +51 -0
  63. data/lib/active_data/model/scopes.rb +77 -0
  64. data/lib/active_data/model/validations.rb +27 -0
  65. data/lib/active_data/model/validations/associated.rb +19 -0
  66. data/lib/active_data/model/validations/nested.rb +39 -0
  67. data/lib/active_data/railtie.rb +7 -0
  68. data/lib/active_data/version.rb +1 -1
  69. data/spec/lib/active_data/active_record/associations_spec.rb +149 -0
  70. data/spec/lib/active_data/active_record/nested_attributes_spec.rb +16 -0
  71. data/spec/lib/active_data/config_spec.rb +44 -0
  72. data/spec/lib/active_data/model/associations/embeds_many_spec.rb +362 -52
  73. data/spec/lib/active_data/model/associations/embeds_one_spec.rb +250 -31
  74. data/spec/lib/active_data/model/associations/nested_attributes_spec.rb +23 -0
  75. data/spec/lib/active_data/model/associations/references_many_spec.rb +196 -0
  76. data/spec/lib/active_data/model/associations/references_one_spec.rb +134 -0
  77. data/spec/lib/active_data/model/associations/reflections/embeds_many_spec.rb +144 -0
  78. data/spec/lib/active_data/model/associations/reflections/embeds_one_spec.rb +116 -0
  79. data/spec/lib/active_data/model/associations/reflections/references_many_spec.rb +255 -0
  80. data/spec/lib/active_data/model/associations/reflections/references_one_spec.rb +208 -0
  81. data/spec/lib/active_data/model/associations/validations_spec.rb +153 -0
  82. data/spec/lib/active_data/model/associations_spec.rb +189 -0
  83. data/spec/lib/active_data/model/attributes/attribute_spec.rb +144 -0
  84. data/spec/lib/active_data/model/attributes/base_spec.rb +82 -0
  85. data/spec/lib/active_data/model/attributes/collection_spec.rb +73 -0
  86. data/spec/lib/active_data/model/attributes/dictionary_spec.rb +93 -0
  87. data/spec/lib/active_data/model/attributes/localized_spec.rb +88 -33
  88. data/spec/lib/active_data/model/attributes/reflections/attribute_spec.rb +72 -0
  89. data/spec/lib/active_data/model/attributes/reflections/base_spec.rb +56 -0
  90. data/spec/lib/active_data/model/attributes/reflections/collection_spec.rb +37 -0
  91. data/spec/lib/active_data/model/attributes/reflections/dictionary_spec.rb +43 -0
  92. data/spec/lib/active_data/model/attributes/reflections/localized_spec.rb +37 -0
  93. data/spec/lib/active_data/model/attributes/reflections/represents_spec.rb +70 -0
  94. data/spec/lib/active_data/model/attributes/represents_spec.rb +153 -0
  95. data/spec/lib/active_data/model/attributes_spec.rb +243 -0
  96. data/spec/lib/active_data/model/callbacks_spec.rb +338 -0
  97. data/spec/lib/active_data/model/conventions_spec.rb +12 -0
  98. data/spec/lib/active_data/model/dirty_spec.rb +75 -0
  99. data/spec/lib/active_data/model/lifecycle_spec.rb +330 -0
  100. data/spec/lib/active_data/model/nested_attributes.rb +202 -0
  101. data/spec/lib/active_data/model/persistence_spec.rb +47 -0
  102. data/spec/lib/active_data/model/primary_spec.rb +84 -0
  103. data/spec/lib/active_data/model/scopes_spec.rb +88 -0
  104. data/spec/lib/active_data/model/typecasting_spec.rb +192 -0
  105. data/spec/lib/active_data/model/validations/associated_spec.rb +94 -0
  106. data/spec/lib/active_data/model/validations/nested_spec.rb +93 -0
  107. data/spec/lib/active_data/model/validations_spec.rb +31 -0
  108. data/spec/lib/active_data/model_spec.rb +1 -32
  109. data/spec/lib/active_data_spec.rb +12 -0
  110. data/spec/spec_helper.rb +39 -0
  111. data/spec/support/model_helpers.rb +10 -0
  112. metadata +246 -54
  113. data/gemfiles/Gemfile.rails-3 +0 -14
  114. data/lib/active_data/attributes/base.rb +0 -69
  115. data/lib/active_data/attributes/localized.rb +0 -42
  116. data/lib/active_data/model/associations/association.rb +0 -30
  117. data/lib/active_data/model/attributable.rb +0 -122
  118. data/lib/active_data/model/collectionizable.rb +0 -55
  119. data/lib/active_data/model/collectionizable/proxy.rb +0 -42
  120. data/lib/active_data/model/extensions.rb +0 -9
  121. data/lib/active_data/model/extensions/array.rb +0 -24
  122. data/lib/active_data/model/extensions/big_decimal.rb +0 -17
  123. data/lib/active_data/model/extensions/boolean.rb +0 -38
  124. data/lib/active_data/model/extensions/date.rb +0 -17
  125. data/lib/active_data/model/extensions/date_time.rb +0 -17
  126. data/lib/active_data/model/extensions/float.rb +0 -17
  127. data/lib/active_data/model/extensions/hash.rb +0 -22
  128. data/lib/active_data/model/extensions/integer.rb +0 -17
  129. data/lib/active_data/model/extensions/localized.rb +0 -22
  130. data/lib/active_data/model/extensions/object.rb +0 -17
  131. data/lib/active_data/model/extensions/string.rb +0 -17
  132. data/lib/active_data/model/extensions/time.rb +0 -17
  133. data/lib/active_data/model/localizable.rb +0 -31
  134. data/lib/active_data/model/nested_attributes.rb +0 -58
  135. data/lib/active_data/model/parameterizable.rb +0 -29
  136. data/lib/active_data/validations.rb +0 -7
  137. data/lib/active_data/validations/associated.rb +0 -17
  138. data/spec/lib/active_data/model/attributable_spec.rb +0 -191
  139. data/spec/lib/active_data/model/collectionizable_spec.rb +0 -60
  140. data/spec/lib/active_data/model/nested_attributes_spec.rb +0 -67
  141. data/spec/lib/active_data/model/type_cast_spec.rb +0 -116
  142. data/spec/lib/active_data/validations/associated_spec.rb +0 -88
@@ -0,0 +1,79 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ class Base
5
+ attr_accessor :owner, :reflection
6
+ delegate :macro, :collection?, to: :reflection
7
+
8
+ def initialize owner, reflection
9
+ @owner, @reflection = owner, reflection
10
+ @evar_loaded = owner.persisted?
11
+ reset
12
+ end
13
+
14
+ def reset
15
+ @loaded = false
16
+ @target = nil
17
+ end
18
+
19
+ def evar_loaded?
20
+ !!@evar_loaded
21
+ end
22
+
23
+ def loaded?
24
+ !!@loaded
25
+ end
26
+
27
+ def loaded!
28
+ @evar_loaded = true
29
+ @loaded = true
30
+ end
31
+
32
+ def target
33
+ return @target if loaded?
34
+ self.target = load_target
35
+ end
36
+
37
+ def reload
38
+ reset
39
+ target
40
+ end
41
+
42
+ def apply_changes!
43
+ apply_changes or raise ActiveData::AssociationChangesNotApplied
44
+ end
45
+
46
+ def transaction &block
47
+ data = Marshal.load(Marshal.dump(read_source))
48
+ block.call
49
+ rescue StandardError => e
50
+ write_source data
51
+ reload
52
+ raise e
53
+ end
54
+
55
+ def inspect
56
+ "#<#{reflection.macro.to_s.camelize} #{target.inspect.truncate(50, omission: collection? ? '...]' : '...')}>"
57
+ end
58
+
59
+ private
60
+
61
+ def read_source
62
+ reflection.read_source owner
63
+ end
64
+
65
+ def write_source value
66
+ reflection.write_source owner, value
67
+ end
68
+
69
+ def target_for_inspect
70
+ if value.length > 50
71
+ "#{value[0..50]}...".inspect
72
+ else
73
+ value.inspect
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ module Collection
5
+ class Embedded < Proxy
6
+ delegate :build, :create, :create!, to: :@association
7
+ alias_method :new, :build
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ module Collection
5
+ class Proxy
6
+ include Enumerable
7
+
8
+ delegate :target, :save, :save!, :loaded?, :reload, :clear, :concat, to: :@association
9
+ delegate :each, :size, :length, :first, :last, :empty?, :many?, :==, :dup, to: :target
10
+ alias_method :<<, :concat
11
+ alias_method :push, :concat
12
+
13
+ def initialize(association)
14
+ @association = association
15
+ end
16
+
17
+ def to_ary
18
+ dup
19
+ end
20
+ alias_method :to_a, :to_ary
21
+
22
+ def inspect
23
+ entries = target.take(10).map!(&:inspect)
24
+ entries[10] = '...' if target.size > 10
25
+
26
+ "#<#{self.class.name.demodulize} [#{entries.join(', ')}]>"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ module Collection
5
+ class Referenced < Proxy
6
+ METHODS_EXCLUDED_FROM_DELEGATION = %w[build create create!].map(&:to_sym).freeze
7
+ delegate :scope, to: :@association
8
+
9
+ def method_missing(method, *args, &block)
10
+ delegate_to_scope?(method) ? scope.send(method, *args, &block) : super
11
+ end
12
+
13
+ def respond_to_missing?(method, include_private = false)
14
+ delegate_to_scope?(method) || super
15
+ end
16
+
17
+ private
18
+
19
+ def delegate_to_scope?(method)
20
+ METHODS_EXCLUDED_FROM_DELEGATION.exclude?(method) && scope.respond_to?(method)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,33 +1,139 @@
1
1
  module ActiveData
2
2
  module Model
3
3
  module Associations
4
- class EmbedsMany < Association
4
+ class EmbedsMany < Base
5
+ def build attributes = {}
6
+ push_object(reflection.klass.new(attributes))
7
+ end
8
+
9
+ def create attributes = {}
10
+ build(attributes).tap(&:save)
11
+ end
12
+
13
+ def create! attributes = {}
14
+ build(attributes).tap(&:save!)
15
+ end
16
+
17
+ def destroyed
18
+ @destroyed ||= []
19
+ end
20
+
21
+ def apply_changes
22
+ result = target.map do |object|
23
+ object.destroyed? || object.marked_for_destruction? ? object.destroy : object.save
24
+ end.all?
25
+ @destroyed = target.select(&:destroyed?)
26
+ target.delete_if(&:destroyed?)
27
+ result
28
+ end
5
29
 
6
- def collection?
7
- true
30
+ def target= objects
31
+ objects.each { |object| setup_performers! object }
32
+ loaded!
33
+ @target = objects
8
34
  end
9
35
 
10
- def define_reader target
11
- target.class_eval <<-EOS
12
- def #{name}
13
- @#{name} ||= begin
14
- association = self.class.reflect_on_association('#{name}')
15
- association.klass.collection
36
+ def load_target
37
+ source = read_source
38
+ source.present? ? reflection.klass.instantiate_collection(source) : default
39
+ end
40
+
41
+ def default
42
+ unless evar_loaded?
43
+ default = Array.wrap(reflection.default(owner))
44
+ if default.present?
45
+ collection = if default.all? { |object| object.is_a?(reflection.klass) }
46
+ default
47
+ else
48
+ default.map do |attributes|
49
+ reflection.klass.with_sanitize(false) do
50
+ reflection.klass.new(attributes)
51
+ end
52
+ end
16
53
  end
54
+ collection.map { |object| object.send(:clear_changes_information) } if reflection.klass.dirty?
55
+ collection
17
56
  end
18
- EOS
57
+ end || []
19
58
  end
20
59
 
21
- def define_writer target
22
- target.class_eval <<-EOS
23
- def #{name}= value
24
- association = self.class.reflect_on_association('#{name}')
25
- @#{name} = association.klass.collection(value)
26
- end
27
- EOS
60
+ def reset
61
+ super
62
+ @target = []
63
+ end
64
+
65
+ def clear
66
+ transaction { target.all?(&:destroy!) } rescue ActiveData::ObjectNotDestroyed
67
+ reload.empty?
68
+ end
69
+
70
+ def reader force_reload = false
71
+ reload if force_reload
72
+ @proxy ||= Collection::Embedded.new self
73
+ end
74
+
75
+ def replace objects
76
+ transaction do
77
+ clear
78
+ append(objects) or raise ActiveData::AssociationChangesNotApplied
79
+ end
80
+ end
81
+ alias_method :writer, :replace
82
+
83
+ def concat *objects
84
+ append objects.flatten
28
85
  end
29
86
 
87
+ private
88
+
89
+ def read_source
90
+ super || []
91
+ end
92
+
93
+ def append objects
94
+ objects.each do |object|
95
+ raise AssociationTypeMismatch.new(reflection.klass, object.class) unless object && object.is_a?(reflection.klass)
96
+ push_object object
97
+ end
98
+ result = owner.persisted? ? apply_changes : true
99
+ result && target
100
+ end
101
+
102
+ def push_object object
103
+ setup_performers! object
104
+ target[target.size] = object
105
+ end
106
+
107
+ def setup_performers! object
108
+ association = self
109
+
110
+ object.define_create do
111
+ source = association.send(:read_source)
112
+ index = association.target.select do |one|
113
+ one.persisted? || one === self
114
+ end.index { |one| one === self }
115
+
116
+ source.insert(index, attributes)
117
+ association.send(:write_source, source)
118
+ end
119
+
120
+ object.define_update do
121
+ source = association.send(:read_source)
122
+ index = association.target.select(&:persisted?).index { |one| one === self }
123
+
124
+ source[index] = attributes
125
+ association.send(:write_source, source)
126
+ end
127
+
128
+ object.define_destroy do
129
+ source = association.send(:read_source)
130
+ index = association.target.select(&:persisted?).index { |one| one === self }
131
+
132
+ source.delete_at(index) if index
133
+ association.send(:write_source, source)
134
+ end
135
+ end
30
136
  end
31
137
  end
32
138
  end
33
- end
139
+ end
@@ -1,30 +1,105 @@
1
1
  module ActiveData
2
2
  module Model
3
3
  module Associations
4
- class EmbedsOne < Association
4
+ class EmbedsOne < Base
5
+ def build attributes = {}
6
+ self.target = reflection.klass.new(attributes)
7
+ end
8
+
9
+ def create attributes = {}
10
+ build(attributes).tap(&:save)
11
+ end
5
12
 
6
- def collection?
7
- false
13
+ def create! attributes = {}
14
+ build(attributes).tap(&:save!)
8
15
  end
9
16
 
10
- def define_reader target
11
- target.class_eval <<-EOS
12
- def #{name}
13
- @#{name}
17
+ def destroyed
18
+ @destroyed
19
+ end
20
+
21
+ def apply_changes
22
+ if target
23
+ if target.destroyed? || target.marked_for_destruction?
24
+ @destroyed = target
25
+ clear
26
+ else
27
+ target.save
14
28
  end
15
- EOS
29
+ else
30
+ true
31
+ end
16
32
  end
17
33
 
18
- def define_writer target
19
- target.class_eval <<-EOS
20
- def #{name}= value
21
- association = self.class.reflect_on_association('#{name}')
22
- @#{name} = association.klass.instantiate(value) if value
34
+ def target= object
35
+ setup_performers! object if object
36
+ loaded!
37
+ @target = object
38
+ end
39
+
40
+ def load_target
41
+ source = read_source
42
+ source ? reflection.klass.instantiate(source) : default
43
+ end
44
+
45
+ def default
46
+ unless evar_loaded?
47
+ default = reflection.default(owner)
48
+ if default
49
+ object = if default.is_a?(reflection.klass)
50
+ default
51
+ else
52
+ reflection.klass.with_sanitize(false) do
53
+ reflection.klass.new(default)
54
+ end
55
+ end
56
+ object.send(:clear_changes_information) if reflection.klass.dirty?
57
+ object
23
58
  end
24
- EOS
59
+ end
60
+ end
61
+
62
+ def clear
63
+ target.try(:destroy)
64
+ reload.nil?
25
65
  end
26
66
 
67
+ def reader force_reload = false
68
+ reload if force_reload
69
+ target
70
+ end
71
+
72
+ def replace object
73
+ if object
74
+ raise AssociationTypeMismatch.new(reflection.klass, object.class) unless object.is_a?(reflection.klass)
75
+ transaction do
76
+ clear
77
+ self.target = object
78
+ apply_changes! if owner.persisted?
79
+ end
80
+ else
81
+ clear
82
+ end
83
+
84
+ target
85
+ end
86
+ alias_method :writer, :replace
87
+
88
+ private
89
+
90
+ def setup_performers! object
91
+ association = self
92
+
93
+ object.define_save do
94
+ association.send(:write_source, attributes)
95
+ end
96
+
97
+ object.define_destroy do
98
+ association.send(:write_source, nil)
99
+ true
100
+ end
101
+ end
27
102
  end
28
103
  end
29
104
  end
30
- end
105
+ end
@@ -0,0 +1,180 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ module NestedAttributes
5
+ extend ActiveSupport::Concern
6
+
7
+ DESTROY_ATTRIBUTE = '_destroy'
8
+
9
+ included do
10
+ class_attribute :nested_attributes_options, instance_writer: false
11
+ self.nested_attributes_options = {}
12
+ end
13
+
14
+ class NestedAttributesMethods
15
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == DESTROY_ATTRIBUTE || value.blank? } }
16
+
17
+ def self.accepts_nested_attributes_for(klass, *attr_names)
18
+ options = { allow_destroy: false, update_only: false }
19
+ options.update(attr_names.extract_options!)
20
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
21
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
22
+
23
+ attr_names.each do |association_name|
24
+ if reflection = klass.reflect_on_association(association_name)
25
+ klass.nested_attributes_options = klass.nested_attributes_options.merge(association_name.to_sym => options)
26
+
27
+ klass.validates_nested association_name if klass.respond_to?(:validates_nested)
28
+ type = (reflection.collection? ? :collection : :one_to_one)
29
+ klass.class_eval <<-METHOD, __FILE__, __LINE__ + 1
30
+ def #{association_name}_attributes=(attributes)
31
+ ActiveData::Model::Associations::NestedAttributes::NestedAttributesMethods
32
+ .assign_nested_attributes_for_#{type}_association(self, :#{association_name}, attributes)
33
+ end
34
+ METHOD
35
+ else
36
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.assign_nested_attributes_for_one_to_one_association(object, association_name, attributes)
42
+ options = object.nested_attributes_options[association_name]
43
+ attributes = attributes.with_indifferent_access
44
+
45
+ association = object.association(association_name)
46
+ existing_record = association.target
47
+ primary_attribute_name = primary_name_for(association.reflection.klass)
48
+ if existing_record
49
+ primary_attribute = existing_record.attribute(primary_attribute_name)
50
+ primary_attribute_value = primary_attribute.typecast(attributes[primary_attribute_name]) if primary_attribute
51
+ end
52
+
53
+ if existing_record && (!primary_attribute || options[:update_only] || existing_record.primary_attribute == primary_attribute_value)
54
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(object, association_name, attributes)
55
+ elsif attributes[primary_attribute_name].present?
56
+ raise ActiveData::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
57
+ elsif !reject_new_object?(object, association_name, attributes)
58
+ assignable_attributes = attributes.except(*unassignable_keys(object))
59
+
60
+ if existing_record && !existing_record.persisted?
61
+ existing_record.assign_attributes(assignable_attributes)
62
+ else
63
+ association.build(assignable_attributes)
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.assign_nested_attributes_for_collection_association(object, association_name, attributes_collection)
69
+ options = object.nested_attributes_options[association_name]
70
+
71
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
72
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
73
+ end
74
+
75
+ check_record_limit!(options[:limit], attributes_collection)
76
+
77
+ association = object.association(association_name)
78
+ primary_attribute_name = primary_name_for(association.reflection.klass)
79
+
80
+ raise ActiveData::UndefinedPrimaryAttribute.new(object.class, association_name) unless primary_attribute_name
81
+
82
+ if attributes_collection.is_a? Hash
83
+ keys = attributes_collection.keys
84
+ attributes_collection = if keys.include?(primary_attribute_name) || keys.include?(primary_attribute_name.to_sym)
85
+ [attributes_collection]
86
+ else
87
+ attributes_collection.values
88
+ end
89
+ end
90
+
91
+ attributes_collection.each do |attributes|
92
+ attributes = attributes.with_indifferent_access
93
+
94
+ if attributes[primary_attribute_name].blank?
95
+ unless reject_new_object?(object, association_name, attributes)
96
+ association.build(attributes.except(*unassignable_keys(object)))
97
+ end
98
+ else
99
+ existing_record = association.target.detect do |record|
100
+ primary_attribute_value = record.attribute(primary_attribute_name)
101
+ .typecast(attributes[primary_attribute_name])
102
+ record.primary_attribute == primary_attribute_value
103
+ end
104
+ if existing_record
105
+ if !call_reject_if(object, association_name, attributes)
106
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
107
+ end
108
+ else
109
+ if association.reflection.embedded?
110
+ unless reject_new_object?(object, association_name, attributes)
111
+ association.reflection.klass.with_sanitize(false) do
112
+ association.build(attributes.except(DESTROY_ATTRIBUTE))
113
+ end
114
+ end
115
+ else
116
+ raise ActiveData::ObjectNotFound.new(object, association_name, attributes[primary_attribute_name])
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def self.check_record_limit!(limit, attributes_collection)
124
+ if limit
125
+ limit = case limit
126
+ when Symbol
127
+ send(limit)
128
+ when Proc
129
+ limit.call
130
+ else
131
+ limit
132
+ end
133
+
134
+ if limit && attributes_collection.size > limit
135
+ raise ActiveData::TooManyObjects.new(limit, attributes_collection.size)
136
+ end
137
+ end
138
+ end
139
+
140
+ def self.assign_to_or_mark_for_destruction(object, attributes, allow_destroy)
141
+ object.assign_attributes(attributes.except(*unassignable_keys(object)))
142
+ object.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
143
+ end
144
+
145
+ def self.has_destroy_flag?(hash)
146
+ ActiveData.typecaster(Boolean).call(hash[DESTROY_ATTRIBUTE])
147
+ end
148
+
149
+ def self.reject_new_object?(object, association_name, attributes)
150
+ has_destroy_flag?(attributes) || call_reject_if(object, association_name, attributes)
151
+ end
152
+
153
+ def self.call_reject_if(object, association_name, attributes)
154
+ return false if has_destroy_flag?(attributes)
155
+ case callback = object.nested_attributes_options[association_name][:reject_if]
156
+ when Symbol
157
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
158
+ when Proc
159
+ callback.call(attributes)
160
+ end
161
+ end
162
+
163
+ def self.unassignable_keys(object)
164
+ [primary_name_for(object.class), DESTROY_ATTRIBUTE].compact
165
+ end
166
+
167
+ def self.primary_name_for(klass)
168
+ klass < ActiveData::Model ? klass.primary_name : 'id'
169
+ end
170
+ end
171
+
172
+ module ClassMethods
173
+ def accepts_nested_attributes_for(*attr_names)
174
+ NestedAttributesMethods.accepts_nested_attributes_for self, *attr_names
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end