rom 0.4.2 → 0.5.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +81 -0
  3. data/.travis.yml +2 -1
  4. data/CHANGELOG.md +41 -0
  5. data/Gemfile +12 -8
  6. data/Guardfile +17 -11
  7. data/README.md +7 -7
  8. data/Rakefile +29 -0
  9. data/lib/rom.rb +9 -66
  10. data/lib/rom/adapter.rb +45 -12
  11. data/lib/rom/adapter/memory.rb +0 -4
  12. data/lib/rom/adapter/memory/commands.rb +0 -10
  13. data/lib/rom/adapter/memory/dataset.rb +18 -6
  14. data/lib/rom/adapter/memory/storage.rb +0 -3
  15. data/lib/rom/command_registry.rb +24 -43
  16. data/lib/rom/commands.rb +5 -6
  17. data/lib/rom/commands/create.rb +5 -5
  18. data/lib/rom/commands/delete.rb +8 -6
  19. data/lib/rom/commands/result.rb +82 -0
  20. data/lib/rom/commands/update.rb +5 -4
  21. data/lib/rom/commands/with_options.rb +1 -4
  22. data/lib/rom/config.rb +70 -0
  23. data/lib/rom/env.rb +11 -3
  24. data/lib/rom/global.rb +107 -0
  25. data/lib/rom/header.rb +122 -89
  26. data/lib/rom/header/attribute.rb +148 -0
  27. data/lib/rom/mapper.rb +46 -67
  28. data/lib/rom/mapper_builder.rb +20 -73
  29. data/lib/rom/mapper_builder/mapper_dsl.rb +114 -0
  30. data/lib/rom/mapper_builder/model_dsl.rb +29 -0
  31. data/lib/rom/mapper_registry.rb +21 -0
  32. data/lib/rom/model_builder.rb +11 -17
  33. data/lib/rom/processor.rb +28 -0
  34. data/lib/rom/processor/transproc.rb +105 -0
  35. data/lib/rom/reader.rb +81 -21
  36. data/lib/rom/reader_builder.rb +14 -4
  37. data/lib/rom/relation.rb +19 -5
  38. data/lib/rom/relation_builder.rb +20 -6
  39. data/lib/rom/repository.rb +0 -2
  40. data/lib/rom/setup.rb +156 -0
  41. data/lib/rom/{boot → setup}/base_relation_dsl.rb +4 -8
  42. data/lib/rom/setup/command_dsl.rb +46 -0
  43. data/lib/rom/setup/finalize.rb +125 -0
  44. data/lib/rom/setup/mapper_dsl.rb +19 -0
  45. data/lib/rom/{boot → setup}/relation_dsl.rb +1 -4
  46. data/lib/rom/setup/schema_dsl.rb +33 -0
  47. data/lib/rom/support/registry.rb +10 -6
  48. data/lib/rom/version.rb +1 -1
  49. data/rom.gemspec +3 -1
  50. data/spec/integration/adapters/extending_relations_spec.rb +0 -2
  51. data/spec/integration/commands/create_spec.rb +2 -9
  52. data/spec/integration/commands/delete_spec.rb +4 -5
  53. data/spec/integration/commands/error_handling_spec.rb +4 -3
  54. data/spec/integration/commands/update_spec.rb +3 -8
  55. data/spec/integration/mappers/deep_embedded_spec.rb +52 -0
  56. data/spec/integration/mappers/definition_dsl_spec.rb +0 -118
  57. data/spec/integration/mappers/embedded_spec.rb +82 -0
  58. data/spec/integration/mappers/group_spec.rb +170 -0
  59. data/spec/integration/mappers/prefixing_attributes_spec.rb +2 -2
  60. data/spec/integration/mappers/renaming_attributes_spec.rb +8 -6
  61. data/spec/integration/mappers/symbolizing_attributes_spec.rb +80 -0
  62. data/spec/integration/mappers/wrap_spec.rb +162 -0
  63. data/spec/integration/multi_repo_spec.rb +64 -0
  64. data/spec/integration/relations/reading_spec.rb +12 -8
  65. data/spec/integration/relations/registry_dsl_spec.rb +1 -3
  66. data/spec/integration/schema_spec.rb +10 -0
  67. data/spec/integration/setup_spec.rb +57 -6
  68. data/spec/spec_helper.rb +2 -1
  69. data/spec/unit/config_spec.rb +60 -0
  70. data/spec/unit/rom/adapter/memory/dataset_spec.rb +52 -0
  71. data/spec/unit/rom/adapter_spec.rb +31 -11
  72. data/spec/unit/rom/header_spec.rb +60 -16
  73. data/spec/unit/rom/mapper_builder_spec.rb +311 -0
  74. data/spec/unit/rom/mapper_registry_spec.rb +25 -0
  75. data/spec/unit/rom/mapper_spec.rb +4 -5
  76. data/spec/unit/rom/model_builder_spec.rb +15 -13
  77. data/spec/unit/rom/processor/transproc_spec.rb +331 -0
  78. data/spec/unit/rom/reader_spec.rb +73 -0
  79. data/spec/unit/rom/registry_spec.rb +38 -0
  80. data/spec/unit/rom/relation_spec.rb +0 -1
  81. data/spec/unit/rom/setup_spec.rb +55 -0
  82. data/spec/unit/rom_spec.rb +14 -0
  83. metadata +62 -22
  84. data/Gemfile.devtools +0 -71
  85. data/lib/rom/boot.rb +0 -197
  86. data/lib/rom/boot/command_dsl.rb +0 -48
  87. data/lib/rom/boot/dsl.rb +0 -37
  88. data/lib/rom/boot/mapper_dsl.rb +0 -23
  89. data/lib/rom/boot/schema_dsl.rb +0 -27
  90. data/lib/rom/ra.rb +0 -172
  91. data/lib/rom/ra/operation/group.rb +0 -47
  92. data/lib/rom/ra/operation/join.rb +0 -39
  93. data/lib/rom/ra/operation/wrap.rb +0 -45
  94. data/lib/rom/transformer.rb +0 -77
  95. data/spec/integration/ra/group_spec.rb +0 -46
  96. data/spec/integration/ra/join_spec.rb +0 -50
  97. data/spec/integration/ra/wrap_spec.rb +0 -37
  98. data/spec/unit/rom/ra/operation/group_spec.rb +0 -55
  99. data/spec/unit/rom/ra/operation/wrap_spec.rb +0 -29
  100. data/spec/unit/rom/transformer_spec.rb +0 -41
@@ -4,7 +4,7 @@ require 'ostruct'
4
4
 
5
5
  describe ROM::Mapper do
6
6
  subject(:mapper) do
7
- ROM::Mapper.build(ROM::Header.coerce(relation.header.zip), user_model)
7
+ ROM::Mapper.build(ROM::Header.coerce(relation.header.zip, user_model))
8
8
  end
9
9
 
10
10
  let(:relation) do
@@ -13,7 +13,7 @@ describe ROM::Mapper do
13
13
 
14
14
  let(:dataset) do
15
15
  ROM::Adapter::Memory::Dataset.new(
16
- [{id: 1, name: 'Jane'}, {id: 2, name: 'Joe'}], [:id, :name]
16
+ [{ id: 1, name: 'Jane' }, { id: 2, name: 'Joe' }], [:id, :name]
17
17
  )
18
18
  end
19
19
 
@@ -28,12 +28,11 @@ describe ROM::Mapper do
28
28
  it "yields all mapped objects" do
29
29
  result = []
30
30
 
31
- relation.each do |tuple|
32
- result << mapper.load(tuple)
31
+ mapper.process(relation).each do |tuple|
32
+ result << tuple
33
33
  end
34
34
 
35
35
  expect(result).to eql([jane, joe])
36
36
  end
37
37
  end
38
-
39
38
  end
@@ -5,7 +5,7 @@ describe ROM::ModelBuilder do
5
5
  it 'builds a class with a constructor accepting attributes' do
6
6
  builder = ROM::ModelBuilder::PORO.new
7
7
 
8
- klass = builder.call(name: :user_name)
8
+ klass = builder.call([:name])
9
9
 
10
10
  object = klass.new(name: 'Jane')
11
11
 
@@ -13,7 +13,7 @@ describe ROM::ModelBuilder do
13
13
 
14
14
  expect { object.name = 'Jane' }.to raise_error(NoMethodError)
15
15
 
16
- klass = builder.call(name: :user_name, email: :user_email)
16
+ klass = builder.call([:name, :email])
17
17
 
18
18
  object = klass.new(name: 'Jane', email: 'jane@doe.org')
19
19
 
@@ -21,23 +21,25 @@ describe ROM::ModelBuilder do
21
21
  expect(object.email).to eql('jane@doe.org')
22
22
  end
23
23
 
24
- it 'defines a constant for the model when :name option is present' do
25
- builder = ROM::ModelBuilder::PORO.new(name: 'User')
24
+ context 'when :name option is present' do
25
+ it 'defines a constant for the model' do
26
+ builder = ROM::ModelBuilder::PORO.new(name: 'User')
26
27
 
27
- builder.call(name: :user_name, email: :user_email)
28
+ builder.call([:name, :email])
28
29
 
29
- expect(Object.const_defined?(:User)).to be(true)
30
- end
30
+ expect(Object.const_defined?(:User)).to be(true)
31
+ end
31
32
 
32
- it 'defines a constant within a namespace for the model when :name option is present' do
33
- module MyApp; module Entities; end; end
33
+ it 'defines a constant within a namespace for the model' do
34
+ module MyApp; module Entities; end; end
34
35
 
35
- builder = ROM::ModelBuilder::PORO.new(name: 'MyApp::Entities::User')
36
+ builder = ROM::ModelBuilder::PORO.new(name: 'MyApp::Entities::User')
36
37
 
37
- builder.call(name: :user_name, email: :user_email)
38
+ builder.call([:name, :email])
38
39
 
39
- expect(MyApp::Entities.const_defined?(:User)).to be(true)
40
- expect(Object.const_defined?(:User)).to be(false)
40
+ expect(MyApp::Entities.const_defined?(:User)).to be(true)
41
+ expect(Object.const_defined?(:User)).to be(false)
42
+ end
41
43
  end
42
44
  end
43
45
  end
@@ -0,0 +1,331 @@
1
+ require 'spec_helper'
2
+
3
+ describe ROM::Processor::Transproc do
4
+ subject(:transproc) { ROM::Processor::Transproc.build(header) }
5
+
6
+ let(:header) { ROM::Header.coerce(attributes) }
7
+
8
+ context 'no mapping' do
9
+ let(:attributes) { [[:name]] }
10
+ let(:relation) { [{ name: 'Jane' }, { name: 'Joe' }] }
11
+
12
+ it 'returns tuples' do
13
+ expect(transproc[relation]).to eql(relation)
14
+ end
15
+ end
16
+
17
+ context 'coercing values' do
18
+ let(:attributes) { [[:name, type: :string], [:age, type: :integer]] }
19
+ let(:relation) { [{ name: :Jane, age: '1' }, { name: :Joe, age: '2' }] }
20
+
21
+ it 'returns tuples' do
22
+ expect(transproc[relation]).to eql([
23
+ { name: 'Jane', age: 1 }, { name: 'Joe', age: 2 }
24
+ ])
25
+ end
26
+ end
27
+
28
+ context 'mapping to object' do
29
+ let(:header) { ROM::Header.coerce(attributes, model) }
30
+
31
+ let(:model) do
32
+ Class.new do
33
+ include Virtus.value_object
34
+ values { attribute :name }
35
+ end
36
+ end
37
+
38
+ let(:attributes) { [[:name]] }
39
+ let(:relation) { [{ name: 'Jane' }, { name: 'Joe' }] }
40
+
41
+ it 'returns tuples' do
42
+ expect(transproc[relation]).to eql([
43
+ model.new(name: 'Jane'), model.new(name: 'Joe')
44
+ ])
45
+ end
46
+ end
47
+
48
+ context 'renaming keys' do
49
+ let(:attributes) { [[:name, from: 'name']] }
50
+ let(:relation) { [{ 'name' => 'Jane' }, { 'name' => 'Joe' }] }
51
+
52
+ it 'returns tuples with renamed keys' do
53
+ expect(transproc[relation]).to eql([{ name: 'Jane' }, { name: 'Joe' }])
54
+ end
55
+ end
56
+
57
+ context 'mapping nested hash' do
58
+ let(:relation) do
59
+ [
60
+ { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } },
61
+ { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } }
62
+ ]
63
+ end
64
+
65
+ context 'when no mapping is needed' do
66
+ let(:attributes) { [['name'], ['task', type: :hash, header: [[:title]]]] }
67
+
68
+ it 'returns tuples' do
69
+ expect(transproc[relation]).to eql(relation)
70
+ end
71
+ end
72
+
73
+ context 'with deeply nested hashes' do
74
+ context 'when no renaming is required' do
75
+ let(:relation) do
76
+ [
77
+ { 'user' => { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } } },
78
+ { 'user' => { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } } }
79
+ ]
80
+ end
81
+
82
+ let(:attributes) do
83
+ [[
84
+ 'user', type: :hash, header: [
85
+ ['name'],
86
+ ['task', type: :hash, header: [['title']]]
87
+ ]
88
+ ]]
89
+ end
90
+
91
+ it 'returns tuples' do
92
+ expect(transproc[relation]).to eql(relation)
93
+ end
94
+ end
95
+
96
+ context 'when renaming is required' do
97
+ let(:relation) do
98
+ [
99
+ { user: { name: 'Jane', task: { title: 'Task One' } } },
100
+ { user: { name: 'Joe', task: { title: 'Task Two' } } }
101
+ ]
102
+ end
103
+
104
+ let(:attributes) do
105
+ [[
106
+ 'user', type: :hash, header: [
107
+ ['name'],
108
+ ['task', type: :hash, header: [['title']]]
109
+ ]
110
+ ]]
111
+ end
112
+
113
+ it 'returns tuples' do
114
+ expect(transproc[relation]).to eql(relation)
115
+ end
116
+ end
117
+ end
118
+
119
+ context 'renaming keys' do
120
+ context 'when only hash needs renaming' do
121
+ let(:attributes) do
122
+ [
123
+ ['name'],
124
+ [:task, from: 'task', type: :hash, header: [[:title, from: 'title']]]
125
+ ]
126
+ end
127
+
128
+ it 'returns tuples with key renamed in the nested hash' do
129
+ expect(transproc[relation]).to eql([
130
+ { 'name' => 'Jane', :task => { title: 'Task One' } },
131
+ { 'name' => 'Joe', :task => { title: 'Task Two' } }
132
+ ])
133
+ end
134
+ end
135
+
136
+ context 'when all attributes need renaming' do
137
+ let(:attributes) do
138
+ [
139
+ [:name, from: 'name'],
140
+ [:task, from: 'task', type: :hash, header: [[:title, from: 'title']]]
141
+ ]
142
+ end
143
+
144
+ it 'returns tuples with key renamed in the nested hash' do
145
+ expect(transproc[relation]).to eql([
146
+ { name: 'Jane', task: { title: 'Task One' } },
147
+ { name: 'Joe', task: { title: 'Task Two' } }
148
+ ])
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'wrapping tuples' do
155
+ let(:relation) do
156
+ [
157
+ { 'name' => 'Jane', 'title' => 'Task One' },
158
+ { 'name' => 'Joe', 'title' => 'Task Two' }
159
+ ]
160
+ end
161
+
162
+ context 'when no mapping is needed' do
163
+ let(:attributes) do
164
+ [
165
+ ['name'],
166
+ ['task', type: :hash, wrap: true, header: [['title']]]
167
+ ]
168
+ end
169
+
170
+ it 'returns wrapped tuples' do
171
+ expect(transproc[relation]).to eql([
172
+ { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } },
173
+ { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } }
174
+ ])
175
+ end
176
+ end
177
+
178
+ context 'with deeply wrapped tuples' do
179
+ let(:attributes) do
180
+ [
181
+ ['user', type: :hash, wrap: true, header: [
182
+ ['name'],
183
+ ['task', type: :hash, wrap: true, header: [['title']]]
184
+ ]]
185
+ ]
186
+ end
187
+
188
+ it 'returns wrapped tuples' do
189
+ expect(transproc[relation]).to eql([
190
+ { 'user' => { 'name' => 'Jane', 'task' => { 'title' => 'Task One' } } },
191
+ { 'user' => { 'name' => 'Joe', 'task' => { 'title' => 'Task Two' } } }
192
+ ])
193
+ end
194
+ end
195
+
196
+ context 'renaming keys' do
197
+ context 'when only wrapped tuple requires renaming' do
198
+ let(:attributes) do
199
+ [
200
+ ['name'],
201
+ ['task', type: :hash, wrap: true, header: [[:title, from: 'title']]]
202
+ ]
203
+ end
204
+
205
+ it 'returns wrapped tuples with renamed keys' do
206
+ expect(transproc[relation]).to eql([
207
+ { 'name' => 'Jane', 'task' => { title: 'Task One' } },
208
+ { 'name' => 'Joe', 'task' => { title: 'Task Two' } }
209
+ ])
210
+ end
211
+ end
212
+
213
+ context 'when all attributes require renaming' do
214
+ let(:attributes) do
215
+ [
216
+ [:name, from: 'name'],
217
+ [:task, type: :hash, wrap: true, header: [[:title, from: 'title']]]
218
+ ]
219
+ end
220
+
221
+ it 'returns wrapped tuples with all keys renamed' do
222
+ expect(transproc[relation]).to eql([
223
+ { name: 'Jane', task: { title: 'Task One' } },
224
+ { name: 'Joe', task: { title: 'Task Two' } }
225
+ ])
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ context 'grouping tuples' do
232
+ let(:relation) do
233
+ [
234
+ { 'name' => 'Jane', 'title' => 'Task One' },
235
+ { 'name' => 'Jane', 'title' => 'Task Two' },
236
+ { 'name' => 'Joe', 'title' => 'Task One' }
237
+ ]
238
+ end
239
+
240
+ context 'when no mapping is needed' do
241
+ let(:attributes) do
242
+ [
243
+ ['name'],
244
+ ['tasks', type: :array, group: true, header: [['title']]]
245
+ ]
246
+ end
247
+
248
+ it 'returns wrapped tuples with all keys renamed' do
249
+ expect(transproc[relation]).to eql([
250
+ { 'name' => 'Jane',
251
+ 'tasks' => [{ 'title' => 'Task One' }, { 'title' => 'Task Two' }] },
252
+ { 'name' => 'Joe',
253
+ 'tasks' => [{ 'title' => 'Task One' }] }
254
+ ])
255
+ end
256
+ end
257
+
258
+ context 'renaming keys' do
259
+ context 'when only grouped tuple requires renaming' do
260
+ let(:attributes) do
261
+ [
262
+ ['name'],
263
+ ['tasks', type: :array, group: true, header: [[:title, from: 'title']]]
264
+ ]
265
+ end
266
+
267
+ it 'returns grouped tuples with renamed keys' do
268
+ expect(transproc[relation]).to eql([
269
+ { 'name' => 'Jane',
270
+ 'tasks' => [{ title: 'Task One' }, { title: 'Task Two' }] },
271
+ { 'name' => 'Joe',
272
+ 'tasks' => [{ title: 'Task One' }] }
273
+ ])
274
+ end
275
+ end
276
+
277
+ context 'when all attributes require renaming' do
278
+ let(:attributes) do
279
+ [
280
+ [:name, from: 'name'],
281
+ [:tasks, type: :array, group: true, header: [[:title, from: 'title']]]
282
+ ]
283
+ end
284
+
285
+ it 'returns grouped tuples with all keys renamed' do
286
+ expect(transproc[relation]).to eql([
287
+ { name: 'Jane',
288
+ tasks: [{ title: 'Task One' }, { title: 'Task Two' }] },
289
+ { name: 'Joe',
290
+ tasks: [{ title: 'Task One' }] }
291
+ ])
292
+ end
293
+ end
294
+ end
295
+
296
+ context 'nested grouping' do
297
+ let(:relation) do
298
+ [
299
+ { name: 'Jane', title: 'Task One', tag: 'red' },
300
+ { name: 'Jane', title: 'Task One', tag: 'green' },
301
+ { name: 'Joe', title: 'Task One', tag: 'blue' }
302
+ ]
303
+ end
304
+
305
+ let(:attributes) do
306
+ [
307
+ [:name],
308
+ [:tasks, type: :array, group: true, header: [
309
+ [:title],
310
+ [:tags, type: :array, group: true, header: [[:tag]]]
311
+ ]]
312
+ ]
313
+ end
314
+
315
+ it 'returns deeply grouped tuples' do
316
+ expect(transproc[relation]).to eql([
317
+ { name: 'Jane',
318
+ tasks: [
319
+ { title: 'Task One', tags: [{ tag: 'red' }, { tag: 'green' }] }
320
+ ]
321
+ },
322
+ { name: 'Joe',
323
+ tasks: [
324
+ { title: 'Task One', tags: [{ tag: 'blue' }] }
325
+ ]
326
+ }
327
+ ])
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe ROM::Reader do
4
+ subject(:reader) { ROM::Reader.new(name, relation, mappers) }
5
+
6
+ let(:name) { :users }
7
+ let(:relation) { [jane, joe] }
8
+ let(:jane) { { name: 'Jane' } }
9
+ let(:joe) { { name: 'Joe' } }
10
+ let(:mappers) { ROM::MapperRegistry.new(users: mapper) }
11
+ let(:mapper) { double('mapper', header: []) }
12
+
13
+ describe '#initialize' do
14
+ it 'raises error when mapper cannot be found' do
15
+ expect { ROM::Reader.new(:not_here, relation, mappers) }
16
+ .to raise_error(ROM::Reader::MapperMissingError, /not_here/)
17
+ end
18
+ end
19
+
20
+ describe '#each' do
21
+ it 'yields mapped tuples from relations' do
22
+ expect(mapper).to receive(:process)
23
+ .with(relation)
24
+ .and_yield(jane).and_yield(joe)
25
+
26
+ result = []
27
+ reader.each { |user| result << user }
28
+ expect(result).to eql([jane, joe])
29
+ end
30
+ end
31
+
32
+ describe '.build' do
33
+ subject(:reader) { ROM::Reader.build(name, relation, mappers, [:all]) }
34
+
35
+ before do
36
+ relation.instance_exec do
37
+ def name
38
+ 'users'
39
+ end
40
+
41
+ def all(*_args)
42
+ find_all
43
+ end
44
+ end
45
+ end
46
+
47
+ it 'sets reader class name' do
48
+ expect(reader.class.name).to eql("ROM::Reader[Users]")
49
+ end
50
+
51
+ it 'defines methods from relation' do
52
+ block = proc {}
53
+
54
+ expect(relation).to receive(:all)
55
+ .with(1, &block)
56
+ .and_return([joe])
57
+
58
+ expect(mapper).to receive(:process)
59
+ .with([joe])
60
+ .and_yield(joe)
61
+
62
+ result = reader.all(1, &block)
63
+
64
+ expect(result.path).to eql('users.all')
65
+ expect(result.to_a).to eql([joe])
66
+ end
67
+
68
+ it 'raises error when relation does not respond to the method' do
69
+ expect { reader.not_here }
70
+ .to raise_error(ROM::NoRelationError, /not_here/)
71
+ end
72
+ end
73
+ end