praxis-mapper 3.1.1

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