nested_record 0.1.1 → 1.0.0.beta
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +12 -12
- data/lib/nested_record.rb +1 -0
- data/lib/nested_record/base.rb +135 -54
- data/lib/nested_record/collection.rb +20 -1
- data/lib/nested_record/errors.rb +1 -0
- data/lib/nested_record/macro.rb +2 -2
- data/lib/nested_record/methods.rb +50 -0
- data/lib/nested_record/methods/many.rb +113 -0
- data/lib/nested_record/methods/one.rb +86 -0
- data/lib/nested_record/setup.rb +72 -177
- data/lib/nested_record/type.rb +3 -39
- data/lib/nested_record/type/many.rb +24 -0
- data/lib/nested_record/type/one.rb +17 -0
- data/lib/nested_record/version.rb +1 -1
- data/spec/nested_record/base_spec.rb +106 -9
- data/spec/nested_record/type/many_spec.rb +23 -0
- data/spec/nested_record/type/one_spec.rb +22 -0
- data/spec/nested_record_spec.rb +346 -13
- data/spec/spec_helper.rb +1 -0
- data/spec/support/model.rb +19 -15
- metadata +11 -4
@@ -0,0 +1,113 @@
|
|
1
|
+
class NestedRecord::Methods
|
2
|
+
class Many < self
|
3
|
+
def initialize(setup)
|
4
|
+
super
|
5
|
+
define :writer
|
6
|
+
define :rewrite_attributes
|
7
|
+
define :upsert_attributes
|
8
|
+
define :validation
|
9
|
+
define_attributes_writer_method
|
10
|
+
end
|
11
|
+
|
12
|
+
def validation_method_name
|
13
|
+
:"validate_associated_records_for_#{@setup.name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def writer_method_body
|
17
|
+
setup = @setup
|
18
|
+
proc do |records|
|
19
|
+
collection_class = setup.collection_class
|
20
|
+
return super(records.dup) if records.is_a? collection_class
|
21
|
+
collection = collection_class.new
|
22
|
+
records.each { |obj| collection << obj }
|
23
|
+
super(collection)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def upsert_attributes_method_body
|
28
|
+
setup = @setup
|
29
|
+
name = @setup.name
|
30
|
+
proc do |data|
|
31
|
+
attributes_collection =
|
32
|
+
if data.is_a? Hash
|
33
|
+
data.values
|
34
|
+
else
|
35
|
+
data
|
36
|
+
end
|
37
|
+
collection = public_send(name)
|
38
|
+
attributes_collection.each do |attributes|
|
39
|
+
attributes = attributes.stringify_keys
|
40
|
+
next if setup.reject_if_proc&.call(attributes)
|
41
|
+
|
42
|
+
if (pkey_attributes = setup.primary_key)
|
43
|
+
klass = setup.record_class
|
44
|
+
else
|
45
|
+
klass = setup.record_class.find_subtype(attributes['type'])
|
46
|
+
while !(pkey_attributes = klass.primary_key) && (klass < NestedRecord::Base)
|
47
|
+
klass = klass.superclass
|
48
|
+
end
|
49
|
+
unless pkey_attributes
|
50
|
+
raise NestedRecord::ConfigurationError, 'You should specify a primary_key when using :upsert strategy'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
key = { _is_a?: klass }
|
54
|
+
pkey_attributes.each do |name|
|
55
|
+
value = attributes[name]
|
56
|
+
if (type = klass.type_for_attribute(name))
|
57
|
+
value = type.cast(value)
|
58
|
+
end
|
59
|
+
key[name] = value
|
60
|
+
end
|
61
|
+
if (record = collection.find_by(key))
|
62
|
+
record.assign_attributes(attributes)
|
63
|
+
else
|
64
|
+
collection.build(attributes)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def rewrite_attributes_method_body
|
71
|
+
setup = @setup
|
72
|
+
writer_method_name = self.writer_method_name
|
73
|
+
proc do |data|
|
74
|
+
attributes_collection =
|
75
|
+
if data.is_a? Hash
|
76
|
+
data.values
|
77
|
+
else
|
78
|
+
data
|
79
|
+
end
|
80
|
+
collection = setup.collection_class.new
|
81
|
+
attributes_collection.each do |attributes|
|
82
|
+
attributes = attributes.stringify_keys
|
83
|
+
next if setup.reject_if_proc&.call(attributes)
|
84
|
+
collection.build(attributes)
|
85
|
+
end
|
86
|
+
public_send(writer_method_name, collection)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def validation_method_body
|
91
|
+
name = @setup.name
|
92
|
+
proc do
|
93
|
+
collection = public_send(name)
|
94
|
+
collection.map.with_index do |record, index|
|
95
|
+
next true if record.valid?
|
96
|
+
record.errors.each do |attribute, message|
|
97
|
+
error_attribute = "#{name}[#{index}].#{attribute}"
|
98
|
+
errors[error_attribute] << message
|
99
|
+
errors[error_attribute].uniq!
|
100
|
+
end
|
101
|
+
record.errors.details.each_key do |attribute|
|
102
|
+
error_attribute = "#{name}[#{index}].#{attribute}"
|
103
|
+
record.errors.details[attribute].each do |error|
|
104
|
+
errors.details[error_attribute] << error
|
105
|
+
errors.details[error_attribute].uniq!
|
106
|
+
end
|
107
|
+
end
|
108
|
+
false
|
109
|
+
end.all?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
class NestedRecord::Methods
|
2
|
+
class One < self
|
3
|
+
def initialize(setup)
|
4
|
+
super
|
5
|
+
define :writer
|
6
|
+
define :build
|
7
|
+
alias_method rewrite_attributes_method_name, build_method_name
|
8
|
+
define :bang
|
9
|
+
define :upsert_attributes
|
10
|
+
define :validation
|
11
|
+
define_attributes_writer_method
|
12
|
+
end
|
13
|
+
|
14
|
+
def writer_method_body
|
15
|
+
setup = @setup
|
16
|
+
proc do |record|
|
17
|
+
unless record.nil? || record.kind_of?(setup.record_class)
|
18
|
+
raise NestedRecord::TypeMismatchError, "#{record.inspect} should be a #{setup.record_class}"
|
19
|
+
end
|
20
|
+
super(record)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_method_name
|
25
|
+
:"build_#{@setup.name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_method_body
|
29
|
+
setup = @setup
|
30
|
+
writer_method_name = self.writer_method_name
|
31
|
+
proc do |attributes = {}|
|
32
|
+
record = setup.record_class.new(attributes)
|
33
|
+
public_send(writer_method_name, record)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def bang_method_name
|
38
|
+
:"#{@setup.name}!"
|
39
|
+
end
|
40
|
+
|
41
|
+
def bang_method_body
|
42
|
+
<<~RUBY
|
43
|
+
#{@setup.name} || #{build_method_name}
|
44
|
+
RUBY
|
45
|
+
end
|
46
|
+
|
47
|
+
def upsert_attributes_method_body
|
48
|
+
build_method_name = self.build_method_name
|
49
|
+
name = @setup.name
|
50
|
+
proc do |attributes|
|
51
|
+
if (record = public_send(name))
|
52
|
+
record.assign_attributes(attributes)
|
53
|
+
else
|
54
|
+
public_send(build_method_name, attributes)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validation_method_name
|
60
|
+
:"validate_associated_record_for_#{@setup.name}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def validation_method_body
|
64
|
+
name = @setup.name
|
65
|
+
proc do
|
66
|
+
record = public_send(name)
|
67
|
+
return true unless record
|
68
|
+
return true if record.valid?
|
69
|
+
|
70
|
+
record.errors.each do |attribute, message|
|
71
|
+
error_attribute = "#{name}.#{attribute}"
|
72
|
+
errors[error_attribute] << message
|
73
|
+
errors[error_attribute].uniq!
|
74
|
+
end
|
75
|
+
record.errors.details.each_key do |attribute|
|
76
|
+
error_attribute = "#{name}.#{attribute}"
|
77
|
+
record.errors.details[attribute].each do |error|
|
78
|
+
errors.details[error_attribute] << error
|
79
|
+
errors.details[error_attribute].uniq!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/nested_record/setup.rb
CHANGED
@@ -1,22 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class NestedRecord::Setup
|
4
|
-
|
4
|
+
attr_reader :name, :primary_key, :reject_if_proc
|
5
|
+
|
6
|
+
def initialize(owner, name, **options, &block)
|
5
7
|
@options = options
|
6
8
|
@owner = owner
|
7
|
-
|
8
|
-
|
9
|
+
@name = name
|
10
|
+
|
11
|
+
if block
|
12
|
+
case (cn = options.fetch(:class_name) { false })
|
13
|
+
when true
|
14
|
+
cn = name.to_s.camelize
|
15
|
+
cn = cn.singularize if self.is_a?(HasMany)
|
16
|
+
class_name = cn
|
17
|
+
when false
|
18
|
+
class_name = nil
|
19
|
+
when String, Symbol
|
20
|
+
class_name = cn.to_s
|
21
|
+
else
|
22
|
+
raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
|
23
|
+
end
|
24
|
+
@record_class = Class.new(NestedRecord::Base, &block)
|
25
|
+
@owner.const_set(class_name, @record_class) if class_name
|
9
26
|
else
|
10
|
-
|
11
|
-
|
27
|
+
if options.key? :class_name
|
28
|
+
case (cn = options.fetch(:class_name))
|
29
|
+
when String, Symbol
|
30
|
+
@record_class = cn.to_s
|
31
|
+
else
|
32
|
+
raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
|
33
|
+
end
|
34
|
+
else
|
35
|
+
cn = name.to_s.camelize
|
36
|
+
cn = cn.singularize if self.is_a?(HasMany)
|
37
|
+
@record_class = cn
|
38
|
+
end
|
12
39
|
end
|
13
|
-
@name = name
|
14
|
-
@extension = extension
|
15
40
|
|
16
|
-
|
41
|
+
case (aw = options.fetch(:attributes_writer) { {} })
|
42
|
+
when Hash
|
43
|
+
@attributes_writer_opts = aw
|
44
|
+
when true, false
|
45
|
+
@attributes_writer_opts = {}
|
46
|
+
when Symbol
|
47
|
+
@attributes_writer_opts = { strategy: aw }
|
48
|
+
else
|
49
|
+
raise NestedRecord::ConfigurationError, "Bad :attributes_writer option #{aw.inspect}"
|
50
|
+
end
|
51
|
+
@reject_if_proc = @attributes_writer_opts[:reject_if]
|
52
|
+
|
53
|
+
@methods_extension = build_methods_extension
|
17
54
|
|
18
55
|
@owner.attribute @name, type, default: default_value
|
19
|
-
@owner.
|
56
|
+
@owner.include @methods_extension
|
57
|
+
@owner.validate @methods_extension.validation_method_name
|
20
58
|
end
|
21
59
|
|
22
60
|
def record_class
|
@@ -26,57 +64,36 @@ class NestedRecord::Setup
|
|
26
64
|
@record_class
|
27
65
|
end
|
28
66
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
67
|
+
def primary_key
|
68
|
+
return @primary_key if defined? @primary_key
|
69
|
+
@primary_key = Array(@options[:primary_key])
|
70
|
+
if @primary_key.empty?
|
71
|
+
@primary_key = nil
|
72
|
+
else
|
73
|
+
@primary_key = @primary_key.map(&:to_s)
|
74
|
+
end
|
75
|
+
@primary_key
|
37
76
|
end
|
38
77
|
|
39
|
-
def
|
40
|
-
|
78
|
+
def attributes_writer_strategy
|
79
|
+
return unless @options.fetch(:attributes_writer) { true }
|
80
|
+
case (strategy = @attributes_writer_opts.fetch(:strategy) { :upsert })
|
81
|
+
when :rewrite, :upsert
|
82
|
+
return strategy
|
83
|
+
else
|
84
|
+
raise NestedRecord::ConfigurationError, "Unknown strategy #{strategy.inspect}"
|
85
|
+
end
|
41
86
|
end
|
42
87
|
|
43
|
-
|
44
|
-
define_writer_method
|
45
|
-
define_attributes_writer_method
|
46
|
-
define_validation_method
|
47
|
-
end
|
88
|
+
private
|
48
89
|
|
49
90
|
class HasMany < self
|
50
|
-
def initialize(*)
|
51
|
-
super
|
52
|
-
if (attributes_writer_opts = @options[:attributes_writer]).is_a? Hash
|
53
|
-
@reject_if_proc = attributes_writer_opts[:reject_if]
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
91
|
def type
|
58
|
-
@type ||= NestedRecord::Type::
|
59
|
-
end
|
60
|
-
|
61
|
-
def collection_class_name
|
62
|
-
@collection_class_name ||= :"NestedRecord_Many#{@name.to_s.camelize}"
|
92
|
+
@type ||= NestedRecord::Type::Many.new(self)
|
63
93
|
end
|
64
94
|
|
65
95
|
def collection_class
|
66
|
-
|
67
|
-
extension = @extension
|
68
|
-
collection_superclass = record_class.collection_class
|
69
|
-
@owner.const_set(
|
70
|
-
collection_class_name,
|
71
|
-
Class.new(collection_superclass) do
|
72
|
-
@record_class = collection_superclass.record_class
|
73
|
-
include Module.new(&extension) if extension
|
74
|
-
end
|
75
|
-
)
|
76
|
-
end
|
77
|
-
|
78
|
-
def reject?(attributes)
|
79
|
-
@reject_if_proc&.call(attributes)
|
96
|
+
record_class.collection_class
|
80
97
|
end
|
81
98
|
|
82
99
|
private
|
@@ -85,76 +102,14 @@ class NestedRecord::Setup
|
|
85
102
|
[]
|
86
103
|
end
|
87
104
|
|
88
|
-
def
|
89
|
-
|
90
|
-
@owner.define_method(writer_method_name) do |records|
|
91
|
-
collection_class = setup.collection_class
|
92
|
-
return super(records.dup) if records.is_a? collection_class
|
93
|
-
collection = collection_class.new
|
94
|
-
records.each do |obj|
|
95
|
-
collection << obj
|
96
|
-
end
|
97
|
-
super(collection)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def define_attributes_writer_method
|
102
|
-
return unless @options.fetch(:attributes_writer) { true }
|
103
|
-
setup = self
|
104
|
-
writer_method = writer_method_name
|
105
|
-
@owner.define_method(attributes_writer_method_name) do |data|
|
106
|
-
attributes_collection =
|
107
|
-
if data.is_a? Hash
|
108
|
-
data.values
|
109
|
-
else
|
110
|
-
data
|
111
|
-
end
|
112
|
-
collection = setup.collection_class.new
|
113
|
-
attributes_collection.each do |attributes|
|
114
|
-
attributes = attributes.stringify_keys
|
115
|
-
next if setup.reject?(attributes)
|
116
|
-
collection.build(attributes)
|
117
|
-
end
|
118
|
-
public_send(writer_method, collection)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def define_validation_method
|
123
|
-
setup = self
|
124
|
-
name = @name
|
125
|
-
@owner.define_method(validation_method_name) do
|
126
|
-
collection = public_send(name)
|
127
|
-
collection.map do |record|
|
128
|
-
next true if record.valid?
|
129
|
-
record.errors.each do |attribute, message|
|
130
|
-
error_attribute = "#{name}.#{attribute}"
|
131
|
-
errors[error_attribute] << message
|
132
|
-
errors[error_attribute].uniq!
|
133
|
-
end
|
134
|
-
record.errors.details.each_key do |attribute|
|
135
|
-
error_attribute = "#{name}.#{attribute}"
|
136
|
-
record.errors.details[attribute].each do |error|
|
137
|
-
errors.details[error_attribute] << error
|
138
|
-
errors.details[error_attribute].uniq!
|
139
|
-
end
|
140
|
-
end
|
141
|
-
false
|
142
|
-
end.all?
|
143
|
-
end
|
105
|
+
def build_methods_extension
|
106
|
+
NestedRecord::Methods::Many.new(self)
|
144
107
|
end
|
145
108
|
end
|
146
109
|
|
147
110
|
class HasOne < self
|
148
|
-
def define_methods
|
149
|
-
define_writer_method
|
150
|
-
define_build_method
|
151
|
-
define_attributes_writer_method
|
152
|
-
define_validation_method
|
153
|
-
define_bang_method
|
154
|
-
end
|
155
|
-
|
156
111
|
def type
|
157
|
-
@type ||= NestedRecord::Type::
|
112
|
+
@type ||= NestedRecord::Type::One.new(self)
|
158
113
|
end
|
159
114
|
|
160
115
|
private
|
@@ -163,68 +118,8 @@ class NestedRecord::Setup
|
|
163
118
|
nil
|
164
119
|
end
|
165
120
|
|
166
|
-
def
|
167
|
-
|
168
|
-
end
|
169
|
-
|
170
|
-
def bang_method_name
|
171
|
-
:"#{@name}!"
|
172
|
-
end
|
173
|
-
|
174
|
-
def define_writer_method
|
175
|
-
setup = self
|
176
|
-
@owner.define_method(writer_method_name) do |record|
|
177
|
-
unless record.nil? || record.kind_of?(setup.record_class)
|
178
|
-
raise NestedRecord::TypeMismatchError, "#{record.inspect} should be a #{setup.record_class}"
|
179
|
-
end
|
180
|
-
super(record)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def define_attributes_writer_method
|
185
|
-
return unless @options.fetch(:attributes_writer) { true }
|
186
|
-
@owner.alias_method attributes_writer_method_name, build_method_name
|
187
|
-
end
|
188
|
-
|
189
|
-
def define_validation_method
|
190
|
-
setup = self
|
191
|
-
name = @name
|
192
|
-
@owner.define_method(validation_method_name) do
|
193
|
-
record = public_send(name)
|
194
|
-
return true unless record
|
195
|
-
return true if record.valid?
|
196
|
-
|
197
|
-
record.errors.each do |attribute, message|
|
198
|
-
error_attribute = "#{name}.#{attribute}"
|
199
|
-
errors.details[error_attribute] << message
|
200
|
-
errors.details[error_attribute].uniq!
|
201
|
-
end
|
202
|
-
record.errors.details.each_key do |attribute|
|
203
|
-
error_attribute = "#{name}.#{attribute}"
|
204
|
-
record.errors.details[attribute].each do |error|
|
205
|
-
errors.details[error_attribute] << error
|
206
|
-
errors.details[error_attribute].uniq!
|
207
|
-
end
|
208
|
-
end
|
209
|
-
false
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
def define_build_method
|
214
|
-
setup = self
|
215
|
-
writer_method = writer_method_name
|
216
|
-
@owner.define_method(build_method_name) do |attributes = {}|
|
217
|
-
record = setup.record_class.new(attributes)
|
218
|
-
public_send(writer_method, record)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
def define_bang_method
|
223
|
-
@owner.class_eval <<~RUBY
|
224
|
-
def #{bang_method_name}
|
225
|
-
#{@name} || #{build_method_name}
|
226
|
-
end
|
227
|
-
RUBY
|
121
|
+
def build_methods_extension
|
122
|
+
NestedRecord::Methods::One.new(self)
|
228
123
|
end
|
229
124
|
end
|
230
125
|
end
|