pod4 0.10.6 → 1.0.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.
- checksums.yaml +5 -5
- data/.bugs/bugs +2 -1
- data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
- data/.hgtags +1 -0
- data/Gemfile +5 -5
- data/README.md +157 -46
- data/lib/pod4/basic_model.rb +9 -22
- data/lib/pod4/connection.rb +67 -0
- data/lib/pod4/connection_pool.rb +154 -0
- data/lib/pod4/errors.rb +20 -0
- data/lib/pod4/interface.rb +34 -12
- data/lib/pod4/model.rb +32 -27
- data/lib/pod4/nebulous_interface.rb +25 -30
- data/lib/pod4/null_interface.rb +22 -16
- data/lib/pod4/pg_interface.rb +84 -104
- data/lib/pod4/sequel_interface.rb +138 -82
- data/lib/pod4/tds_interface.rb +83 -70
- data/lib/pod4/tweaking.rb +105 -0
- data/lib/pod4/version.rb +1 -1
- data/md/breaking_changes.md +80 -0
- data/spec/common/basic_model_spec.rb +67 -70
- data/spec/common/connection_pool_parallelism_spec.rb +154 -0
- data/spec/common/connection_pool_spec.rb +246 -0
- data/spec/common/connection_spec.rb +129 -0
- data/spec/common/model_ai_missing_id_spec.rb +256 -0
- data/spec/common/model_plus_encrypting_spec.rb +16 -4
- data/spec/common/model_plus_tweaking_spec.rb +128 -0
- data/spec/common/model_plus_typecasting_spec.rb +10 -4
- data/spec/common/model_spec.rb +283 -363
- data/spec/common/nebulous_interface_spec.rb +159 -108
- data/spec/common/null_interface_spec.rb +88 -65
- data/spec/common/sequel_interface_pg_spec.rb +217 -161
- data/spec/common/shared_examples_for_interface.rb +50 -50
- data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
- data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
- data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
- data/spec/mri/pg_encrypting_spec.rb +1 -1
- data/spec/mri/pg_interface_spec.rb +311 -223
- data/spec/mri/sequel_encrypting_spec.rb +1 -1
- data/spec/mri/sequel_interface_spec.rb +177 -180
- data/spec/mri/tds_encrypting_spec.rb +1 -1
- data/spec/mri/tds_interface_spec.rb +296 -212
- data/tags +340 -174
- metadata +19 -11
- data/md/fixme.md +0 -3
- data/md/roadmap.md +0 -125
- data/md/typecasting.md +0 -80
- data/spec/common/model_new_validate_spec.rb +0 -204
data/lib/pod4/model.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require "octothorpe"
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
3
|
+
require_relative "basic_model"
|
4
|
+
require_relative "errors"
|
5
|
+
require_relative "alert"
|
6
6
|
|
7
7
|
|
8
8
|
module Pod4
|
@@ -112,7 +112,7 @@ module Pod4
|
|
112
112
|
# :id -- otherwise we raise a Pod4Error.
|
113
113
|
#
|
114
114
|
# Note also that while list returns an array of model objects, `read` has _not_ been run
|
115
|
-
# against each object. The data is there, but @model_status == :
|
115
|
+
# against each object. The data is there, but @model_status == :unknown, and validation has not
|
116
116
|
# been run. This is partly for the sake of efficiency, partly to help avoid recursive loops
|
117
117
|
# in validation.
|
118
118
|
#
|
@@ -136,7 +136,7 @@ module Pod4
|
|
136
136
|
|
137
137
|
def test_for_invalid_status(action, status)
|
138
138
|
raise( Pod4Error, "Invalid model status for an action of #{action}", caller ) \
|
139
|
-
if [:
|
139
|
+
if [:unknown, :deleted].include? status
|
140
140
|
|
141
141
|
end
|
142
142
|
|
@@ -159,13 +159,13 @@ module Pod4
|
|
159
159
|
##
|
160
160
|
# Call this to write a new record to the data source.
|
161
161
|
#
|
162
|
-
# Note: create needs to set @
|
162
|
+
# Note: create needs to set @model_id. But interface.create should return it, so that's okay.
|
163
163
|
#
|
164
164
|
def create
|
165
165
|
run_validation(:create)
|
166
166
|
@model_id = interface.create(map_to_interface) unless @model_status == :error
|
167
167
|
|
168
|
-
@model_status = :okay if @model_status == :
|
168
|
+
@model_status = :okay if @model_status == :unknown
|
169
169
|
self
|
170
170
|
rescue Pod4::WeakError
|
171
171
|
add_alert(:error, $!)
|
@@ -183,7 +183,7 @@ module Pod4
|
|
183
183
|
else
|
184
184
|
map_to_model(r)
|
185
185
|
run_validation(:read)
|
186
|
-
@model_status = :okay if @model_status == :
|
186
|
+
@model_status = :okay if @model_status == :unknown
|
187
187
|
end
|
188
188
|
|
189
189
|
self
|
@@ -201,6 +201,10 @@ module Pod4
|
|
201
201
|
clear_alerts; run_validation(:update)
|
202
202
|
interface.update(@model_id, map_to_interface) unless @model_status == :error
|
203
203
|
|
204
|
+
unless interface.id_ai
|
205
|
+
@model_id = instance_variable_get( "@#{interface.id_fld}".to_sym )
|
206
|
+
end
|
207
|
+
|
204
208
|
self
|
205
209
|
rescue Pod4::WeakError
|
206
210
|
add_alert(:error, $!)
|
@@ -259,11 +263,12 @@ module Pod4
|
|
259
263
|
end
|
260
264
|
|
261
265
|
##
|
262
|
-
# Return an Octothorpe of all the attr_columns attributes.
|
266
|
+
# Return an Octothorpe of all the attr_columns attributes. This includes the ID field, whether
|
267
|
+
# or not it has been named in attr_columns.
|
263
268
|
#
|
264
269
|
# Override if you want to return any extra data. (You will need to create a new Octothorpe.)
|
265
270
|
#
|
266
|
-
# See also: `set
|
271
|
+
# See also: `set`
|
267
272
|
#
|
268
273
|
def to_ot
|
269
274
|
Octothorpe.new(to_h)
|
@@ -272,13 +277,11 @@ module Pod4
|
|
272
277
|
##
|
273
278
|
# Used by the interface to set the column values on the model.
|
274
279
|
#
|
275
|
-
# Don't use this to set model attributes from your code; use `set`, instead.
|
276
|
-
#
|
277
280
|
# By default this does exactly the same as `set`. Override it if you want the model to
|
278
281
|
# represent data differently than the data source does -- but then you will have to override
|
279
282
|
# `map_to_interface`, too, to convert the data back.
|
280
283
|
#
|
281
|
-
# See also: `
|
284
|
+
# See also: `map_to_interface'
|
282
285
|
#
|
283
286
|
def map_to_model(ot)
|
284
287
|
merge(ot)
|
@@ -286,19 +289,18 @@ module Pod4
|
|
286
289
|
end
|
287
290
|
|
288
291
|
##
|
289
|
-
#
|
290
|
-
#
|
291
|
-
# Don't use this to get model values in your code; use `to_ot`, instead.# This is called by
|
292
|
-
# model.create and model.update when it needs to write to the data source.
|
292
|
+
# Used by the model to get an OT to pass to the interface on #create and #update.
|
293
293
|
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
# `map_to_model`.
|
294
|
+
# Override it if you want the model to represent data differently than the data source -- in
|
295
|
+
# which case you also need to override `map_to_model`.
|
297
296
|
#
|
298
297
|
# Bear in mind that any attribute could be nil, and likely will be when `map_to_interface` is
|
299
298
|
# called from the create method.
|
300
299
|
#
|
301
|
-
#
|
300
|
+
# NB: we always pass the ID field to the Interface, regardless of whether the field
|
301
|
+
# autoincrements or whether it's been named in `attr_columns`.
|
302
|
+
#
|
303
|
+
# See also: `map_to_model'
|
302
304
|
#
|
303
305
|
def map_to_interface
|
304
306
|
Octothorpe.new(to_h)
|
@@ -307,12 +309,15 @@ module Pod4
|
|
307
309
|
private
|
308
310
|
|
309
311
|
##
|
310
|
-
# Output a hash of the columns
|
312
|
+
# Output a hash of the columns.
|
313
|
+
# _Always_ include the ID field, even if it's not an attribute.
|
311
314
|
#
|
312
315
|
def to_h
|
313
|
-
columns.each_with_object({}) do |col, hash|
|
316
|
+
h = columns.each_with_object({}) do |col, hash|
|
314
317
|
hash[col] = instance_variable_get("@#{col}".to_sym)
|
315
318
|
end
|
319
|
+
|
320
|
+
{interface.id_fld => @model_id}.merge h
|
316
321
|
end
|
317
322
|
|
318
323
|
##
|
@@ -327,11 +332,11 @@ module Pod4
|
|
327
332
|
end
|
328
333
|
|
329
334
|
##
|
330
|
-
# Call the validate method on the model.
|
331
|
-
# the vmode paramter, as they choose.
|
335
|
+
# Call the validate method on the model.
|
332
336
|
#
|
333
337
|
def run_validation(vmode)
|
334
|
-
|
338
|
+
validate(vmode)
|
339
|
+
self
|
335
340
|
end
|
336
341
|
|
337
342
|
end # of Model
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
1
|
+
require_relative "interface"
|
2
|
+
require_relative "errors"
|
3
3
|
|
4
4
|
|
5
5
|
module Pod4
|
@@ -8,7 +8,7 @@ module Pod4
|
|
8
8
|
##
|
9
9
|
# An interface to talk to a Nebulous Target.
|
10
10
|
#
|
11
|
-
# Each interface can only speak with one target, designated with #set_target
|
11
|
+
# Each interface can only speak with one target, designated with #set_target. The developer must
|
12
12
|
# also set a unique ID key using #set_id_fld.
|
13
13
|
#
|
14
14
|
# The primary challenge here is to map the CRUDL methods (which interfaces contract to implement)
|
@@ -42,7 +42,7 @@ module Pod4
|
|
42
42
|
# only keys which are symbols are translated to the corresponding values in the record or
|
43
43
|
# selection hash; anything else is passed literally in the Nebulous parameter string.
|
44
44
|
#
|
45
|
-
# When you subclass
|
45
|
+
# When you subclass NebulousInterface, you may want to override some or all of the CRUDL methods
|
46
46
|
# so that your callers can pass specific parameters rather than a hash; the above example
|
47
47
|
# demonstrates this.
|
48
48
|
#
|
@@ -72,9 +72,13 @@ module Pod4
|
|
72
72
|
# self
|
73
73
|
# end
|
74
74
|
#
|
75
|
+
# NB: Connections: Nebulous does not use the Connection class. The user must configure
|
76
|
+
# NebulousStomp themselves, once, when their application starts; but they don't need to do this
|
77
|
+
# before requiring the models. And there is no need for a connection pool.
|
78
|
+
#
|
75
79
|
class NebulousInterface < Interface
|
76
80
|
|
77
|
-
attr_reader :id_fld
|
81
|
+
attr_reader :id_fld, :id_ai
|
78
82
|
|
79
83
|
# The NebulousStomp Message object holding the response from the last message sent, or, nil.
|
80
84
|
attr_reader :response
|
@@ -91,7 +95,6 @@ module Pod4
|
|
91
95
|
# status for that.
|
92
96
|
attr_reader :response_status
|
93
97
|
|
94
|
-
|
95
98
|
Verb = Struct.new(:name, :params)
|
96
99
|
|
97
100
|
|
@@ -118,7 +121,6 @@ module Pod4
|
|
118
121
|
|
119
122
|
def verbs; {}; end
|
120
123
|
|
121
|
-
|
122
124
|
##
|
123
125
|
# Set the name of the Nebulous target in the interface definition
|
124
126
|
#
|
@@ -132,18 +134,22 @@ module Pod4
|
|
132
134
|
raise Pod4Error, "You need to use set_target on your interface"
|
133
135
|
end
|
134
136
|
|
135
|
-
|
136
137
|
##
|
137
138
|
# Set the name of the ID parameter (needs to be in the CRUD verbs param list)
|
138
|
-
def set_id_fld(idFld)
|
139
|
+
def set_id_fld(idFld, opts={})
|
140
|
+
ai = opts.fetch(:autoincrement) { true }
|
139
141
|
define_class_method(:id_fld) {idFld}
|
142
|
+
define_class_method(:id_ai) {!!ai}
|
140
143
|
end
|
141
144
|
|
142
145
|
def id_fld
|
143
146
|
raise Pod4Error, "You need to use set_id_fld"
|
144
147
|
end
|
145
148
|
|
146
|
-
|
149
|
+
def id_ai
|
150
|
+
raise Pod4Error, "You need to use set_id_fld"
|
151
|
+
end
|
152
|
+
|
147
153
|
##
|
148
154
|
# Make sure all of the above is consistent
|
149
155
|
#
|
@@ -164,29 +170,26 @@ module Pod4
|
|
164
170
|
|
165
171
|
end
|
166
172
|
|
167
|
-
|
168
|
-
end
|
169
|
-
##
|
170
|
-
|
173
|
+
end # of class << self
|
171
174
|
|
172
175
|
##
|
173
176
|
# In normal operation, takes no parameters.
|
174
177
|
#
|
175
178
|
# For testing purposes you may pass something here. Whatever it is you pass, it must respond to
|
176
|
-
# a `send` method, take the same parameters as NebulousStomp::Request.new (that is, a
|
177
|
-
# and a message) and return something that behaves like a NebulousStomp::Request.
|
178
|
-
# will be called instead of creating a NebulousStomp::Request directly.
|
179
|
+
# a `send` method, take the same parameters as NebulousStomp::Request.new (that is, a
|
180
|
+
# target and a message) and return something that behaves like a NebulousStomp::Request.
|
181
|
+
# This method will be called instead of creating a NebulousStomp::Request directly.
|
179
182
|
#
|
180
183
|
def initialize(requestObj=nil)
|
181
184
|
@request_object = requestObj # might as well be a reference
|
182
185
|
@response = nil
|
183
186
|
@response_status = nil
|
184
187
|
@id_fld = self.class.id_fld
|
188
|
+
@id_ai = self.class.id_ai
|
185
189
|
|
186
190
|
self.class.validate_params
|
187
191
|
end
|
188
192
|
|
189
|
-
|
190
193
|
##
|
191
194
|
# Pass a parameter string or array (which will be taken as the literal Nebulous parameter) or a
|
192
195
|
# Hash or Octothorpe (which will be interpreted as per your list of keys set in add_verb
|
@@ -215,7 +218,6 @@ module Pod4
|
|
215
218
|
handle_error(e)
|
216
219
|
end
|
217
220
|
|
218
|
-
|
219
221
|
##
|
220
222
|
# Pass a parameter string or an array as the record. returns the ID. We assume that the
|
221
223
|
# response to the create message returns the ID as the parameter part of the success verb. If
|
@@ -224,6 +226,9 @@ module Pod4
|
|
224
226
|
def create(record)
|
225
227
|
raise ArgumentError, 'create takes a Hash or an Octothorpe' unless hashy?(record)
|
226
228
|
|
229
|
+
raise ArgumentError, "ID field missing from record" \
|
230
|
+
if !@id_ai && record[@id_fld].nil? && record[@id_fld.to_s].nil?
|
231
|
+
|
227
232
|
send_message( verb_for(:create), param_string(:create, record), false )
|
228
233
|
@response.params
|
229
234
|
|
@@ -231,7 +236,6 @@ module Pod4
|
|
231
236
|
handle_error(e)
|
232
237
|
end
|
233
238
|
|
234
|
-
|
235
239
|
##
|
236
240
|
# Given the id, return an Octothorpe of the record.
|
237
241
|
#
|
@@ -253,7 +257,6 @@ module Pod4
|
|
253
257
|
Octothorpe.new( @response.body.is_a?(Hash) ? @response.body : {} )
|
254
258
|
end
|
255
259
|
|
256
|
-
|
257
260
|
##
|
258
261
|
# Given an id an a record (Octothorpe or Hash), update the record. Returns self.
|
259
262
|
#
|
@@ -268,7 +271,6 @@ module Pod4
|
|
268
271
|
self
|
269
272
|
end
|
270
273
|
|
271
|
-
|
272
274
|
##
|
273
275
|
# Given an ID, delete the record. Return self.
|
274
276
|
#
|
@@ -283,7 +285,6 @@ module Pod4
|
|
283
285
|
|
284
286
|
self
|
285
287
|
end
|
286
|
-
|
287
288
|
|
288
289
|
##
|
289
290
|
# Bonus method: chain this method before a CRUDL method to clear the cache for that parameter
|
@@ -337,10 +338,8 @@ module Pod4
|
|
337
338
|
handle_error(err)
|
338
339
|
end
|
339
340
|
|
340
|
-
|
341
341
|
private
|
342
342
|
|
343
|
-
|
344
343
|
##
|
345
344
|
# Given :create, :read, :update, :delete or :list, return the Nebulous verb
|
346
345
|
#
|
@@ -348,7 +347,6 @@ module Pod4
|
|
348
347
|
self.class.verbs[action].name
|
349
348
|
end
|
350
349
|
|
351
|
-
|
352
350
|
##
|
353
351
|
# Work out the parameter string based on the corresponding #set_Verb call. Insert the ID value
|
354
352
|
# if given
|
@@ -365,7 +363,6 @@ module Pod4
|
|
365
363
|
para.join(',')
|
366
364
|
end
|
367
365
|
|
368
|
-
|
369
366
|
##
|
370
367
|
# Deal with any exceptions that are raised.
|
371
368
|
#
|
@@ -393,7 +390,6 @@ module Pod4
|
|
393
390
|
|
394
391
|
end
|
395
392
|
|
396
|
-
|
397
393
|
##
|
398
394
|
# A little helper method to create a response object (unless we were given one for testing
|
399
395
|
# purposes), clear the cache if we are supposed to, and then send the message.
|
@@ -417,12 +413,11 @@ module Pod4
|
|
417
413
|
with_cache ? request.send : request.send_no_cache
|
418
414
|
end
|
419
415
|
|
420
|
-
|
421
416
|
def hashy?(obj)
|
422
417
|
obj.kind_of?(Hash) || obj.kind_of?(Octothorpe)
|
423
418
|
end
|
424
419
|
|
425
|
-
end
|
420
|
+
end # of NebulousInterface
|
426
421
|
|
427
422
|
|
428
423
|
end
|
data/lib/pod4/null_interface.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require
|
1
|
+
require "octothorpe"
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
3
|
+
require_relative "interface"
|
4
|
+
require_relative "errors"
|
5
5
|
|
6
6
|
|
7
7
|
module Pod4
|
@@ -13,16 +13,22 @@ module Pod4
|
|
13
13
|
# Example:
|
14
14
|
# class TestModel < Pod4::Model
|
15
15
|
# attr_columns :one, :two
|
16
|
-
# set_interface NullInterface.new( :one, :two [ {one: 1, two: 2}
|
16
|
+
# set_interface NullInterface.new( :one, :two [ {one: 1, two: 2},
|
17
|
+
# {one: 2, two: 4} ] )
|
17
18
|
# ...
|
18
19
|
#
|
19
|
-
# The first column passed is taken to be the ID.
|
20
|
-
#
|
20
|
+
# The first column passed is taken to be the ID. We default to autoincrement = true, as
|
21
|
+
# standard. Note that ID values for the initial data are not auto-assigned; you need to specify them.
|
22
|
+
#
|
23
|
+
# You can switch to autoincrement = false by setting `interface.id_ai = false`.
|
24
|
+
#
|
25
|
+
# Note: this is quite different from the behaviour before v1.0, where NullInterface effectively
|
26
|
+
# only allowed you to create a non-autoincrement interface.
|
21
27
|
#
|
22
28
|
class NullInterface < Interface
|
23
29
|
|
24
|
-
attr_reader
|
25
|
-
|
30
|
+
attr_reader :id_fld
|
31
|
+
attr_accessor :id_ai
|
26
32
|
|
27
33
|
##
|
28
34
|
# Initialise the interface by passing it a list of columns and an array of hashes to fill them.
|
@@ -33,12 +39,12 @@ module Pod4
|
|
33
39
|
@cols = cols.dup.map(&:to_sym)
|
34
40
|
@data = Array.new(data.dup).flatten
|
35
41
|
@id_fld = @cols.first
|
42
|
+
@id_ai = true
|
36
43
|
|
37
44
|
rescue => e
|
38
45
|
handle_error(e)
|
39
46
|
end
|
40
47
|
|
41
|
-
|
42
48
|
##
|
43
49
|
# Selection is a hash, but only the first key/value pair is honoured.
|
44
50
|
#
|
@@ -56,7 +62,6 @@ module Pod4
|
|
56
62
|
handle_error(e)
|
57
63
|
end
|
58
64
|
|
59
|
-
|
60
65
|
##
|
61
66
|
# Record is a hash of field: value
|
62
67
|
#
|
@@ -65,14 +70,19 @@ module Pod4
|
|
65
70
|
def create(record)
|
66
71
|
raise(ArgumentError, "Create requires an ID") if record.nil? || ! record.respond_to?(:to_h)
|
67
72
|
|
73
|
+
raise(ArgumentError, "Record missing ID field") \
|
74
|
+
if !@id_ai && record[@id_fld].nil? && record[@id_fld.to_s].nil?
|
75
|
+
|
76
|
+
datum = record.to_h
|
77
|
+
datum[@id_fld] = (@data.size + 1) if @id_ai
|
68
78
|
@data << record.to_h
|
69
|
-
|
79
|
+
|
80
|
+
datum[@id_fld]
|
70
81
|
|
71
82
|
rescue => e
|
72
83
|
handle_error(e)
|
73
84
|
end
|
74
85
|
|
75
|
-
|
76
86
|
##
|
77
87
|
# ID is the first column you named in new()
|
78
88
|
#
|
@@ -86,7 +96,6 @@ module Pod4
|
|
86
96
|
handle_error(e)
|
87
97
|
end
|
88
98
|
|
89
|
-
|
90
99
|
##
|
91
100
|
# ID is the first column you named in new(). Record should be a Hash or Octothorpe.
|
92
101
|
# Again, note that we don't care what columns you send us.
|
@@ -103,7 +112,6 @@ module Pod4
|
|
103
112
|
handle_error(e)
|
104
113
|
end
|
105
114
|
|
106
|
-
|
107
115
|
##
|
108
116
|
# ID is that first column
|
109
117
|
#
|
@@ -117,10 +125,8 @@ module Pod4
|
|
117
125
|
handle_error(e)
|
118
126
|
end
|
119
127
|
|
120
|
-
|
121
128
|
private
|
122
129
|
|
123
|
-
|
124
130
|
def handle_error(err, kaller=nil)
|
125
131
|
kaller ||= caller[1..-1]
|
126
132
|
|