rom-mapper 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,15 @@
1
+ desc "Run benchmarks (tweak count via COUNT envvar)"
2
+ task :benchmark do
3
+ FileList["benchmarks/**/*_bench.rb"].each do |bench|
4
+ sh "ruby #{bench}"
5
+ end
6
+ end
7
+
8
+ namespace :benchmark do
9
+ desc "Verify benchmarks"
10
+ task :verify do
11
+ ENV['VERIFY'] = "true"
12
+ ENV['COUNT'] = "1"
13
+ Rake::Task[:benchmark].invoke
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ desc "Run mutant against a specific subject"
2
+ task :mutant do
3
+ subject = ARGV.last
4
+ if subject == 'mutant'
5
+ abort "usage: rake mutant SUBJECT\nexample: rake mutant ROM::Header"
6
+ else
7
+ opts = {
8
+ 'include' => 'lib',
9
+ 'require' => 'rom',
10
+ 'use' => 'rspec',
11
+ 'ignore-subject' => "#{subject}#respond_to_missing?"
12
+ }.to_a.map { |k, v| "--#{k} #{v}" }.join(' ')
13
+
14
+ exec("bundle exec mutant #{opts} #{subject}")
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require "rubocop/rake_task"
3
+
4
+ Rake::Task[:default].enhance [:rubocop]
5
+
6
+ RuboCop::RakeTask.new do |task|
7
+ task.options << "--display-cop-names"
8
+ end
9
+
10
+ namespace :rubocop do
11
+ desc 'Generate a configuration file acting as a TODO list.'
12
+ task :auto_gen_config do
13
+ exec "bundle exec rubocop --auto-gen-config"
14
+ end
15
+ end
16
+
17
+ rescue LoadError
18
+ end
data/rom-mapper.gemspec CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/rom/mapper/version', __FILE__)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "rom-mapper"
7
- gem.description = "rom-mapper"
7
+ gem.description = "ROM mapper component"
8
8
  gem.summary = gem.description
9
9
  gem.authors = 'Piotr Solnica'
10
10
  gem.email = 'piotr.solnica@gmail.com'
@@ -15,9 +15,10 @@ Gem::Specification.new do |gem|
15
15
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
16
16
  gem.license = 'MIT'
17
17
 
18
- gem.add_dependency 'concord', '~> 0.1.4'
19
- gem.add_dependency 'equalizer', '~> 0.0.7'
20
- gem.add_dependency 'descendants_tracker', '~> 0.0.1'
21
- gem.add_dependency 'abstract_type', '~> 0.0.6'
22
- gem.add_dependency 'adamantium', '~> 0.1'
18
+ gem.add_dependency 'transproc', '~> 0.3'
19
+ gem.add_dependency 'equalizer', '~> 0.0', '>= 0.0.10'
20
+ gem.add_dependency 'rom-support', '~> 0.1', '>= 0.1.0'
21
+
22
+ gem.add_development_dependency 'rake', '~> 10.3'
23
+ gem.add_development_dependency 'rspec', '~> 3.3'
23
24
  end
data/spec/spec_helper.rb CHANGED
@@ -1,48 +1,47 @@
1
1
  # encoding: utf-8
2
2
 
3
- if ENV['COVERAGE'] == 'true'
4
- require 'simplecov'
5
- require 'coveralls'
3
+ # this is needed for guard to work, not sure why :(
4
+ require "bundler"
5
+ Bundler.setup
6
6
 
7
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
- SimpleCov::Formatter::HTMLFormatter,
9
- Coveralls::SimpleCov::Formatter
10
- ]
11
-
12
- SimpleCov.start do
13
- command_name 'spec:unit'
14
-
15
- add_filter 'config'
16
- add_filter 'lib/rom/support'
17
- add_filter 'spec'
18
- end
7
+ if RUBY_ENGINE == "rbx"
8
+ require "codeclimate-test-reporter"
9
+ CodeClimate::TestReporter.start
19
10
  end
20
11
 
21
12
  require 'rom-mapper'
22
- require 'axiom'
23
-
24
- require 'devtools/spec_helper'
25
- require 'bogus/rspec'
26
13
 
27
- Bogus.configure do |config|
28
- config.search_modules << ROM
14
+ begin
15
+ require 'byebug'
16
+ rescue LoadError
29
17
  end
30
18
 
31
- RSpec.configure do |config|
32
- config.mock_with Bogus::RSpecAdapter
19
+ root = Pathname(__FILE__).dirname
20
+
21
+ Dir[root.join('support/*.rb').to_s].each do |f|
22
+ require f
23
+ end
24
+ Dir[root.join('shared/*.rb').to_s].each do |f|
25
+ require f
33
26
  end
34
27
 
35
- include ROM
28
+ # Namespace holding all objects created during specs
29
+ module Test
30
+ def self.remove_constants
31
+ constants.each(&method(:remove_const))
32
+ end
33
+ end
36
34
 
37
- def mock_model(*attributes)
38
- Class.new {
39
- include Equalizer.new(*attributes)
35
+ def T(*args)
36
+ ROM::Processor::Transproc::Functions[*args]
37
+ end
40
38
 
41
- attributes.each { |attribute| attr_accessor attribute }
39
+ RSpec.configure do |config|
40
+ config.after do
41
+ Test.remove_constants
42
+ end
42
43
 
43
- def initialize(attrs, &block)
44
- attrs.each { |name, value| send("#{name}=", value) }
45
- instance_eval(&block) if block
46
- end
47
- }
44
+ config.around do |example|
45
+ ConstantLeakFinder.find(example)
46
+ end
48
47
  end
@@ -0,0 +1,14 @@
1
+ # Finds leaking constants created during ROM specs
2
+ module ConstantLeakFinder
3
+ def self.find(example)
4
+ constants = Object.constants
5
+
6
+ example.run
7
+
8
+ added_constants = (Object.constants - constants)
9
+ added = added_constants.map(&Object.method(:const_get))
10
+ if added.any? { |mod| mod.ancestors.map(&:name).grep(/\AROM/).any? }
11
+ raise "Leaking constants: #{added_constants.inspect}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Mutant
2
+ class Selector
3
+ # Expression based test selector
4
+ class Expression < self
5
+ def call(_subject)
6
+ integration.all_tests
7
+ end
8
+ end # Expression
9
+ end # Selector
10
+ end # Mutant
@@ -0,0 +1,467 @@
1
+ require 'spec_helper'
2
+
3
+ describe ROM::Mapper do
4
+ subject(:mapper) do
5
+ klass = Class.new(parent)
6
+ options.each do |k, v|
7
+ klass.send(k, v)
8
+ end
9
+ klass
10
+ end
11
+
12
+ let(:parent) { Class.new(ROM::Mapper) }
13
+
14
+ let(:options) { {} }
15
+ let(:header) { mapper.header }
16
+
17
+ let(:expected_header) { ROM::Header.coerce(attributes) }
18
+
19
+ describe '#attribute' do
20
+ context 'simple attribute' do
21
+ let(:attributes) { [[:name]] }
22
+
23
+ it 'adds an attribute for the header' do
24
+ mapper.attribute :name
25
+
26
+ expect(header).to eql(expected_header)
27
+ end
28
+ end
29
+
30
+ context 'aliased attribute' do
31
+ let(:attributes) { [[:name, from: :user_name]] }
32
+
33
+ it 'adds an aliased attribute for the header' do
34
+ mapper.attribute :name, from: :user_name
35
+
36
+ expect(header).to eql(expected_header)
37
+ end
38
+ end
39
+
40
+ context 'prefixed attribute' do
41
+ let(:attributes) { [[:name, from: :user_name]] }
42
+ let(:options) { { prefix: :user } }
43
+
44
+ it 'adds an aliased attribute for the header using configured :prefix' do
45
+ mapper.attribute :name
46
+
47
+ expect(header).to eql(expected_header)
48
+ end
49
+ end
50
+
51
+ context 'prefixed attribute using custom separator' do
52
+ let(:attributes) { [[:name, from: :'u.name']] }
53
+ let(:options) { { prefix: :u, prefix_separator: '.' } }
54
+
55
+ it 'adds an aliased attribute for the header using configured :prefix' do
56
+ mapper.attribute :name
57
+
58
+ expect(header).to eql(expected_header)
59
+ end
60
+ end
61
+
62
+ context 'symbolized attribute' do
63
+ let(:attributes) { [[:name, from: 'name']] }
64
+ let(:options) { { symbolize_keys: true } }
65
+
66
+ it 'adds an attribute with symbolized alias' do
67
+ mapper.attribute :name
68
+
69
+ expect(header).to eql(expected_header)
70
+ end
71
+ end
72
+ end
73
+
74
+ describe 'reject_keys' do
75
+ let(:attributes) { [[:name, type: :string]] }
76
+ let(:options) { { reject_keys: true } }
77
+
78
+ it 'sets rejected_keys for the header' do
79
+ mapper.reject_keys true
80
+ mapper.attribute :name, type: :string
81
+
82
+ expect(header).to eql(expected_header)
83
+ end
84
+ end
85
+
86
+ describe 'overriding inherited attributes' do
87
+ context 'when name matches' do
88
+ let(:attributes) { [[:name, type: :string]] }
89
+
90
+ it 'excludes the inherited attribute' do
91
+ parent.attribute :name
92
+
93
+ mapper.attribute :name, type: :string
94
+
95
+ expect(header).to eql(expected_header)
96
+ end
97
+ end
98
+
99
+ context 'when alias matches' do
100
+ let(:attributes) { [[:name, from: 'name', type: :string]] }
101
+
102
+ it 'excludes the inherited attribute' do
103
+ parent.attribute 'name'
104
+
105
+ mapper.attribute :name, from: 'name', type: :string
106
+
107
+ expect(header).to eql(expected_header)
108
+ end
109
+ end
110
+
111
+ context 'when name in a wrapped attribute matches' do
112
+ let(:attributes) do
113
+ [
114
+ [:city, type: :hash, wrap: true, header: [[:name, from: :city_name]]]
115
+ ]
116
+ end
117
+
118
+ it 'excludes the inherited attribute' do
119
+ parent.attribute :city_name
120
+
121
+ mapper.wrap :city do
122
+ attribute :name, from: :city_name
123
+ end
124
+
125
+ expect(header).to eql(expected_header)
126
+ end
127
+ end
128
+
129
+ context 'when name in a grouped attribute matches' do
130
+ let(:attributes) do
131
+ [
132
+ [:tags, type: :array, group: true, header: [[:name, from: :tag_name]]]
133
+ ]
134
+ end
135
+
136
+ it 'excludes the inherited attribute' do
137
+ parent.attribute :tag_name
138
+
139
+ mapper.group :tags do
140
+ attribute :name, from: :tag_name
141
+ end
142
+
143
+ expect(header).to eql(expected_header)
144
+ end
145
+ end
146
+
147
+ context 'when name in a hash attribute matches' do
148
+ let(:attributes) do
149
+ [
150
+ [:city, type: :hash, header: [[:name, from: :city_name]]]
151
+ ]
152
+ end
153
+
154
+ it 'excludes the inherited attribute' do
155
+ parent.attribute :city
156
+
157
+ mapper.embedded :city, type: :hash do
158
+ attribute :name, from: :city_name
159
+ end
160
+
161
+ expect(header).to eql(expected_header)
162
+ end
163
+ end
164
+
165
+ context 'when name of an array attribute matches' do
166
+ let(:attributes) do
167
+ [
168
+ [:tags, type: :array, header: [[:name, from: :tag_name]]]
169
+ ]
170
+ end
171
+
172
+ it 'excludes the inherited attribute' do
173
+ parent.attribute :tags
174
+
175
+ mapper.embedded :tags, type: :array do
176
+ attribute :name, from: :tag_name
177
+ end
178
+
179
+ expect(header).to eql(expected_header)
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#exclude' do
185
+ let(:attributes) { [[:name, from: 'name']] }
186
+
187
+ it 'removes an attribute from the inherited header' do
188
+ mapper.attribute :name, from: 'name'
189
+ expect(header).to eql(expected_header)
190
+ end
191
+ end
192
+
193
+ describe '#embedded' do
194
+ context 'when :type is set to :hash' do
195
+ let(:attributes) { [[:city, type: :hash, header: [[:name]]]] }
196
+
197
+ it 'adds an embedded hash attribute' do
198
+ mapper.embedded :city, type: :hash do
199
+ attribute :name
200
+ end
201
+
202
+ expect(header).to eql(expected_header)
203
+ end
204
+ end
205
+
206
+ context 'when :type is set to :array' do
207
+ let(:attributes) { [[:tags, type: :array, header: [[:name]]]] }
208
+
209
+ it 'adds an embedded array attribute' do
210
+ mapper.embedded :tags, type: :array do
211
+ attribute :name
212
+ end
213
+
214
+ expect(header).to eql(expected_header)
215
+ end
216
+ end
217
+ end
218
+
219
+ describe '#wrap' do
220
+ let(:attributes) { [[:city, type: :hash, wrap: true, header: [[:name]]]] }
221
+
222
+ it 'adds an wrapped hash attribute using a block to define attributes' do
223
+ mapper.wrap :city do
224
+ attribute :name
225
+ end
226
+
227
+ expect(header).to eql(expected_header)
228
+ end
229
+
230
+ it 'adds an wrapped hash attribute using a options define attributes' do
231
+ mapper.wrap city: [:name]
232
+
233
+ expect(header).to eql(expected_header)
234
+ end
235
+
236
+ it 'raises an exception when using a block and options to define attributes' do
237
+ expect {
238
+ mapper.wrap(city: [:name]) { attribute :other_name }
239
+ }.to raise_error(ROM::MapperMisconfiguredError)
240
+ end
241
+
242
+ it 'raises an exception when using options and a mapper to define attributes' do
243
+ task_mapper = Class.new(ROM::Mapper) { attribute :title }
244
+ expect {
245
+ mapper.wrap city: [:name], mapper: task_mapper
246
+ }.to raise_error(ROM::MapperMisconfiguredError)
247
+ end
248
+ end
249
+
250
+ describe '#group' do
251
+ let(:attributes) { [[:tags, type: :array, group: true, header: [[:name]]]] }
252
+
253
+ it 'adds a group attribute using a block to define attributes' do
254
+ mapper.group :tags do
255
+ attribute :name
256
+ end
257
+
258
+ expect(header).to eql(expected_header)
259
+ end
260
+
261
+ it 'adds a group attribute using a options define attributes' do
262
+ mapper.group tags: [:name]
263
+
264
+ expect(header).to eql(expected_header)
265
+ end
266
+
267
+ it 'raises an exception when using a block and options to define attributes' do
268
+ expect {
269
+ mapper.group(cities: [:name]) { attribute :other_name }
270
+ }.to raise_error(ROM::MapperMisconfiguredError)
271
+ end
272
+
273
+ it 'raises an exception when using options and a mapper to define attributes' do
274
+ task_mapper = Class.new(ROM::Mapper) { attribute :title }
275
+ expect {
276
+ mapper.group cities: [:name], mapper: task_mapper
277
+ }.to raise_error(ROM::MapperMisconfiguredError)
278
+ end
279
+ end
280
+
281
+ describe 'top-level :prefix option' do
282
+ let(:options) do
283
+ { prefix: :user }
284
+ end
285
+
286
+ context 'when no attribute overrides top-level setting' do
287
+ let(:attributes) do
288
+ [
289
+ [:name, from: :user_name],
290
+ [:address, from: :user_address, type: :hash, header: [
291
+ [:city, from: :user_city]]
292
+ ],
293
+ [:contact, type: :hash, wrap: true, header: [
294
+ [:mobile, from: :user_mobile]]
295
+ ],
296
+ [:tasks, type: :array, group: true, header: [
297
+ [:title, from: :user_title]]
298
+ ]
299
+ ]
300
+ end
301
+
302
+ it 'sets aliased attributes using prefix automatically' do
303
+ mapper.attribute :name
304
+
305
+ mapper.embedded :address, type: :hash do
306
+ attribute :city
307
+ end
308
+
309
+ mapper.wrap :contact do
310
+ attribute :mobile
311
+ end
312
+
313
+ mapper.group :tasks do
314
+ attribute :title
315
+ end
316
+
317
+ expect(header).to eql(expected_header)
318
+ end
319
+ end
320
+
321
+ context 'when an attribute overrides top-level setting' do
322
+ let(:attributes) do
323
+ [
324
+ [:name, from: :user_name],
325
+ [:birthday, from: :user_birthday, type: :hash, header: [
326
+ [:year, from: :bd_year],
327
+ [:month, from: :bd_month],
328
+ [:day, from: :bd_day]]
329
+ ],
330
+ [:address, from: :user_address, type: :hash, header: [[:city]]],
331
+ [:contact, type: :hash, wrap: true, header: [
332
+ [:mobile, from: :contact_mobile]]
333
+ ],
334
+ [:tasks, type: :array, group: true, header: [
335
+ [:title, from: :task_title]]
336
+ ]
337
+ ]
338
+ end
339
+
340
+ it 'excludes from aliasing the ones which override it' do
341
+ mapper.attribute :name
342
+
343
+ mapper.embedded :birthday, type: :hash, prefix: :bd do
344
+ attribute :year
345
+ attribute :month
346
+ attribute :day
347
+ end
348
+
349
+ mapper.embedded :address, type: :hash, prefix: false do
350
+ attribute :city
351
+ end
352
+
353
+ mapper.wrap :contact, prefix: :contact do
354
+ attribute :mobile
355
+ end
356
+
357
+ mapper.group :tasks, prefix: :task do
358
+ attribute :title
359
+ end
360
+
361
+ expect(header).to eql(expected_header)
362
+ end
363
+ end
364
+ end
365
+
366
+ context 'reusing mappers' do
367
+ describe '#group' do
368
+ let(:task_mapper) do
369
+ Class.new(ROM::Mapper) { attribute :title }
370
+ end
371
+
372
+ let(:attributes) do
373
+ [
374
+ [:name],
375
+ [:tasks, type: :array, group: true, header: task_mapper.header]
376
+ ]
377
+ end
378
+
379
+ it 'uses other mapper header' do
380
+ mapper.attribute :name
381
+ mapper.group :tasks, mapper: task_mapper
382
+
383
+ expect(header).to eql(expected_header)
384
+ end
385
+ end
386
+
387
+ describe '#wrap' do
388
+ let(:task_mapper) do
389
+ Class.new(ROM::Mapper) { attribute :title }
390
+ end
391
+
392
+ let(:attributes) do
393
+ [
394
+ [:name],
395
+ [:task, type: :hash, wrap: true, header: task_mapper.header]
396
+ ]
397
+ end
398
+
399
+ it 'uses other mapper header' do
400
+ mapper.attribute :name
401
+ mapper.wrap :task, mapper: task_mapper
402
+
403
+ expect(header).to eql(expected_header)
404
+ end
405
+ end
406
+
407
+ describe '#embedded' do
408
+ let(:task_mapper) do
409
+ Class.new(ROM::Mapper) { attribute :title }
410
+ end
411
+
412
+ let(:attributes) do
413
+ [
414
+ [:name],
415
+ [:task, type: :hash, header: task_mapper.header]
416
+ ]
417
+ end
418
+
419
+ it 'uses other mapper header' do
420
+ mapper.attribute :name
421
+ mapper.embedded :task, mapper: task_mapper, type: :hash
422
+
423
+ expect(header).to eql(expected_header)
424
+ end
425
+ end
426
+ end
427
+
428
+ describe '#combine' do
429
+ let(:attributes) do
430
+ [
431
+ [:title],
432
+ [:tasks, combine: true, type: :array, header: [[:title]]]
433
+ ]
434
+ end
435
+
436
+ it 'adds combine attributes' do
437
+ mapper.attribute :title
438
+
439
+ mapper.combine :tasks, on: { title: :title } do
440
+ attribute :title
441
+ end
442
+
443
+ expect(header).to eql(expected_header)
444
+ end
445
+
446
+ it 'works without a block' do
447
+ expected_header = ROM::Header.coerce(
448
+ [
449
+ [:title],
450
+ [:tasks, combine: true, type: :array, header: []]
451
+ ]
452
+ )
453
+
454
+ mapper.attribute :title
455
+
456
+ mapper.combine :tasks, on: { title: :title }
457
+
458
+ expect(header).to eql(expected_header)
459
+ end
460
+ end
461
+
462
+ describe '#method_missing' do
463
+ it 'responds to DSL methods' do
464
+ expect(mapper).to respond_to(:attribute)
465
+ end
466
+ end
467
+ end