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,83 @@
1
+ require 'spec_helper'
2
+
3
+ require 'ostruct'
4
+
5
+ describe ROM::Mapper do
6
+ subject(:mapper) { mapper_class.build }
7
+
8
+ let(:mapper_class) do
9
+ user_model = self.user_model
10
+
11
+ Class.new(ROM::Mapper) do
12
+ attribute :id
13
+ attribute :name
14
+ model user_model
15
+ end
16
+ end
17
+
18
+ let(:relation) do
19
+ [{ id: 1, name: 'Jane' }, { id: 2, name: 'Joe' }]
20
+ end
21
+
22
+ let(:user_model) do
23
+ Class.new(OpenStruct) { include Equalizer.new(:id, :name) }
24
+ end
25
+
26
+ let(:jane) { user_model.new(id: 1, name: 'Jane') }
27
+ let(:joe) { user_model.new(id: 2, name: 'Joe') }
28
+
29
+ describe '.registry' do
30
+ it 'builds mapper class registry for base and virtual relations' do
31
+ users = Class.new(ROM::Mapper) { relation(:users) }
32
+ entity = Class.new(ROM::Mapper) do
33
+ relation(:users)
34
+ register_as(:entity)
35
+ end
36
+ active = Class.new(users) { relation(:active) }
37
+ admins = Class.new(users) { relation(:admins) }
38
+ custom = Class.new(users) { register_as(:custom) }
39
+
40
+ registry = ROM::Mapper.registry([users, entity, active, admins, custom])
41
+
42
+ expect(registry).to eql(
43
+ users: {
44
+ users: users.build,
45
+ entity: entity.build,
46
+ active: active.build,
47
+ admins: admins.build,
48
+ custom: custom.build
49
+ }
50
+ )
51
+ end
52
+ end
53
+
54
+ describe '.relation' do
55
+ it 'inherits from parent' do
56
+ base = Class.new(ROM::Mapper) { relation(:users) }
57
+ virt = Class.new(base)
58
+
59
+ expect(virt.relation).to be(:users)
60
+ expect(virt.base_relation).to be(:users)
61
+ end
62
+
63
+ it 'allows overriding' do
64
+ base = Class.new(ROM::Mapper) { relation(:users) }
65
+ virt = Class.new(base) { relation(:active) }
66
+
67
+ expect(virt.relation).to be(:active)
68
+ expect(virt.base_relation).to be(:users)
69
+ end
70
+ end
71
+
72
+ describe "#each" do
73
+ it "yields all mapped objects" do
74
+ result = []
75
+
76
+ mapper.call(relation).each do |tuple|
77
+ result << tuple
78
+ end
79
+
80
+ expect(result).to eql([jane, joe])
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,448 @@
1
+ require 'spec_helper'
2
+ require 'virtus'
3
+
4
+ describe ROM::Processor::Transproc do
5
+ subject(:transproc) { ROM::Processor::Transproc.build(header) }
6
+
7
+ let(:header) { ROM::Header.coerce(attributes, options) }
8
+ let(:options) { {} }
9
+
10
+ context 'no mapping' do
11
+ let(:attributes) { [[:name]] }
12
+ let(:relation) { [{ name: 'Jane' }, { name: 'Joe' }] }
13
+
14
+ it 'returns tuples' do
15
+ expect(transproc[relation]).to eql(relation)
16
+ end
17
+ end
18
+
19
+ context 'coercing values' do
20
+ let(:attributes) { [[:name, type: :string], [:age, type: :integer]] }
21
+ let(:relation) { [{ name: :Jane, age: '1' }, { name: :Joe, age: '2' }] }
22
+
23
+ it 'returns tuples' do
24
+ expect(transproc[relation]).to eql([
25
+ { name: 'Jane', age: 1 }, { name: 'Joe', age: 2 }
26
+ ])
27
+ end
28
+ end
29
+
30
+ context 'mapping to object' do
31
+ let(:options) { { model: model } }
32
+
33
+ let(:model) do
34
+ Class.new do
35
+ include Virtus.value_object
36
+ values { attribute :name }
37
+ end
38
+ end
39
+
40
+ let(:attributes) { [[:name]] }
41
+ let(:relation) { [{ name: 'Jane' }, { name: 'Joe' }] }
42
+
43
+ it 'returns tuples' do
44
+ expect(transproc[relation]).to eql([
45
+ model.new(name: 'Jane'), model.new(name: 'Joe')
46
+ ])
47
+ end
48
+ end
49
+
50
+ context 'renaming keys' do
51
+ let(:attributes) do
52
+ [[:name, from: 'name']]
53
+ end
54
+
55
+ let(:options) do
56
+ { reject_keys: true }
57
+ end
58
+
59
+ let(:relation) do
60
+ [
61
+ { 'name' => 'Jane', 'age' => 21 }, { 'name' => 'Joe', age: 22 }
62
+ ]
63
+ end
64
+
65
+ it 'returns tuples with rejected keys' do
66
+ expect(transproc[relation]).to eql([{ name: 'Jane' }, { name: 'Joe' }])
67
+ end
68
+ end
69
+
70
+ describe 'rejecting keys' do
71
+ let(:options) { { reject_keys: true } }
72
+
73
+ let(:attributes) do
74
+ [
75
+ ['name'],
76
+ ['tasks', type: :array, group: true, header: [['title']]]
77
+ ]
78
+ end
79
+
80
+ let(:relation) do
81
+ [
82
+ { 'name' => 'Jane', 'age' => 21, 'title' => 'Task One' },
83
+ { 'name' => 'Jane', 'age' => 21, 'title' => 'Task Two' },
84
+ { 'name' => 'Joe', 'age' => 22, 'title' => 'Task One' }
85
+ ]
86
+ end
87
+
88
+ it 'returns tuples with unknown keys rejected' do
89
+ expect(transproc[relation]).to eql([
90
+ { 'name' => 'Jane',
91
+ 'tasks' => [{ 'title' => 'Task One' }, { 'title' => 'Task Two' }] },
92
+ { 'name' => 'Joe',
93
+ 'tasks' => [{ 'title' => 'Task One' }] }
94
+ ])
95
+ end
96
+ end
97
+
98
+ context 'mapping nested hash' do
99
+ let(:relation) do
100
+ [
101
+ { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } },
102
+ { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } }
103
+ ]
104
+ end
105
+
106
+ context 'when no mapping is needed' do
107
+ let(:attributes) { [['name'], ['task', type: :hash, header: [[:title]]]] }
108
+
109
+ it 'returns tuples' do
110
+ expect(transproc[relation]).to eql(relation)
111
+ end
112
+ end
113
+
114
+ context 'with deeply nested hashes' do
115
+ context 'when no renaming is required' do
116
+ let(:relation) do
117
+ [
118
+ { 'user' => { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } } },
119
+ { 'user' => { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } } }
120
+ ]
121
+ end
122
+
123
+ let(:attributes) do
124
+ [[
125
+ 'user', type: :hash, header: [
126
+ ['name'],
127
+ ['task', type: :hash, header: [['title']]]
128
+ ]
129
+ ]]
130
+ end
131
+
132
+ it 'returns tuples' do
133
+ expect(transproc[relation]).to eql(relation)
134
+ end
135
+ end
136
+
137
+ context 'when renaming is required' do
138
+ let(:relation) do
139
+ [
140
+ { user: { name: 'Jane', task: { title: 'Task One' } } },
141
+ { user: { name: 'Joe', task: { title: 'Task Two' } } }
142
+ ]
143
+ end
144
+
145
+ let(:attributes) do
146
+ [[
147
+ 'user', type: :hash, header: [
148
+ ['name'],
149
+ ['task', type: :hash, header: [['title']]]
150
+ ]
151
+ ]]
152
+ end
153
+
154
+ it 'returns tuples' do
155
+ expect(transproc[relation]).to eql(relation)
156
+ end
157
+ end
158
+ end
159
+
160
+ context 'renaming keys' do
161
+ context 'when only hash needs renaming' do
162
+ let(:attributes) do
163
+ [
164
+ ['name'],
165
+ [:task, from: 'task', type: :hash, header: [[:title, from: 'title']]]
166
+ ]
167
+ end
168
+
169
+ it 'returns tuples with key renamed in the nested hash' do
170
+ expect(transproc[relation]).to eql([
171
+ { 'name' => 'Jane', :task => { title: 'Task One' } },
172
+ { 'name' => 'Joe', :task => { title: 'Task Two' } }
173
+ ])
174
+ end
175
+ end
176
+
177
+ context 'when all attributes need renaming' do
178
+ let(:attributes) do
179
+ [
180
+ [:name, from: 'name'],
181
+ [:task, from: 'task', type: :hash, header: [[:title, from: 'title']]]
182
+ ]
183
+ end
184
+
185
+ it 'returns tuples with key renamed in the nested hash' do
186
+ expect(transproc[relation]).to eql([
187
+ { name: 'Jane', task: { title: 'Task One' } },
188
+ { name: 'Joe', task: { title: 'Task Two' } }
189
+ ])
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'wrapping tuples' do
196
+ let(:relation) do
197
+ [
198
+ { 'name' => 'Jane', 'title' => 'Task One' },
199
+ { 'name' => 'Joe', 'title' => 'Task Two' }
200
+ ]
201
+ end
202
+
203
+ context 'when no mapping is needed' do
204
+ let(:attributes) do
205
+ [
206
+ ['name'],
207
+ ['task', type: :hash, wrap: true, header: [['title']]]
208
+ ]
209
+ end
210
+
211
+ it 'returns wrapped tuples' do
212
+ expect(transproc[relation]).to eql([
213
+ { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } },
214
+ { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } }
215
+ ])
216
+ end
217
+ end
218
+
219
+ context 'with deeply wrapped tuples' do
220
+ let(:attributes) do
221
+ [
222
+ ['user', type: :hash, wrap: true, header: [
223
+ ['name'],
224
+ ['task', type: :hash, wrap: true, header: [['title']]]
225
+ ]]
226
+ ]
227
+ end
228
+
229
+ it 'returns wrapped tuples' do
230
+ expect(transproc[relation]).to eql([
231
+ { 'user' => { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } } },
232
+ { 'user' => { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } } }
233
+ ])
234
+ end
235
+ end
236
+
237
+ context 'renaming keys' do
238
+ context 'when only wrapped tuple requires renaming' do
239
+ let(:attributes) do
240
+ [
241
+ ['name'],
242
+ ['task', type: :hash, wrap: true, header: [[:title, from: 'title']]]
243
+ ]
244
+ end
245
+
246
+ it 'returns wrapped tuples with renamed keys' do
247
+ expect(transproc[relation]).to eql([
248
+ { 'name' => 'Jane', 'task' => { title: 'Task One' } },
249
+ { 'name' => 'Joe', 'task' => { title: 'Task Two' } }
250
+ ])
251
+ end
252
+ end
253
+
254
+ context 'when all attributes require renaming' do
255
+ let(:attributes) do
256
+ [
257
+ [:name, from: 'name'],
258
+ [:task, type: :hash, wrap: true, header: [[:title, from: 'title']]]
259
+ ]
260
+ end
261
+
262
+ it 'returns wrapped tuples with all keys renamed' do
263
+ expect(transproc[relation]).to eql([
264
+ { name: 'Jane', task: { title: 'Task One' } },
265
+ { name: 'Joe', task: { title: 'Task Two' } }
266
+ ])
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ context 'unwrapping tuples' do
273
+ let(:relation) do
274
+ [
275
+ { 'user' => { 'name' => 'Leo', 'task' => { 'title' => 'Task 1' } } },
276
+ { 'user' => { 'name' => 'Joe', 'task' => { 'title' => 'Task 2' } } }
277
+ ]
278
+ end
279
+
280
+ context 'when no mapping is needed' do
281
+ let(:attributes) do
282
+ [
283
+ ['user', type: :hash, unwrap: true, header: [['name'], ['task']]]
284
+ ]
285
+ end
286
+
287
+ it 'returns unwrapped tuples' do
288
+ expect(transproc[relation]).to eql([
289
+ { 'name' => 'Leo', 'task' => { 'title' => 'Task 1' } },
290
+ { 'name' => 'Joe', 'task' => { 'title' => 'Task 2' } }
291
+ ])
292
+ end
293
+ end
294
+
295
+ context 'partially' do
296
+ context 'without renaming the rest of the wrap' do
297
+ let(:attributes) do
298
+ [
299
+ ['user', type: :hash, unwrap: true, header: [['task']]]
300
+ ]
301
+ end
302
+
303
+ it 'returns unwrapped tuples' do
304
+ expect(transproc[relation]).to eql([
305
+ { 'user' => { 'name' => 'Leo' }, 'task' => { 'title' => 'Task 1' } },
306
+ { 'user' => { 'name' => 'Joe' }, 'task' => { 'title' => 'Task 2' } }
307
+ ])
308
+ end
309
+ end
310
+
311
+ context 'with renaming the rest of the wrap' do
312
+ let(:attributes) do
313
+ [
314
+ ['man', from: 'user', type: :hash, unwrap: true, header: [['task']]]
315
+ ]
316
+ end
317
+
318
+ it 'returns unwrapped tuples' do
319
+ expect(transproc[relation]).to eql([
320
+ { 'man' => { 'name' => 'Leo' }, 'task' => { 'title' => 'Task 1' } },
321
+ { 'man' => { 'name' => 'Joe' }, 'task' => { 'title' => 'Task 2' } }
322
+ ])
323
+ end
324
+ end
325
+ end
326
+
327
+ context 'deeply' do
328
+ let(:attributes) do
329
+ [
330
+ ['user', type: :hash, unwrap: true, header: [
331
+ ['name'],
332
+ ['title'],
333
+ ['task', type: :hash, unwrap: true, header: [['title']]]
334
+ ]]
335
+ ]
336
+ end
337
+
338
+ it 'returns unwrapped tuples' do
339
+ expect(transproc[relation]).to eql([
340
+ { 'name' => 'Leo', 'title' => 'Task 1' },
341
+ { 'name' => 'Joe', 'title' => 'Task 2' }
342
+ ])
343
+ end
344
+ end
345
+ end
346
+
347
+ context 'grouping tuples' do
348
+ let(:relation) do
349
+ [
350
+ { 'name' => 'Jane', 'title' => 'Task One' },
351
+ { 'name' => 'Jane', 'title' => 'Task Two' },
352
+ { 'name' => 'Joe', 'title' => 'Task One' },
353
+ { 'name' => 'Joe', 'title' => nil }
354
+ ]
355
+ end
356
+
357
+ context 'when no mapping is needed' do
358
+ let(:attributes) do
359
+ [
360
+ ['name'],
361
+ ['tasks', type: :array, group: true, header: [['title']]]
362
+ ]
363
+ end
364
+
365
+ it 'returns wrapped tuples with all keys renamed' do
366
+ expect(transproc[relation]).to eql([
367
+ { 'name' => 'Jane',
368
+ 'tasks' => [{ 'title' => 'Task One' }, { 'title' => 'Task Two' }] },
369
+ { 'name' => 'Joe',
370
+ 'tasks' => [{ 'title' => 'Task One' }] }
371
+ ])
372
+ end
373
+ end
374
+
375
+ context 'renaming keys' do
376
+ context 'when only grouped tuple requires renaming' do
377
+ let(:attributes) do
378
+ [
379
+ ['name'],
380
+ ['tasks', type: :array, group: true, header: [[:title, from: 'title']]]
381
+ ]
382
+ end
383
+
384
+ it 'returns grouped tuples with renamed keys' do
385
+ expect(transproc[relation]).to eql([
386
+ { 'name' => 'Jane',
387
+ 'tasks' => [{ title: 'Task One' }, { title: 'Task Two' }] },
388
+ { 'name' => 'Joe',
389
+ 'tasks' => [{ title: 'Task One' }] }
390
+ ])
391
+ end
392
+ end
393
+
394
+ context 'when all attributes require renaming' do
395
+ let(:attributes) do
396
+ [
397
+ [:name, from: 'name'],
398
+ [:tasks, type: :array, group: true, header: [[:title, from: 'title']]]
399
+ ]
400
+ end
401
+
402
+ it 'returns grouped tuples with all keys renamed' do
403
+ expect(transproc[relation]).to eql([
404
+ { name: 'Jane',
405
+ tasks: [{ title: 'Task One' }, { title: 'Task Two' }] },
406
+ { name: 'Joe',
407
+ tasks: [{ title: 'Task One' }] }
408
+ ])
409
+ end
410
+ end
411
+ end
412
+
413
+ context 'nested grouping' do
414
+ let(:relation) do
415
+ [
416
+ { name: 'Jane', title: 'Task One', tag: 'red' },
417
+ { name: 'Jane', title: 'Task One', tag: 'green' },
418
+ { name: 'Joe', title: 'Task One', tag: 'blue' }
419
+ ]
420
+ end
421
+
422
+ let(:attributes) do
423
+ [
424
+ [:name],
425
+ [:tasks, type: :array, group: true, header: [
426
+ [:title],
427
+ [:tags, type: :array, group: true, header: [[:tag]]]
428
+ ]]
429
+ ]
430
+ end
431
+
432
+ it 'returns deeply grouped tuples' do
433
+ expect(transproc[relation]).to eql([
434
+ { name: 'Jane',
435
+ tasks: [
436
+ { title: 'Task One', tags: [{ tag: 'red' }, { tag: 'green' }] }
437
+ ]
438
+ },
439
+ { name: 'Joe',
440
+ tasks: [
441
+ { title: 'Task One', tags: [{ tag: 'blue' }] }
442
+ ]
443
+ }
444
+ ])
445
+ end
446
+ end
447
+ end
448
+ end