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