active_data 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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