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
@@ -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