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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -1
  4. data/.travis.yml +19 -13
  5. data/{Changelog.md → CHANGELOG.md} +6 -0
  6. data/Gemfile +23 -10
  7. data/README.md +17 -12
  8. data/Rakefile +12 -4
  9. data/lib/rom-mapper.rb +6 -15
  10. data/lib/rom/header.rb +195 -0
  11. data/lib/rom/header/attribute.rb +184 -0
  12. data/lib/rom/mapper.rb +63 -100
  13. data/lib/rom/mapper/attribute_dsl.rb +477 -0
  14. data/lib/rom/mapper/dsl.rb +120 -0
  15. data/lib/rom/mapper/model_dsl.rb +55 -0
  16. data/lib/rom/mapper/version.rb +3 -7
  17. data/lib/rom/model_builder.rb +99 -0
  18. data/lib/rom/processor.rb +28 -0
  19. data/lib/rom/processor/transproc.rb +388 -0
  20. data/rakelib/benchmark.rake +15 -0
  21. data/rakelib/mutant.rake +16 -0
  22. data/rakelib/rubocop.rake +18 -0
  23. data/rom-mapper.gemspec +7 -6
  24. data/spec/spec_helper.rb +32 -33
  25. data/spec/support/constant_leak_finder.rb +14 -0
  26. data/spec/support/mutant.rb +10 -0
  27. data/spec/unit/rom/mapper/dsl_spec.rb +467 -0
  28. data/spec/unit/rom/mapper_spec.rb +83 -0
  29. data/spec/unit/rom/processor/transproc_spec.rb +448 -0
  30. metadata +68 -89
  31. data/.ruby-version +0 -1
  32. data/Gemfile.devtools +0 -55
  33. data/config/devtools.yml +0 -2
  34. data/config/flay.yml +0 -3
  35. data/config/flog.yml +0 -2
  36. data/config/mutant.yml +0 -3
  37. data/config/reek.yml +0 -103
  38. data/config/rubocop.yml +0 -45
  39. data/lib/rom/mapper/attribute.rb +0 -31
  40. data/lib/rom/mapper/dumper.rb +0 -27
  41. data/lib/rom/mapper/loader.rb +0 -22
  42. data/lib/rom/mapper/loader/allocator.rb +0 -32
  43. data/lib/rom/mapper/loader/attribute_writer.rb +0 -23
  44. data/lib/rom/mapper/loader/object_builder.rb +0 -28
  45. data/spec/shared/unit/loader_call.rb +0 -13
  46. data/spec/shared/unit/loader_identity.rb +0 -13
  47. data/spec/shared/unit/mapper_context.rb +0 -13
  48. data/spec/unit/rom/mapper/call_spec.rb +0 -32
  49. data/spec/unit/rom/mapper/class_methods/build_spec.rb +0 -64
  50. data/spec/unit/rom/mapper/dump_spec.rb +0 -15
  51. data/spec/unit/rom/mapper/dumper/call_spec.rb +0 -29
  52. data/spec/unit/rom/mapper/dumper/identity_spec.rb +0 -28
  53. data/spec/unit/rom/mapper/header/each_spec.rb +0 -28
  54. data/spec/unit/rom/mapper/header/element_reader_spec.rb +0 -25
  55. data/spec/unit/rom/mapper/header/keys_spec.rb +0 -32
  56. data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +0 -15
  57. data/spec/unit/rom/mapper/identity_spec.rb +0 -15
  58. data/spec/unit/rom/mapper/load_spec.rb +0 -15
  59. data/spec/unit/rom/mapper/loader/allocator/call_spec.rb +0 -7
  60. data/spec/unit/rom/mapper/loader/allocator/identity_spec.rb +0 -7
  61. data/spec/unit/rom/mapper/loader/attribute_writer/call_spec.rb +0 -7
  62. data/spec/unit/rom/mapper/loader/attribute_writer/identity_spec.rb +0 -7
  63. data/spec/unit/rom/mapper/loader/object_builder/call_spec.rb +0 -7
  64. data/spec/unit/rom/mapper/loader/object_builder/identity_spec.rb +0 -7
  65. data/spec/unit/rom/mapper/model_spec.rb +0 -11
  66. data/spec/unit/rom/mapper/new_object_spec.rb +0 -14
data/lib/rom/mapper.rb CHANGED
@@ -1,135 +1,98 @@
1
- # encoding: utf-8
1
+ require 'rom/mapper/dsl'
2
2
 
3
- module ROM
3
+ require 'rom/support/inheritance_hook'
4
4
 
5
- # Mappers load tuples into objects and dump objects back into tuples
5
+ module ROM
6
+ # Mapper is a simple object that uses transformers to load relations
6
7
  #
8
+ # @private
7
9
  class Mapper
8
- include Concord::Public.new(:header, :loader, :dumper)
9
-
10
- LOADERS = {
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
- DUMPERS = {
17
- default: Dumper
18
- }
14
+ defines :relation, :register_as, :symbolize_keys,
15
+ :prefix, :prefix_separator, :inherit_header, :reject_keys
19
16
 
20
- DEFAULT_LOADER = :allocator
21
- DEFAULT_DUMPER = :default
17
+ inherit_header true
18
+ reject_keys false
19
+ prefix_separator '_'.freeze
22
20
 
23
- # Build a mapper
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
- # @param [Header]
33
- # @param [Class]
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
- header = Header.build(header, options)
44
- loader = loader_class.new(header, model)
45
- dumper = dumper_class.new(header)
26
+ # @return [Header] header that was used to build the transformers
27
+ #
28
+ # @api private
29
+ attr_reader :header
46
30
 
47
- new(header, loader, dumper)
31
+ # @return [Hash] registered processors
32
+ #
33
+ # @api private
34
+ def self.processors
35
+ @_processors ||= {}
48
36
  end
49
37
 
50
- # Project and rename given relation
51
- #
52
- # @example
53
- #
54
- # mapper.call(relation)
38
+ # Register a processor class
55
39
  #
56
- # @param [Axiom::Relation]
40
+ # @return [Hash]
57
41
  #
58
- # @return [Axiom::Relation]
59
- #
60
- # @api public
61
- def call(relation)
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
- # Retrieve identity from the given object
69
- #
70
- # @example
48
+ # Prepares an array of headers for a potentially multistep mapper
71
49
  #
72
- # mapper.identity(user) # => [1]
50
+ # @return [Array<Header>]
73
51
  #
74
- # @param [Object]
75
- #
76
- # @return [Array]
77
- #
78
- # @api public
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
- # Return identity from the given tuple
84
- #
85
- # @example
86
- #
87
- # mapper.identity_from_tuple({id: 1}) # => [1]
59
+ # Build a mapper using provided processor type
88
60
  #
89
- # @param [Axiom::Tuple,Hash]
90
- #
91
- # @return [Array]
61
+ # @return [Mapper]
92
62
  #
93
- # @api public
94
- def identity_from_tuple(tuple)
95
- loader.identity(tuple)
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
- # Build a new model instance
99
- #
100
- # @example
101
- #
102
- # mapper = Mapper.build(header, User)
103
- # mapper.new_object(id: 1, name: 'Jane') # => #<User @id=1 @name="Jane">
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
- # Return model used by this mapper
111
- #
112
- # @return [Class]
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
- # Load an object instance from the tuple
84
+ # @return [Class] optional model that is instantiated by a mapper
120
85
  #
121
86
  # @api private
122
- def load(tuple)
123
- loader.call(tuple)
87
+ def model
88
+ header.model
124
89
  end
125
90
 
126
- # Dump an object into a tuple
91
+ # Process a relation using the transformers
127
92
  #
128
93
  # @api private
129
- def dump(object)
130
- dumper.call(object)
94
+ def call(relation)
95
+ transformers.reduce(relation.to_a) { |a, e| e.call(a) }
131
96
  end
132
-
133
- end # Mapper
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