pod4 0.6.2

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.
@@ -0,0 +1,466 @@
1
+ require 'pod4/sequel_interface'
2
+
3
+ require 'date'
4
+ require 'time'
5
+ require 'bigdecimal'
6
+
7
+ require_relative 'shared_examples_for_interface'
8
+
9
+
10
+ class TestSequelInterface < SequelInterface
11
+ set_table :customer
12
+ set_id_fld :id
13
+ end
14
+
15
+ class SchemaSequelInterface < SequelInterface
16
+ set_schema :public
17
+ set_table :customer
18
+ set_id_fld :id
19
+ end
20
+
21
+ class BadSequelInterface1 < SequelInterface
22
+ set_table :customer
23
+ end
24
+
25
+ class BadSequelInterface2 < SequelInterface
26
+ set_id_fld :id
27
+ end
28
+
29
+
30
+ describe TestSequelInterface do
31
+
32
+ # We actually connect to a special test database for this. I don't generally
33
+ # like unit tests to involve other classes at all, but otherwise we are
34
+ # hardly testing anything, and in any case we do need to test that this class
35
+ # successfully interfaces with Sequel. We can't really do that without
36
+ # talking to a database.
37
+
38
+ let(:data) do
39
+ d = []
40
+ d << { name: 'Barney',
41
+ level: 1.23,
42
+ day: Date.parse("2016-01-01"),
43
+ timestamp: Time.parse('2015-01-01 12:11'),
44
+ price: BigDecimal.new("1.24") }
45
+
46
+ d << { name: 'Fred',
47
+ level: 2.34,
48
+ day: Date.parse("2016-02-02"),
49
+ timestamp: Time.parse('2015-01-02 12:22'),
50
+ price: BigDecimal.new("2.35") }
51
+
52
+ d << { name: 'Betty',
53
+ level: 3.45,
54
+ day: Date.parse("2016-03-03"),
55
+ timestamp: Time.parse('2015-01-03 12:33'),
56
+ price: BigDecimal.new("3.46") }
57
+
58
+ d
59
+ end
60
+
61
+ def fill_data(ifce)
62
+ data.each{|r| ifce.create(r) }
63
+ end
64
+
65
+ # This is stolen almost verbatim from the Sequel Readme. We use an in-memory
66
+ # sqlite database, and we assume that Sequel is sane and behaves broadly the
67
+ # same for our limited purposes as it would when talking to TinyTDS or Pg.
68
+ # This may be an entirely unwarranted assumption. If so, we will have to
69
+ # change this. But in any case, we are not in the business of testing Sequel:
70
+ # just our interface to it.
71
+ let (:db) do
72
+ db = Sequel.sqlite
73
+ db.create_table :customer do
74
+ primary_key :id
75
+ String :name
76
+ Float :level
77
+ Date :day
78
+ Time :timestamp
79
+ BigDecimal :price, :size=>[10.2] # Sequel doesn't support money
80
+ end
81
+ db
82
+ end
83
+
84
+ let(:interface) { TestSequelInterface.new(db) }
85
+
86
+ before do
87
+ fill_data(interface)
88
+ end
89
+
90
+ ##
91
+
92
+
93
+ it_behaves_like 'an interface' do
94
+
95
+ let(:interface) do
96
+ db2 = Sequel.sqlite
97
+ db2.create_table :customer do
98
+ primary_key :id
99
+ String :name
100
+ Float :level
101
+ Date :day
102
+ Time :timestamp
103
+ BigDecimal :price, :size=>[10.2]
104
+ end
105
+
106
+ TestSequelInterface.new(db2)
107
+ end
108
+
109
+ let(:record) { {name: 'Barney', price: 1.11} }
110
+ #let(:record_id) { 'Barney' }
111
+
112
+ end
113
+ ##
114
+
115
+
116
+ describe 'SequelInterface.set_schema' do
117
+ it 'takes one argument' do
118
+ expect( SequelInterface ).to respond_to(:set_schema).with(1).argument
119
+ end
120
+ end
121
+ ##
122
+
123
+
124
+ describe 'SequelInterface.schema' do
125
+ it 'returns the schema' do
126
+ expect( SchemaSequelInterface.schema ).to eq :public
127
+ end
128
+
129
+ it 'is optional' do
130
+ expect{ TestSequelInterface.schema }.not_to raise_exception
131
+ expect( TestSequelInterface.schema ).to eq nil
132
+ end
133
+ end
134
+ ##
135
+
136
+
137
+ describe 'SequelInterface.set_table' do
138
+ it 'takes one argument' do
139
+ expect( SequelInterface ).to respond_to(:set_table).with(1).argument
140
+ end
141
+ end
142
+ ##
143
+
144
+
145
+ describe 'SequelInterface.table' do
146
+ it 'returns the table' do
147
+ expect( TestSequelInterface.table ).to eq :customer
148
+ end
149
+ end
150
+ ##
151
+
152
+
153
+ describe 'SequelInterface.set_id_fld' do
154
+ it 'takes one argument' do
155
+ expect( SequelInterface ).to respond_to(:set_id_fld).with(1).argument
156
+ end
157
+ end
158
+ ##
159
+
160
+
161
+ describe 'SequelInterface.id_fld' do
162
+ it 'returns the ID field name' do
163
+ expect( TestSequelInterface.id_fld ).to eq :id
164
+ end
165
+ end
166
+ ##
167
+
168
+
169
+ describe '#new' do
170
+
171
+ it 'requires a Sequel DB object' do
172
+ expect{ TestSequelInterface.new }.to raise_exception ArgumentError
173
+ expect{ TestSequelInterface.new(nil) }.to raise_exception ArgumentError
174
+ expect{ TestSequelInterface.new('foo') }.to raise_exception ArgumentError
175
+
176
+ expect{ TestSequelInterface.new(db) }.not_to raise_exception
177
+ end
178
+
179
+ it 'requires the table and id field to be defined in the class' do
180
+ expect{ SequelInterface.new(db) }.to raise_exception Pod4Error
181
+ expect{ BadSequelInterface1.new(db) }.to raise_exception Pod4Error
182
+ expect{ BadSequelInterface2.new(db) }.to raise_exception Pod4Error
183
+ end
184
+
185
+ end
186
+ ##
187
+
188
+
189
+ describe '#quoted_table' do
190
+
191
+ it 'returns just the table when the schema is not set' do
192
+ expect( interface.quoted_table ).to eq( %Q|`customer`| )
193
+ end
194
+
195
+ it 'returns the schema plus table when the schema is set' do
196
+ ifce = SchemaSequelInterface.new(db)
197
+ expect( ifce.quoted_table ).to eq( %|`public`.`customer`| )
198
+ end
199
+
200
+ end
201
+ ##
202
+
203
+
204
+ describe '#create' do
205
+
206
+ let(:hash) { {name: 'Bam-Bam', price: 4.44} }
207
+ let(:ot) { Octothorpe.new(name: 'Wilma', price: 5.55) }
208
+
209
+ it 'raises a Pod4::DatabaseError if anything goes wrong' do
210
+ expect{ interface.create(one: 'two') }.to raise_exception DatabaseError
211
+ end
212
+
213
+ it 'creates the record when given a hash' do
214
+ # kinda impossible to seperate these two tests
215
+ id = interface.create(hash)
216
+
217
+ expect{ interface.read(id) }.not_to raise_exception
218
+ expect( interface.read(id).to_h ).to include hash
219
+ end
220
+
221
+ it 'creates the record when given an Octothorpe' do
222
+ id = interface.create(ot)
223
+
224
+ expect{ interface.read(id) }.not_to raise_exception
225
+ expect( interface.read(id).to_h ).to include ot.to_h
226
+ end
227
+
228
+ it 'does not freak out if the hash has symbol values' do
229
+ # Which, Sequel does
230
+ expect{ interface.create(name: :Booboo) }.not_to raise_exception
231
+ end
232
+
233
+ it 'shouldnt have a problem with record values of nil' do
234
+ record = {name: 'Ranger', price: nil}
235
+ expect{ interface.create(record) }.not_to raise_exception
236
+ id = interface.create(record)
237
+ expect( interface.read(id).to_h ).to include(record)
238
+ end
239
+
240
+ end
241
+ ##
242
+
243
+
244
+ describe '#read' do
245
+
246
+ it 'returns the record for the id as an Octothorpe' do
247
+ expect( interface.read(2).to_h ).to include(name: 'Fred', price: 2.35)
248
+ end
249
+
250
+ it 'raises a Pod4::CantContinue if the ID is bad' do
251
+ expect{ interface.read(:foo) }.to raise_exception CantContinue
252
+ end
253
+
254
+ it 'returns an empty Octothorpe if no record matches the ID' do
255
+ expect{ interface.read(99) }.not_to raise_exception
256
+ expect( interface.read(99) ).to be_a_kind_of Octothorpe
257
+ expect( interface.read(99) ).to be_empty
258
+ end
259
+
260
+ it 'returns real fields as Float' do
261
+ level = interface.read(1).>>.level
262
+
263
+ expect( level ).to be_a_kind_of Float
264
+ expect( level ).to be_within(0.001).of( data.first[:level] )
265
+ end
266
+
267
+ it 'returns date fields as Date' do
268
+ date = interface.read(1).>>.day
269
+
270
+ expect( date ).to be_a_kind_of Date
271
+ expect( date ).to eq data.first[:day]
272
+ end
273
+
274
+ it 'returns datetime fields as Time' do
275
+ timestamp = interface.read(1).>>.timestamp
276
+
277
+ expect( timestamp ).to be_a_kind_of Time
278
+ expect( timestamp ).to eq data.first[:timestamp]
279
+ end
280
+
281
+ it 'returns numeric fields as BigDecimal' do
282
+ price = interface.read(1).>>.price
283
+
284
+ expect( price ).to be_a_kind_of BigDecimal
285
+ expect( price ).to eq data.first[:price]
286
+ end
287
+
288
+ end
289
+ ##
290
+
291
+
292
+
293
+ describe '#list' do
294
+
295
+ it 'has an optional selection parameter, a hash' do
296
+ # Actually it does not have to be a hash, but FTTB we only support that.
297
+ expect{ interface.list(name: 'Barney') }.not_to raise_exception
298
+ end
299
+
300
+ it 'returns an array of Octothorpes that match the records' do
301
+ # convert each OT to a hash and remove the ID key
302
+ arr = interface.list.map {|ot| x = ot.to_h; x.delete(:id); x }
303
+
304
+ expect( arr ).to match_array data
305
+ end
306
+
307
+ it 'returns a subset of records based on the selection parameter' do
308
+ expect( interface.list(name: 'Fred').size ).to eq 1
309
+
310
+ expect( interface.list(name: 'Betty').first.to_h ).
311
+ to include(name: 'Betty', price: 3.46)
312
+
313
+ end
314
+
315
+ it 'returns an empty Array if nothing matches' do
316
+ expect( interface.list(name: 'Yogi') ).to eq([])
317
+ end
318
+
319
+ it 'raises DatabaseError if the selection criteria is nonsensical' do
320
+ expect{ interface.list('foo') }.to raise_exception Pod4::DatabaseError
321
+ end
322
+
323
+ it 'returns an empty array if there is no data' do
324
+ interface.list.each {|x| interface.delete(x[interface.id_fld]) }
325
+ expect( interface.list ).to eq([])
326
+ end
327
+
328
+ it 'does not freak out if the hash has symbol values' do
329
+ # Which, Sequel does
330
+ expect{ interface.list(name: :Barney) }.not_to raise_exception
331
+ end
332
+
333
+
334
+ end
335
+ ##
336
+
337
+
338
+ describe '#update' do
339
+
340
+ let(:id) { interface.list.first[:id] }
341
+
342
+ it 'updates the record at ID with record parameter' do
343
+ record = {name: 'Booboo', price: 99.99}
344
+ interface.update(id, record)
345
+
346
+ booboo = interface.read(id)
347
+ expect( booboo.>>.name ).to eq( record[:name] )
348
+ expect( booboo.>>.price.to_f ).to eq( record[:price] )
349
+ end
350
+
351
+ it 'raises a CantContinue if anything weird happens with the ID' do
352
+ expect{ interface.update(99, name: 'Booboo') }.
353
+ to raise_exception CantContinue
354
+
355
+ end
356
+
357
+ it 'raises a DatabaseError if anything weird happensi with the record' do
358
+ expect{ interface.update(id, smarts: 'more') }.
359
+ to raise_exception DatabaseError
360
+
361
+ end
362
+
363
+ it 'does not freak out if the hash has symbol values' do
364
+ # Which, Sequel does
365
+ expect{ interface.update(id, name: :Booboo) }.not_to raise_exception
366
+ end
367
+
368
+ it 'shouldnt have a problem with record values of nil' do
369
+ record = {name: 'Ranger', price: nil}
370
+ expect{ interface.update(id, record) }.not_to raise_exception
371
+ expect( interface.read(id).to_h ).to include(record)
372
+ end
373
+
374
+ end
375
+ ##
376
+
377
+
378
+ describe '#delete' do
379
+
380
+ def list_contains(id)
381
+ interface.list.find {|x| x[interface.id_fld] == id }
382
+ end
383
+
384
+ let(:id) { interface.list.first[:id] }
385
+
386
+ it 'raises CantContinue if anything hinky happens with the ID' do
387
+ expect{ interface.delete(:foo) }.to raise_exception CantContinue
388
+ expect{ interface.delete(99) }.to raise_exception CantContinue
389
+ end
390
+
391
+ it 'makes the record at ID go away' do
392
+ expect( list_contains(id) ).to be_truthy
393
+ interface.delete(id)
394
+ expect( list_contains(id) ).to be_falsy
395
+ end
396
+
397
+ end
398
+ ##
399
+
400
+
401
+ describe '#execute' do
402
+
403
+ let(:sql) { 'delete from customer where price < 2.0;' }
404
+
405
+ it 'requires an SQL string' do
406
+ expect{ interface.execute }.to raise_exception ArgumentError
407
+ expect{ interface.execute(nil) }.to raise_exception ArgumentError
408
+ expect{ interface.execute(14) }.to raise_exception ArgumentError
409
+ end
410
+
411
+ it 'raises some sort of Pod4 error if it runs into problems' do
412
+ expect{ interface.execute('delete from not_a_table') }.
413
+ to raise_exception Pod4Error
414
+
415
+ end
416
+
417
+ it 'executes the string' do
418
+ expect{ interface.execute(sql) }.not_to raise_exception
419
+ expect( interface.list.size ).to eq(data.size - 1)
420
+ expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
421
+ end
422
+
423
+ end
424
+ ##
425
+
426
+
427
+ describe '#select' do
428
+
429
+ it 'requires an SQL string' do
430
+ expect{ interface.select }.to raise_exception ArgumentError
431
+ expect{ interface.select(nil) }.to raise_exception ArgumentError
432
+ expect{ interface.select(14) }.to raise_exception ArgumentError
433
+ end
434
+
435
+ it 'raises some sort of Pod4 error if it runs into problems' do
436
+ expect{ interface.select('select * from not_a_table') }.
437
+ to raise_exception Pod4Error
438
+
439
+ end
440
+
441
+ it 'returns the result of the sql' do
442
+ sql1 = 'select name from customer where price < 2.0;'
443
+ sql2 = 'select name from customer where price < 0.0;'
444
+
445
+ expect{ interface.select(sql1) }.not_to raise_exception
446
+ expect( interface.select(sql1) ).to eq( [{name: 'Barney'}] )
447
+ expect( interface.select(sql2) ).to eq( [] )
448
+ end
449
+
450
+ it 'works if you pass a non-select' do
451
+ # By which I mean: still executes the SQL; returns []
452
+ sql = 'delete from customer where price < 2.0;'
453
+ ret = interface.select(sql)
454
+
455
+ expect( interface.list.size ).to eq(data.size - 1)
456
+ expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
457
+ expect( ret ).to eq( [] )
458
+ end
459
+
460
+
461
+ end
462
+ ##
463
+
464
+
465
+ end
466
+
@@ -0,0 +1,160 @@
1
+ require 'octothorpe'
2
+
3
+
4
+ ##
5
+ # These are the shared tests for all interfaces. To use them you need to
6
+ # supply:
7
+ #
8
+ # * record - a record to insert
9
+ # * interface - an instance of the interface to call.
10
+ #
11
+ # For example (from nebulous_interface_spec):
12
+ #
13
+ # it_behaves_like 'an interface' do
14
+ # let(:record) { {id: 1, name: 'percy'} }
15
+ #
16
+ # let(:interface) do
17
+ # init_nebulous
18
+ # TestNebulousInterface.new( FakeRequester.new )
19
+ # end
20
+ # end
21
+ #
22
+ # 'record' does not have to include every column in your table, and you should
23
+ # actively exclude floats (where you cannot guarantee equality) or anything
24
+ # that is supposed to render to a BigDecimal. Make sure these fields are NULL
25
+ # in the table definition.
26
+ #
27
+ # Note that these shared tests only test the common parts of the API that the
28
+ # interface exposes to the *model*; they make no assumptions about where your
29
+ # test data is coming from, or how you are calling or mocking whatever library
30
+ # the interface is an adapter to.
31
+ #
32
+ # It's up to the individual specs to test that the interface is calling its
33
+ # library correctly and deal with all the things specific to that interface -
34
+ # which includes how the model calls new() and list(), for example.
35
+ #
36
+ RSpec.shared_examples 'an interface' do
37
+
38
+ let(:record_as_ot) { Octothorpe.new(record) }
39
+
40
+
41
+ describe '#id_fld' do
42
+ it 'is an attribute' do
43
+ expect( interface.id_fld ).not_to be_nil
44
+ end
45
+ end
46
+ ##
47
+
48
+
49
+ describe '#create' do
50
+
51
+ it 'requires a hash or an Octothorpe' do
52
+ expect{ interface.create }.to raise_exception ArgumentError
53
+ expect{ interface.create(nil) }.to raise_exception ArgumentError
54
+ expect{ interface.create(3) }.to raise_exception ArgumentError
55
+
56
+ expect{ interface.create(record) }.not_to raise_exception
57
+ expect{ interface.create(record_as_ot) }.not_to raise_exception
58
+ end
59
+
60
+ it 'returns the ID' do
61
+ record_id = interface.create(record)
62
+ expect{ interface.read(record_id) }.not_to raise_exception
63
+ expect( interface.read(record_id).to_h ).to include record
64
+ end
65
+
66
+ end
67
+ ##
68
+
69
+
70
+ describe '#read' do
71
+
72
+ before do
73
+ interface.create(record)
74
+ @id = interface.list.first[interface.id_fld]
75
+ end
76
+
77
+ it 'requires an ID' do
78
+ expect{ interface.read }.to raise_exception ArgumentError
79
+ expect{ interface.read(nil) }.to raise_exception ArgumentError
80
+
81
+ expect{ interface.read(@id) }.not_to raise_exception
82
+ end
83
+
84
+ it 'returns an Octothorpe' do
85
+ expect( interface.read(@id) ).to be_a_kind_of Octothorpe
86
+ end
87
+
88
+ end
89
+ ##
90
+
91
+
92
+ describe '#update' do
93
+
94
+ before do
95
+ interface.create(record)
96
+ @id = interface.list.first[interface.id_fld]
97
+ end
98
+
99
+
100
+ it 'requires an ID and a record (hash or OT)' do
101
+ expect{ interface.update }.to raise_exception ArgumentError
102
+ expect{ interface.update(nil) }.to raise_exception ArgumentError
103
+ expect{ interface.update(14) }.to raise_exception ArgumentError
104
+
105
+ expect{ interface.update(@id, record) }.not_to raise_exception
106
+ end
107
+
108
+ it 'returns self' do
109
+ expect( interface.update(@id, record) ).to eq interface
110
+ end
111
+
112
+ end
113
+ ##
114
+
115
+
116
+ describe '#delete' do
117
+
118
+ before do
119
+ interface.create(record)
120
+ @id = interface.list.first[interface.id_fld]
121
+ end
122
+
123
+ it 'requires an id' do
124
+ expect{ interface.delete }.to raise_exception ArgumentError
125
+ expect{ interface.delete(nil) }.to raise_exception ArgumentError
126
+
127
+ expect{ interface.delete(@id) }.not_to raise_exception
128
+ end
129
+
130
+ it 'returns self' do
131
+ expect( interface.delete(@id) ).to eq interface
132
+ end
133
+
134
+ end
135
+ ##
136
+
137
+
138
+ describe '#list' do
139
+
140
+ it 'will allow itself to be called with no parameter' do
141
+ expect{ interface.list }.not_to raise_exception
142
+ end
143
+
144
+ it 'returns an array of Octothorpes' do
145
+ interface.create(record)
146
+ expect( interface.list ).to be_a_kind_of Array
147
+ expect( interface.list.first ).to be_a_kind_of Octothorpe
148
+ end
149
+
150
+ it 'has the ID field as one of the Octothorpe keys' do
151
+ interface.create(record)
152
+ expect( interface.list.first.to_h ).to have_key interface.id_fld
153
+ end
154
+
155
+ end
156
+ ##
157
+
158
+
159
+ end
160
+
@@ -0,0 +1,25 @@
1
+ require 'pry' #always useful when debugging
2
+ require 'pod4'
3
+
4
+
5
+ include Pod4
6
+
7
+
8
+ RSpec.configure do |config|
9
+ config.expect_with :rspec do |expectations|
10
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
11
+ end
12
+
13
+ config.mock_with :rspec do |mocks|
14
+ mocks.verify_partial_doubles = true
15
+ end
16
+
17
+ config.filter_run :focus
18
+ config.run_all_when_everything_filtered = true
19
+
20
+ if config.files_to_run.one?
21
+ config.default_formatter = 'doc'
22
+ end
23
+
24
+ end
25
+