praxis-mapper 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +83 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +102 -0
  8. data/Guardfile +11 -0
  9. data/LICENSE +22 -0
  10. data/README.md +19 -0
  11. data/Rakefile +14 -0
  12. data/lib/praxis-mapper/config_hash.rb +40 -0
  13. data/lib/praxis-mapper/connection_manager.rb +102 -0
  14. data/lib/praxis-mapper/finalizable.rb +38 -0
  15. data/lib/praxis-mapper/identity_map.rb +532 -0
  16. data/lib/praxis-mapper/logging.rb +22 -0
  17. data/lib/praxis-mapper/model.rb +430 -0
  18. data/lib/praxis-mapper/query/base.rb +213 -0
  19. data/lib/praxis-mapper/query/sql.rb +183 -0
  20. data/lib/praxis-mapper/query_statistics.rb +46 -0
  21. data/lib/praxis-mapper/resource.rb +226 -0
  22. data/lib/praxis-mapper/support/factory_girl.rb +104 -0
  23. data/lib/praxis-mapper/support/memory_query.rb +34 -0
  24. data/lib/praxis-mapper/support/memory_repository.rb +44 -0
  25. data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
  26. data/lib/praxis-mapper/support/schema_loader.rb +56 -0
  27. data/lib/praxis-mapper/support.rb +2 -0
  28. data/lib/praxis-mapper/version.rb +5 -0
  29. data/lib/praxis-mapper.rb +60 -0
  30. data/praxis-mapper.gemspec +38 -0
  31. data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
  32. data/spec/praxis-mapper/identity_map_spec.rb +905 -0
  33. data/spec/praxis-mapper/logging_spec.rb +9 -0
  34. data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
  35. data/spec/praxis-mapper/model_spec.rb +389 -0
  36. data/spec/praxis-mapper/query/base_spec.rb +317 -0
  37. data/spec/praxis-mapper/query/sql_spec.rb +184 -0
  38. data/spec/praxis-mapper/resource_spec.rb +154 -0
  39. data/spec/praxis_mapper_spec.rb +21 -0
  40. data/spec/spec_fixtures.rb +12 -0
  41. data/spec/spec_helper.rb +63 -0
  42. data/spec/support/spec_models.rb +215 -0
  43. data/spec/support/spec_resources.rb +39 -0
  44. metadata +298 -0
@@ -0,0 +1,9 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe Praxis::Mapper do
4
+
5
+ it 'has a default logger' do
6
+ Praxis::Mapper.logger.should be_kind_of(Praxis::Mapper::NullLogger)
7
+ end
8
+
9
+ end
@@ -0,0 +1,56 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Praxis::Mapper::Support::MemoryRepository do
4
+
5
+ subject(:repository) { Praxis::Mapper::Support::MemoryRepository.new }
6
+
7
+ let(:simple_rows) {[
8
+ {:id => 1, :name => "george jr", :parent_id => 1, :description => "one"},
9
+ {:id => 2, :name => "george iii", :parent_id => 2, :description => "two"},
10
+ {:id => 3, :name => "george xvi", :parent_id => 2, :description => "three"}
11
+
12
+ ]}
13
+
14
+ let(:person_rows) {[
15
+ {id: 1, email: "one@example.com", address_id: 1},
16
+ {id: 2, email: "two@example.com", address_id: 2},
17
+ {id: 3, email: "three@example.com", address_id: 2}
18
+
19
+ ]}
20
+
21
+ before do
22
+ repository.insert(:simple_model, simple_rows)
23
+ repository.insert(:people, person_rows)
24
+ end
25
+
26
+ context 'insert' do
27
+
28
+ it 'adds the records to the right repository collection' do
29
+ simple_rows.each do |simple_row|
30
+ repository.collection(:simple_model).should include(simple_row)
31
+ end
32
+ end
33
+
34
+ context 'with a Model class' do
35
+ let(:row) { {id: 4, name: "bob", parent_id: 5, description: "four"} }
36
+ it 'adds the records to Model.table_name collection' do
37
+ repository.insert(SimpleModel, [row])
38
+ repository.all(:simple_model, id: 4).should =~ [row]
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+
45
+ context 'all' do
46
+
47
+ it 'retrieves all matching records' do
48
+ repository.all(:simple_model, parent_id: 2).should =~ simple_rows[1..2]
49
+ repository.all(SimpleModel, parent_id: 2).should =~ simple_rows[1..2]
50
+
51
+ repository.all(:simple_model, id: 1, parent_id: 2).should be_empty
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,389 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+
4
+ describe Praxis::Mapper::Model do
5
+ subject(:model) { SimpleModel }
6
+
7
+
8
+ its(:excluded_scopes) { should =~ [:account] }
9
+ its(:repository_name) { should == :default }
10
+ its(:identities) { should =~ [:id, :name] }
11
+ its(:table_name) { should == "simple_model" }
12
+ its(:associations) { should have(2).items }
13
+
14
+ let(:id) { /\d{10}/.gen.to_i }
15
+ let(:name) { /\w+/.gen }
16
+ let(:parent_id) { /\d{10}/.gen.to_i }
17
+ let(:data) do
18
+ { :id => id,
19
+ :name => name,
20
+ :parent_id => parent_id
21
+ }
22
+ end
23
+
24
+ let(:person_rows) {[
25
+ {id: 1, email: "one@example.com", address_id: 1, prior_address_ids: JSON.dump([2])},
26
+ {id: 2, email: "two@example.com", address_id: 2, prior_address_ids: JSON.dump([])},
27
+ {id: 3, email: "three@example.com", address_id: 2, prior_address_ids: JSON.dump([])}
28
+ ]}
29
+
30
+ let(:person_records) { person_rows.collect { |r| PersonModel.new(r) } }
31
+
32
+ let(:address_rows) { [{id: 1, owner_id: 1},{id: 2, owner_id: 3}] }
33
+ let(:address_records) { address_rows.collect { |r| AddressModel.new(r) } }
34
+
35
+ let(:composite_id_rows) { [
36
+ {id:1, type:"foo", state:"running"},
37
+ {id:2, type:"bar", state:"terminated"}
38
+ ]}
39
+
40
+ let(:composite_id_records) { composite_id_rows.collect {|r| CompositeIdModel.new(r) } }
41
+
42
+ let(:composite_array_rows) { [
43
+ {id:1, type: 'CompositeArrayModel', composite_array_keys: JSON.dump([[1, "foo"], [2,"bar"]])},
44
+ {id:2, type: 'CompositeArrayModel', composite_array_keys: JSON.dump([[1, "foo"]])}
45
+ ]}
46
+
47
+ let(:composite_array_records) { composite_array_rows.collect {|r| CompositeArrayModel.new(r) } }
48
+
49
+
50
+ let(:identity_map) { Praxis::Mapper::IdentityMap.current }
51
+
52
+
53
+ context "new-style associations" do
54
+ subject(:person_model) { PersonModel }
55
+ its(:associations) { should have(3).items }
56
+
57
+ its(:associations) { should include(:properties) }
58
+ its(:associations) { should include(:address) }
59
+ its(:associations) { should include(:prior_addresses) }
60
+
61
+ it 'finalizes properly' do
62
+ person_model.associations[:properties].should == {
63
+ model: AddressModel,
64
+ key: :owner_id,
65
+ primary_key: :id,
66
+ type: :one_to_many
67
+ }
68
+ end
69
+
70
+ end
71
+
72
+ context "record finders" do
73
+
74
+ it 'have .get' do
75
+ identity_map.should_receive(:get).with(SimpleModel, :id => id)
76
+ SimpleModel.get(:id => id)
77
+ end
78
+
79
+ it 'have .all' do
80
+ identity_map.should_receive(:all).with(SimpleModel, :id => id)
81
+ SimpleModel.all(:id => id)
82
+ end
83
+
84
+ end
85
+
86
+ context "with a record" do
87
+
88
+ subject { SimpleModel.new(data) }
89
+
90
+ its(:id) { should == id }
91
+ its(:name) { should == name }
92
+ its(:parent_id) { should == parent_id }
93
+ its(:_resource) { should be_nil }
94
+
95
+ before do
96
+ identity_map.add_records([subject])
97
+ end
98
+
99
+
100
+ context 'creating accessors' do
101
+
102
+ context 'for identities and attributes' do
103
+ let(:model) do
104
+ Class.new(Praxis::Mapper::Model) do
105
+ identity :id
106
+ end
107
+ end
108
+ before do
109
+ model.finalize!
110
+ end
111
+ subject { model.new(data) }
112
+
113
+ it 'eagerly defines accessors for identities'do
114
+
115
+ subject.methods.should include(:id)
116
+ end
117
+
118
+ it 'lazily defines accessors for other attributes' do
119
+ subject.methods.should_not include(:name)
120
+ subject.name
121
+ subject.methods.should include(:name)
122
+ end
123
+ end
124
+
125
+ context 'for associations' do
126
+ context 'that are many_to_one' do
127
+
128
+ it { should respond_to(:parent) }
129
+
130
+ it 'retrieves related records' do
131
+ parent_record = double("parent_record")
132
+
133
+ identity_map.should_receive(:get).with(ParentModel, :id => parent_id).and_return(parent_record)
134
+ subject.parent.should == parent_record
135
+ end
136
+
137
+ context 'where the source_key value is nil' do
138
+ subject { SimpleModel.new(data.merge(:parent_id=>nil)) }
139
+ it 'returns nil' do
140
+ Praxis::Mapper::IdentityMap.current.should_not_receive(:get)
141
+ subject.parent.should be_nil
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ context 'that involve a serialized array' do
148
+
149
+ before do
150
+ identity_map.add_records(person_records)
151
+ identity_map.add_records(address_records)
152
+ end
153
+
154
+ context 'array_to_many' do
155
+ subject(:person_record) { person_records[0] }
156
+ its(:prior_addresses) { should =~ address_records[1..1] }
157
+ end
158
+
159
+ context 'many_to_array' do
160
+ subject(:address_record) { address_records[1] }
161
+ its(:prior_residents) { should =~ person_records[0..0] }
162
+ end
163
+
164
+ end
165
+
166
+ context 'that involve a serialized array with composite keys' do
167
+
168
+ before do
169
+ identity_map.add_records(composite_id_records)
170
+ identity_map.add_records(composite_array_records)
171
+ end
172
+
173
+ context 'array_to_many' do
174
+ subject(:composite_array_record) { composite_array_records[0] }
175
+ its(:composite_id_models) { should =~ composite_id_records }
176
+ end
177
+
178
+ context 'many_to_array' do
179
+ subject(:composite_id_record) { composite_id_records[0] }
180
+ its(:composite_array_models) { should =~ composite_array_records }
181
+ end
182
+ end
183
+
184
+ context 'that are one_to_many' do
185
+ subject(:address_record) { address_records.first }
186
+
187
+ before do
188
+ identity_map.add_records(person_records)
189
+ identity_map.add_records(address_records)
190
+ end
191
+
192
+ it { should respond_to(:owner) }
193
+ it { should respond_to(:residents) }
194
+
195
+ its(:owner) { should be(person_records[0]) }
196
+ its(:residents) { should =~ person_records[0..0] }
197
+
198
+ context 'for a composite key' do
199
+
200
+ let(:other_records) {[
201
+ OtherModel.new(id:10, composite_id:1, composite_type:"foo", name:"something"),
202
+ OtherModel.new(id:11, composite_id:1, composite_type:"foo", name:"nothing"),
203
+ OtherModel.new(id:12, composite_id:1, composite_type:"bar", name:"nothing")
204
+ ]}
205
+
206
+ subject(:composite_record) { composite_id_records.first }
207
+
208
+ before do
209
+ identity_map.add_records(composite_id_records)
210
+ identity_map.add_records(other_records)
211
+ end
212
+
213
+ its(:other_models) { should =~ other_records[0..1] }
214
+
215
+ end
216
+ end
217
+
218
+
219
+
220
+
221
+ end
222
+
223
+
224
+ end
225
+
226
+ it 'does respond_to attributes in the underlying record' do
227
+ subject.should respond_to(:id)
228
+ subject.should respond_to(:name)
229
+ subject.should respond_to(:parent_id)
230
+ end
231
+
232
+ it 'does not respond_to attributes not in the underlying record' do
233
+ subject.should_not respond_to(:foo)
234
+ end
235
+
236
+ it 'raises NoMethodError for undefined attributes' do
237
+ expect { subject.foo }.to raise_error(NoMethodError)
238
+ end
239
+
240
+ end
241
+
242
+ context 'supports composite identities' do
243
+ subject { CompositeIdModel }
244
+ its(:identities) { should =~ [[:id, :type]] }
245
+ end
246
+
247
+ #TODO: Refactor these cases...yaml and json serialization are exactly the same test...except for using YAML or JSON class
248
+ context 'serialized attributes' do
249
+ context 'yaml attributes' do
250
+ let(:model) { YamlArrayModel }
251
+ let(:names) { ["george jr", "george iii"] }
252
+ let(:parent_ids) { [1,2] }
253
+
254
+ let(:record) {
255
+ model.new(:id => 1,
256
+ :parent_ids => YAML.dump(parent_ids),
257
+ :names => YAML.dump(names)
258
+ )
259
+ }
260
+
261
+
262
+ it 'de-serializes in the accessor' do
263
+ record.should respond_to(:names)
264
+ record.names.should =~ names
265
+ end
266
+
267
+ it 'memoizes the de-serialization in the accessor' do
268
+ YAML.should_receive(:load).with(YAML.dump(names)).once.and_call_original
269
+ 2.times { record.names }
270
+ end
271
+
272
+ it 'defines an accessor for the raw yaml string' do
273
+ record.should respond_to(:_raw_names)
274
+ record._raw_names.should == YAML.dump(names)
275
+ end
276
+
277
+ context "with a nil value" do
278
+ let(:record) {
279
+ model.new(:id => 1,
280
+ :parent_ids => nil,
281
+ :names => nil
282
+ )
283
+ }
284
+
285
+ it 'returns nil if no default value was specified' do
286
+ record.names.should be_nil
287
+ end
288
+
289
+ it 'returns the default value if specified' do
290
+ record.parent_ids.should == []
291
+ end
292
+
293
+ end
294
+
295
+ end
296
+
297
+ context 'json attributes' do
298
+ let(:model) { JsonArrayModel }
299
+ let(:names) { ["george jr", "george iii"] }
300
+ let(:parent_ids) { [1,2] }
301
+
302
+ let(:record) {
303
+ model.new(:id => 1,
304
+ :parent_ids => JSON.dump(parent_ids),
305
+ :names => JSON.dump(names)
306
+ )
307
+ }
308
+
309
+
310
+ it 'de-serializes in the accessor' do
311
+ record.should respond_to(:names)
312
+ record.names.should =~ names
313
+ end
314
+
315
+ it 'memoizes the de-serialization in the accessor' do
316
+ JSON.should_receive(:load).with(JSON.dump(names)).once.and_call_original
317
+ 2.times { record.names }
318
+ end
319
+
320
+ it 'defines an accessor for the raw yaml string' do
321
+ record.should respond_to(:_raw_names)
322
+ record._raw_names.should == JSON.dump(names)
323
+ end
324
+
325
+ context "with a nil value" do
326
+ let(:record) {
327
+ model.new(:id => 1,
328
+ :parent_ids => nil,
329
+ :names => nil
330
+ )
331
+ }
332
+
333
+ it 'returns nil if no default value was specified' do
334
+ record.names.should be_nil
335
+ end
336
+
337
+ it 'returns the default value if specified' do
338
+ record.parent_ids.should == []
339
+ end
340
+
341
+ end
342
+
343
+ end
344
+ end
345
+
346
+ context '.context' do
347
+ let(:model_class) { PersonModel }
348
+
349
+ it 'works' do
350
+ PersonModel.contexts[:default][:select].should be_empty
351
+ PersonModel.contexts[:default][:track].should eq [:address]
352
+ end
353
+
354
+ context 'with nested tracks' do
355
+ it 'works too' do
356
+ name, block = PersonModel.contexts[:addresses][:track][0]
357
+ name.should be(:address)
358
+ block.should be_kind_of(Proc)
359
+ Praxis::Mapper::ConfigHash.from(&block).to_hash.should eq({context: :default})
360
+
361
+ PersonModel.contexts[:addresses][:track][1].should be(:prior_addresses)
362
+ end
363
+ end
364
+
365
+ end
366
+
367
+ context '#inspect' do
368
+ subject(:inspectable) { PersonModel.new( person_rows.first ) }
369
+ its(:inspect){ should =~ /@data: /}
370
+ its(:inspect){ should =~ /@deserialized_data: /}
371
+ its(:inspect){ should_not =~ /@query: /}
372
+ its(:inspect){ should_not =~ /@identity_map: /}
373
+ end
374
+
375
+ context '#identities' do
376
+ context 'with simple keys' do
377
+ subject(:record) { person_records.first }
378
+
379
+ its(:identities) { should eq(id: record.id, email: record.email)}
380
+ end
381
+
382
+ context 'with composite keys' do
383
+ subject(:record) { composite_id_records.first }
384
+ its(:identities) { should eq({[:id, :type] => [record.id, record.type]})}
385
+ end
386
+
387
+ end
388
+
389
+ end