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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.bugs/bugs +2 -1
  3. data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
  4. data/.hgtags +1 -0
  5. data/Gemfile +5 -5
  6. data/README.md +157 -46
  7. data/lib/pod4/basic_model.rb +9 -22
  8. data/lib/pod4/connection.rb +67 -0
  9. data/lib/pod4/connection_pool.rb +154 -0
  10. data/lib/pod4/errors.rb +20 -0
  11. data/lib/pod4/interface.rb +34 -12
  12. data/lib/pod4/model.rb +32 -27
  13. data/lib/pod4/nebulous_interface.rb +25 -30
  14. data/lib/pod4/null_interface.rb +22 -16
  15. data/lib/pod4/pg_interface.rb +84 -104
  16. data/lib/pod4/sequel_interface.rb +138 -82
  17. data/lib/pod4/tds_interface.rb +83 -70
  18. data/lib/pod4/tweaking.rb +105 -0
  19. data/lib/pod4/version.rb +1 -1
  20. data/md/breaking_changes.md +80 -0
  21. data/spec/common/basic_model_spec.rb +67 -70
  22. data/spec/common/connection_pool_parallelism_spec.rb +154 -0
  23. data/spec/common/connection_pool_spec.rb +246 -0
  24. data/spec/common/connection_spec.rb +129 -0
  25. data/spec/common/model_ai_missing_id_spec.rb +256 -0
  26. data/spec/common/model_plus_encrypting_spec.rb +16 -4
  27. data/spec/common/model_plus_tweaking_spec.rb +128 -0
  28. data/spec/common/model_plus_typecasting_spec.rb +10 -4
  29. data/spec/common/model_spec.rb +283 -363
  30. data/spec/common/nebulous_interface_spec.rb +159 -108
  31. data/spec/common/null_interface_spec.rb +88 -65
  32. data/spec/common/sequel_interface_pg_spec.rb +217 -161
  33. data/spec/common/shared_examples_for_interface.rb +50 -50
  34. data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
  35. data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
  36. data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
  37. data/spec/mri/pg_encrypting_spec.rb +1 -1
  38. data/spec/mri/pg_interface_spec.rb +311 -223
  39. data/spec/mri/sequel_encrypting_spec.rb +1 -1
  40. data/spec/mri/sequel_interface_spec.rb +177 -180
  41. data/spec/mri/tds_encrypting_spec.rb +1 -1
  42. data/spec/mri/tds_interface_spec.rb +296 -212
  43. data/tags +340 -174
  44. metadata +19 -11
  45. data/md/fixme.md +0 -3
  46. data/md/roadmap.md +0 -125
  47. data/md/typecasting.md +0 -80
  48. data/spec/common/model_new_validate_spec.rb +0 -204
@@ -1,8 +1,8 @@
1
- require 'octothorpe'
1
+ require "octothorpe"
2
2
 
3
- require_relative 'basic_model'
4
- require_relative 'errors'
5
- require_relative 'alert'
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 == :empty, and validation has not
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 [:empty, :deleted].include? status
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 @id. But interface.create should return it, so that's okay.
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 == :empty
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 == :empty
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`, `map_to_model', 'map_to_interface'
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: `to_ot`, `map_to_model'
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
- # used by the model to get an OT of column values for the interface.
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
- # By default this behaves exactly the same as to_ot. Override it if you want the model to
295
- # represent data differently than the data source -- in which case you also need to override
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
- # See also: `to_ot`, `set`.
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. Allow the user to override the method with or without
331
- # the vmode paramter, as they choose.
335
+ # Call the validate method on the model.
332
336
  #
333
337
  def run_validation(vmode)
334
- method(:validate).arity == 0 ? validate : validate(vmode)
338
+ validate(vmode)
339
+ self
335
340
  end
336
341
 
337
342
  end # of Model
@@ -1,5 +1,5 @@
1
- require_relative 'interface'
2
- require_relative 'errors'
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.# The developer must
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 NebulousInterfce, you may want to override some or all of the CRUDL methods
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 target
177
- # and a message) and return something that behaves like a NebulousStomp::Request. This method
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
@@ -1,7 +1,7 @@
1
- require 'octothorpe'
1
+ require "octothorpe"
2
2
 
3
- require_relative 'interface'
4
- require_relative 'errors'
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. Note that ID is not auto-assigned; you need to
20
- # specify it in the record.
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 :id_fld
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
- record[@id_fld]
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