rom-mapper 0.1.1 → 0.2.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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/.travis.yml +19 -13
- data/{Changelog.md → CHANGELOG.md} +6 -0
- data/Gemfile +23 -10
- data/README.md +17 -12
- data/Rakefile +12 -4
- data/lib/rom-mapper.rb +6 -15
- data/lib/rom/header.rb +195 -0
- data/lib/rom/header/attribute.rb +184 -0
- data/lib/rom/mapper.rb +63 -100
- data/lib/rom/mapper/attribute_dsl.rb +477 -0
- data/lib/rom/mapper/dsl.rb +120 -0
- data/lib/rom/mapper/model_dsl.rb +55 -0
- data/lib/rom/mapper/version.rb +3 -7
- data/lib/rom/model_builder.rb +99 -0
- data/lib/rom/processor.rb +28 -0
- data/lib/rom/processor/transproc.rb +388 -0
- data/rakelib/benchmark.rake +15 -0
- data/rakelib/mutant.rake +16 -0
- data/rakelib/rubocop.rake +18 -0
- data/rom-mapper.gemspec +7 -6
- data/spec/spec_helper.rb +32 -33
- data/spec/support/constant_leak_finder.rb +14 -0
- data/spec/support/mutant.rb +10 -0
- data/spec/unit/rom/mapper/dsl_spec.rb +467 -0
- data/spec/unit/rom/mapper_spec.rb +83 -0
- data/spec/unit/rom/processor/transproc_spec.rb +448 -0
- metadata +68 -89
- data/.ruby-version +0 -1
- data/Gemfile.devtools +0 -55
- data/config/devtools.yml +0 -2
- data/config/flay.yml +0 -3
- data/config/flog.yml +0 -2
- data/config/mutant.yml +0 -3
- data/config/reek.yml +0 -103
- data/config/rubocop.yml +0 -45
- data/lib/rom/mapper/attribute.rb +0 -31
- data/lib/rom/mapper/dumper.rb +0 -27
- data/lib/rom/mapper/loader.rb +0 -22
- data/lib/rom/mapper/loader/allocator.rb +0 -32
- data/lib/rom/mapper/loader/attribute_writer.rb +0 -23
- data/lib/rom/mapper/loader/object_builder.rb +0 -28
- data/spec/shared/unit/loader_call.rb +0 -13
- data/spec/shared/unit/loader_identity.rb +0 -13
- data/spec/shared/unit/mapper_context.rb +0 -13
- data/spec/unit/rom/mapper/call_spec.rb +0 -32
- data/spec/unit/rom/mapper/class_methods/build_spec.rb +0 -64
- data/spec/unit/rom/mapper/dump_spec.rb +0 -15
- data/spec/unit/rom/mapper/dumper/call_spec.rb +0 -29
- data/spec/unit/rom/mapper/dumper/identity_spec.rb +0 -28
- data/spec/unit/rom/mapper/header/each_spec.rb +0 -28
- data/spec/unit/rom/mapper/header/element_reader_spec.rb +0 -25
- data/spec/unit/rom/mapper/header/keys_spec.rb +0 -32
- data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +0 -15
- data/spec/unit/rom/mapper/identity_spec.rb +0 -15
- data/spec/unit/rom/mapper/load_spec.rb +0 -15
- data/spec/unit/rom/mapper/loader/allocator/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/allocator/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/attribute_writer/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/attribute_writer/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/object_builder/call_spec.rb +0 -7
- data/spec/unit/rom/mapper/loader/object_builder/identity_spec.rb +0 -7
- data/spec/unit/rom/mapper/model_spec.rb +0 -11
- data/spec/unit/rom/mapper/new_object_spec.rb +0 -14
data/lib/rom/mapper.rb
CHANGED
@@ -1,135 +1,98 @@
|
|
1
|
-
|
1
|
+
require 'rom/mapper/dsl'
|
2
2
|
|
3
|
-
|
3
|
+
require 'rom/support/inheritance_hook'
|
4
4
|
|
5
|
-
|
5
|
+
module ROM
|
6
|
+
# Mapper is a simple object that uses transformers to load relations
|
6
7
|
#
|
8
|
+
# @private
|
7
9
|
class Mapper
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
allocator: Loader::Allocator,
|
12
|
-
object_builder: Loader::ObjectBuilder,
|
13
|
-
attribute_writer: Loader::AttributeWriter
|
14
|
-
}
|
10
|
+
extend ROM::Support::InheritanceHook
|
11
|
+
include DSL
|
12
|
+
include Equalizer.new(:transformers, :header)
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
}
|
14
|
+
defines :relation, :register_as, :symbolize_keys,
|
15
|
+
:prefix, :prefix_separator, :inherit_header, :reject_keys
|
19
16
|
|
20
|
-
|
21
|
-
|
17
|
+
inherit_header true
|
18
|
+
reject_keys false
|
19
|
+
prefix_separator '_'.freeze
|
22
20
|
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# @example
|
26
|
-
#
|
27
|
-
# header = Mapper::Header.build([[:user_name, String]], map: { user_name: :name })
|
28
|
-
#
|
29
|
-
# mapper = Mapper.build(header, User)
|
30
|
-
# mapper = Mapper.build(header, User, loader_class: Loader::ObjectBuilder)
|
21
|
+
# @return [Object] transformers object built by a processor
|
31
22
|
#
|
32
|
-
# @
|
33
|
-
|
34
|
-
# @param [Hash]
|
35
|
-
#
|
36
|
-
# @return [Mapper]
|
37
|
-
#
|
38
|
-
# @api public
|
39
|
-
def self.build(header, model, options = {})
|
40
|
-
loader_class = LOADERS[options.fetch(:loader, DEFAULT_LOADER)]
|
41
|
-
dumper_class = DUMPERS[options.fetch(:dumper, DEFAULT_DUMPER)]
|
23
|
+
# @api private
|
24
|
+
attr_reader :transformers
|
42
25
|
|
43
|
-
|
44
|
-
|
45
|
-
|
26
|
+
# @return [Header] header that was used to build the transformers
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
attr_reader :header
|
46
30
|
|
47
|
-
|
31
|
+
# @return [Hash] registered processors
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
def self.processors
|
35
|
+
@_processors ||= {}
|
48
36
|
end
|
49
37
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# @example
|
53
|
-
#
|
54
|
-
# mapper.call(relation)
|
38
|
+
# Register a processor class
|
55
39
|
#
|
56
|
-
# @
|
40
|
+
# @return [Hash]
|
57
41
|
#
|
58
|
-
# @
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
mapping = header.mapping
|
63
|
-
attributes = mapping.keys
|
64
|
-
|
65
|
-
relation.project(attributes).rename(mapping)
|
42
|
+
# @api private
|
43
|
+
def self.register_processor(processor)
|
44
|
+
name = processor.name.split('::').last.downcase.to_sym
|
45
|
+
processors.update(name => processor)
|
66
46
|
end
|
67
47
|
|
68
|
-
#
|
69
|
-
#
|
70
|
-
# @example
|
48
|
+
# Prepares an array of headers for a potentially multistep mapper
|
71
49
|
#
|
72
|
-
#
|
50
|
+
# @return [Array<Header>]
|
73
51
|
#
|
74
|
-
# @
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def identity(object)
|
80
|
-
dumper.identity(object)
|
52
|
+
# @api private
|
53
|
+
def self.headers(header)
|
54
|
+
return [header] if steps.empty?
|
55
|
+
return steps.map(&:header) if attributes.empty?
|
56
|
+
raise(MapperMisconfiguredError, "cannot mix outer attributes and steps")
|
81
57
|
end
|
82
58
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
# @example
|
86
|
-
#
|
87
|
-
# mapper.identity_from_tuple({id: 1}) # => [1]
|
59
|
+
# Build a mapper using provided processor type
|
88
60
|
#
|
89
|
-
# @
|
90
|
-
#
|
91
|
-
# @return [Array]
|
61
|
+
# @return [Mapper]
|
92
62
|
#
|
93
|
-
# @api
|
94
|
-
def
|
95
|
-
|
63
|
+
# @api private
|
64
|
+
def self.build(header = self.header, processor = :transproc)
|
65
|
+
processor = Mapper.processors.fetch(processor)
|
66
|
+
transformers = headers(header).map(&processor.method(:build))
|
67
|
+
new(transformers, header)
|
96
68
|
end
|
97
69
|
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
#
|
105
|
-
# @api public
|
106
|
-
def new_object(*args, &block)
|
107
|
-
model.new(*args, &block)
|
70
|
+
# @api private
|
71
|
+
def self.registry(descendants)
|
72
|
+
descendants.each_with_object({}) do |klass, h|
|
73
|
+
name = klass.register_as || klass.relation
|
74
|
+
(h[klass.base_relation] ||= {})[name] = klass.build
|
75
|
+
end
|
108
76
|
end
|
109
77
|
|
110
|
-
#
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
# @api public
|
115
|
-
def model
|
116
|
-
loader.model
|
78
|
+
# @api private
|
79
|
+
def initialize(transformers, header)
|
80
|
+
@transformers = Array(transformers)
|
81
|
+
@header = header
|
117
82
|
end
|
118
83
|
|
119
|
-
#
|
84
|
+
# @return [Class] optional model that is instantiated by a mapper
|
120
85
|
#
|
121
86
|
# @api private
|
122
|
-
def
|
123
|
-
|
87
|
+
def model
|
88
|
+
header.model
|
124
89
|
end
|
125
90
|
|
126
|
-
#
|
91
|
+
# Process a relation using the transformers
|
127
92
|
#
|
128
93
|
# @api private
|
129
|
-
def
|
130
|
-
|
94
|
+
def call(relation)
|
95
|
+
transformers.reduce(relation.to_a) { |a, e| e.call(a) }
|
131
96
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
end # ROM
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,477 @@
|
|
1
|
+
require 'rom/header'
|
2
|
+
require 'rom/mapper/model_dsl'
|
3
|
+
|
4
|
+
module ROM
|
5
|
+
class Mapper
|
6
|
+
# Mapper attribute DSL exposed by mapper subclasses
|
7
|
+
#
|
8
|
+
# This class is private even though its methods are exposed by mappers.
|
9
|
+
# Typically it's not meant to be used directly.
|
10
|
+
#
|
11
|
+
# TODO: break this madness down into smaller pieces
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class AttributeDSL
|
15
|
+
include ModelDSL
|
16
|
+
|
17
|
+
attr_reader :attributes, :options, :symbolize_keys, :reject_keys, :steps
|
18
|
+
|
19
|
+
# @param [Array] attributes accumulator array
|
20
|
+
# @param [Hash] options
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def initialize(attributes, options)
|
24
|
+
@attributes = attributes
|
25
|
+
@options = options
|
26
|
+
@symbolize_keys = options.fetch(:symbolize_keys)
|
27
|
+
@prefix = options.fetch(:prefix)
|
28
|
+
@prefix_separator = options.fetch(:prefix_separator)
|
29
|
+
@reject_keys = options.fetch(:reject_keys)
|
30
|
+
@steps = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Redefine the prefix for the following attributes
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
#
|
37
|
+
# dsl = AttributeDSL.new([])
|
38
|
+
# dsl.attribute(:prefix, 'user')
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def prefix(value = Undefined)
|
42
|
+
if value.equal?(Undefined)
|
43
|
+
@prefix
|
44
|
+
else
|
45
|
+
@prefix = value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Redefine the prefix separator for the following attributes
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
#
|
53
|
+
# dsl = AttributeDSL.new([])
|
54
|
+
# dsl.attribute(:prefix_separator, '.')
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def prefix_separator(value = Undefined)
|
58
|
+
if value.equal?(Undefined)
|
59
|
+
@prefix_separator
|
60
|
+
else
|
61
|
+
@prefix_separator = value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Define a mapping attribute with its options and/or block
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# dsl = AttributeDSL.new([])
|
69
|
+
#
|
70
|
+
# dsl.attribute(:name)
|
71
|
+
# dsl.attribute(:email, from: 'user_email')
|
72
|
+
# dsl.attribute(:name) { 'John' }
|
73
|
+
# dsl.attribute(:name) { |t| t.upcase }
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def attribute(name, options = EMPTY_HASH, &block)
|
77
|
+
with_attr_options(name, options) do |attr_options|
|
78
|
+
raise ArgumentError,
|
79
|
+
"can't specify type and block at the same time" if options[:type] && block
|
80
|
+
attr_options[:coercer] = block if block
|
81
|
+
add_attribute(name, attr_options)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def exclude(name)
|
86
|
+
attributes << [name, { exclude: true }]
|
87
|
+
end
|
88
|
+
|
89
|
+
# Perform transformations sequentially
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# dsl = AttributeDSL.new()
|
93
|
+
#
|
94
|
+
# dsl.step do
|
95
|
+
# attribute :name
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def step(options = EMPTY_HASH, &block)
|
100
|
+
steps << new(options, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Define an embedded attribute
|
104
|
+
#
|
105
|
+
# Block exposes the attribute dsl too
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# dsl = AttributeDSL.new([])
|
109
|
+
#
|
110
|
+
# dsl.embedded :tags, type: :array do
|
111
|
+
# attribute :name
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# dsl.embedded :address, type: :hash do
|
115
|
+
# model Address
|
116
|
+
# attribute :name
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# @param [Symbol] name attribute
|
120
|
+
#
|
121
|
+
# @param [Hash] options
|
122
|
+
# @option options [Symbol] :type Embedded type can be :hash or :array
|
123
|
+
# @option options [Symbol] :prefix Prefix that should be used for
|
124
|
+
# its attributes
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
def embedded(name, options, &block)
|
128
|
+
with_attr_options(name) do |attr_options|
|
129
|
+
mapper = options[:mapper]
|
130
|
+
|
131
|
+
if mapper
|
132
|
+
embedded_options = { type: :array }.update(options)
|
133
|
+
attributes_from_mapper(
|
134
|
+
mapper, name, embedded_options.update(attr_options)
|
135
|
+
)
|
136
|
+
else
|
137
|
+
dsl = new(options, &block)
|
138
|
+
attr_options.update(options)
|
139
|
+
add_attribute(
|
140
|
+
name, { header: dsl.header, type: :array }.update(attr_options)
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Define an embedded hash attribute that requires "wrapping" transformation
|
147
|
+
#
|
148
|
+
# Typically this is used in sql context when relation is a join.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# dsl = AttributeDSL.new([])
|
152
|
+
#
|
153
|
+
# dsl.wrap(address: [:street, :zipcode, :city])
|
154
|
+
#
|
155
|
+
# dsl.wrap(:address) do
|
156
|
+
# model Address
|
157
|
+
# attribute :street
|
158
|
+
# attribute :zipcode
|
159
|
+
# attribute :city
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# @see AttributeDSL#embedded
|
163
|
+
#
|
164
|
+
# @api public
|
165
|
+
def wrap(*args, &block)
|
166
|
+
ensure_mapper_configuration('wrap', args, block_given?)
|
167
|
+
|
168
|
+
with_name_or_options(*args) do |name, options, mapper|
|
169
|
+
wrap_options = { type: :hash, wrap: true }.update(options)
|
170
|
+
|
171
|
+
if mapper
|
172
|
+
attributes_from_mapper(mapper, name, wrap_options)
|
173
|
+
else
|
174
|
+
dsl(name, wrap_options, &block)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Define an embedded hash attribute that requires "unwrapping" transformation
|
180
|
+
#
|
181
|
+
# Typically this is used in no-sql context to normalize data before
|
182
|
+
# inserting to sql gateway.
|
183
|
+
#
|
184
|
+
# @example
|
185
|
+
# dsl = AttributeDSL.new([])
|
186
|
+
#
|
187
|
+
# dsl.unwrap(address: [:street, :zipcode, :city])
|
188
|
+
#
|
189
|
+
# dsl.unwrap(:address) do
|
190
|
+
# attribute :street
|
191
|
+
# attribute :zipcode
|
192
|
+
# attribute :city
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# @see AttributeDSL#embedded
|
196
|
+
#
|
197
|
+
# @api public
|
198
|
+
def unwrap(*args, &block)
|
199
|
+
with_name_or_options(*args) do |name, options, mapper|
|
200
|
+
unwrap_options = { type: :hash, unwrap: true }.update(options)
|
201
|
+
|
202
|
+
if mapper
|
203
|
+
attributes_from_mapper(mapper, name, unwrap_options)
|
204
|
+
else
|
205
|
+
dsl(name, unwrap_options, &block)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Define an embedded hash attribute that requires "grouping" transformation
|
211
|
+
#
|
212
|
+
# Typically this is used in sql context when relation is a join.
|
213
|
+
#
|
214
|
+
# @example
|
215
|
+
# dsl = AttributeDSL.new([])
|
216
|
+
#
|
217
|
+
# dsl.group(tags: [:name])
|
218
|
+
#
|
219
|
+
# dsl.group(:tags) do
|
220
|
+
# model Tag
|
221
|
+
# attribute :name
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# @see AttributeDSL#embedded
|
225
|
+
#
|
226
|
+
# @api public
|
227
|
+
def group(*args, &block)
|
228
|
+
ensure_mapper_configuration('group', args, block_given?)
|
229
|
+
|
230
|
+
with_name_or_options(*args) do |name, options, mapper|
|
231
|
+
group_options = { type: :array, group: true }.update(options)
|
232
|
+
|
233
|
+
if mapper
|
234
|
+
attributes_from_mapper(mapper, name, group_options)
|
235
|
+
else
|
236
|
+
dsl(name, group_options, &block)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Define an embedded array attribute that requires "ungrouping" transformation
|
242
|
+
#
|
243
|
+
# Typically this is used in non-sql context being prepared for import to sql.
|
244
|
+
#
|
245
|
+
# @example
|
246
|
+
# dsl = AttributeDSL.new([])
|
247
|
+
# dsl.ungroup(tags: [:name])
|
248
|
+
#
|
249
|
+
# @see AttributeDSL#embedded
|
250
|
+
#
|
251
|
+
# @api public
|
252
|
+
def ungroup(*args, &block)
|
253
|
+
with_name_or_options(*args) do |name, options, *|
|
254
|
+
ungroup_options = { type: :array, ungroup: true }.update(options)
|
255
|
+
dsl(name, ungroup_options, &block)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Define an embedded hash attribute that requires "fold" transformation
|
260
|
+
#
|
261
|
+
# Typically this is used in sql context to fold single joined field
|
262
|
+
# to the array of values.
|
263
|
+
#
|
264
|
+
# @example
|
265
|
+
# dsl = AttributeDSL.new([])
|
266
|
+
#
|
267
|
+
# dsl.fold(tags: [:name])
|
268
|
+
#
|
269
|
+
# @see AttributeDSL#embedded
|
270
|
+
#
|
271
|
+
# @api public
|
272
|
+
def fold(*args, &block)
|
273
|
+
with_name_or_options(*args) do |name, *|
|
274
|
+
fold_options = { type: :array, fold: true }
|
275
|
+
dsl(name, fold_options, &block)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Define an embedded hash attribute that requires "unfold" transformation
|
280
|
+
#
|
281
|
+
# Typically this is used in non-sql context to convert array of
|
282
|
+
# values (like in Cassandra 'SET' or 'LIST' types) to array of tuples.
|
283
|
+
#
|
284
|
+
# Source values are assigned to the first key, the other keys being left blank.
|
285
|
+
#
|
286
|
+
# @example
|
287
|
+
# dsl = AttributeDSL.new([])
|
288
|
+
#
|
289
|
+
# dsl.unfold(tags: [:name, :type], from: :tags_list)
|
290
|
+
#
|
291
|
+
# dsl.unfold :tags, from: :tags_list do
|
292
|
+
# attribute :name, from: :tag_name
|
293
|
+
# attribute :type, from: :tag_type
|
294
|
+
# end
|
295
|
+
#
|
296
|
+
# @see AttributeDSL#embedded
|
297
|
+
#
|
298
|
+
# @api public
|
299
|
+
def unfold(name, options = EMPTY_HASH)
|
300
|
+
with_attr_options(name, options) do |attr_options|
|
301
|
+
old_name = attr_options.fetch(:from, name)
|
302
|
+
dsl(old_name, type: :array, unfold: true) do
|
303
|
+
attribute name, attr_options
|
304
|
+
yield if block_given?
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Define an embedded combined attribute that requires "combine" transformation
|
310
|
+
#
|
311
|
+
# Typically this can be used to process results of eager-loading
|
312
|
+
#
|
313
|
+
# @example
|
314
|
+
# dsl = AttributeDSL.new([])
|
315
|
+
#
|
316
|
+
# dsl.combine(:tags, user_id: :id) do
|
317
|
+
# model Tag
|
318
|
+
#
|
319
|
+
# attribute :name
|
320
|
+
# end
|
321
|
+
#
|
322
|
+
# @param [Symbol] name
|
323
|
+
# @param [Hash] options
|
324
|
+
# @option options [Hash] :on The "join keys"
|
325
|
+
# @option options [Symbol] :type The type, either :array (default) or :hash
|
326
|
+
#
|
327
|
+
# @api public
|
328
|
+
def combine(name, options, &block)
|
329
|
+
dsl = new(options, &block)
|
330
|
+
|
331
|
+
attr_opts = {
|
332
|
+
type: options.fetch(:type, :array),
|
333
|
+
keys: options.fetch(:on),
|
334
|
+
combine: true,
|
335
|
+
header: dsl.header
|
336
|
+
}
|
337
|
+
|
338
|
+
add_attribute(name, attr_opts)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Generate a header from attribute definitions
|
342
|
+
#
|
343
|
+
# @return [Header]
|
344
|
+
#
|
345
|
+
# @api private
|
346
|
+
def header
|
347
|
+
Header.coerce(attributes, model: model, reject_keys: reject_keys)
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
# Remove the attribute used somewhere else (in wrap, group, model etc.)
|
353
|
+
#
|
354
|
+
# @api private
|
355
|
+
def remove(*names)
|
356
|
+
attributes.delete_if { |attr| names.include?(attr.first) }
|
357
|
+
end
|
358
|
+
|
359
|
+
# Handle attribute options common for all definitions
|
360
|
+
#
|
361
|
+
# @api private
|
362
|
+
def with_attr_options(name, options = EMPTY_HASH)
|
363
|
+
attr_options = options.dup
|
364
|
+
|
365
|
+
if @prefix
|
366
|
+
attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
|
367
|
+
attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
|
368
|
+
end
|
369
|
+
|
370
|
+
if symbolize_keys
|
371
|
+
attr_options.update(from: attr_options.fetch(:from) { name }.to_s)
|
372
|
+
end
|
373
|
+
|
374
|
+
yield(attr_options)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Handle "name or options" syntax used by `wrap` and `group`
|
378
|
+
#
|
379
|
+
# @api private
|
380
|
+
def with_name_or_options(*args)
|
381
|
+
name, options =
|
382
|
+
if args.size > 1
|
383
|
+
args
|
384
|
+
else
|
385
|
+
[args.first, {}]
|
386
|
+
end
|
387
|
+
|
388
|
+
yield(name, options, options[:mapper])
|
389
|
+
end
|
390
|
+
|
391
|
+
# Create another instance of the dsl for nested definitions
|
392
|
+
#
|
393
|
+
# This is used by embedded, wrap and group
|
394
|
+
#
|
395
|
+
# @api private
|
396
|
+
def dsl(name_or_attrs, options, &block)
|
397
|
+
if block
|
398
|
+
attributes_from_block(name_or_attrs, options, &block)
|
399
|
+
else
|
400
|
+
attributes_from_hash(name_or_attrs, options)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Define attributes from a nested block
|
405
|
+
#
|
406
|
+
# Used by embedded, wrap and group
|
407
|
+
#
|
408
|
+
# @api private
|
409
|
+
def attributes_from_block(name, options, &block)
|
410
|
+
dsl = new(options, &block)
|
411
|
+
header = dsl.header
|
412
|
+
add_attribute(name, options.update(header: header))
|
413
|
+
header.each { |attr| remove(attr.key) unless name == attr.key }
|
414
|
+
end
|
415
|
+
|
416
|
+
# Define attributes from the `name => attributes` hash syntax
|
417
|
+
#
|
418
|
+
# Used by wrap and group
|
419
|
+
#
|
420
|
+
# @api private
|
421
|
+
def attributes_from_hash(hash, options)
|
422
|
+
hash.each do |name, header|
|
423
|
+
with_attr_options(name, options) do |attr_options|
|
424
|
+
add_attribute(name, attr_options.update(header: header.zip))
|
425
|
+
header.each { |attr| remove(attr) unless name == attr }
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# Infer mapper header for an embedded attribute
|
431
|
+
#
|
432
|
+
# @api private
|
433
|
+
def attributes_from_mapper(mapper, name, options)
|
434
|
+
if mapper.is_a?(Class)
|
435
|
+
add_attribute(name, { header: mapper.header }.update(options))
|
436
|
+
else
|
437
|
+
raise(
|
438
|
+
ArgumentError, ":mapper must be a class #{mapper.inspect}"
|
439
|
+
)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Add a new attribute and make sure it overrides previous definition
|
444
|
+
#
|
445
|
+
# @api private
|
446
|
+
def add_attribute(name, options)
|
447
|
+
remove(name, name.to_s)
|
448
|
+
attributes << [name, options]
|
449
|
+
end
|
450
|
+
|
451
|
+
# Create a new dsl instance of potentially overidden options
|
452
|
+
#
|
453
|
+
# Embedded, wrap and group can override top-level options like `prefix`
|
454
|
+
#
|
455
|
+
# @api private
|
456
|
+
def new(options, &block)
|
457
|
+
dsl = self.class.new([], @options.merge(options))
|
458
|
+
dsl.instance_exec(&block) unless block.nil?
|
459
|
+
dsl
|
460
|
+
end
|
461
|
+
|
462
|
+
# Ensure the mapping configuration isn't ambiguous
|
463
|
+
#
|
464
|
+
# @api private
|
465
|
+
def ensure_mapper_configuration(method_name, args, block_present)
|
466
|
+
if args.first.is_a?(Hash) && block_present
|
467
|
+
raise MapperMisconfiguredError,
|
468
|
+
"Cannot configure `#{method_name}#` using both options and a block"
|
469
|
+
end
|
470
|
+
if args.first.is_a?(Hash) && args.first[:mapper]
|
471
|
+
raise MapperMisconfiguredError,
|
472
|
+
"Cannot configure `#{method_name}#` using both options and a mapper"
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|