pod4 0.10.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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