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,408 @@
1
+ require 'nebulous'
2
+
3
+ require_relative 'interface'
4
+ require_relative 'errors'
5
+
6
+
7
+ module Pod4
8
+
9
+
10
+ ##
11
+ # An interface to talk to a Nebulous Target.
12
+ #
13
+ # Each interface can only speak with one target, designated with #set_target.
14
+ # The developer must also set a unique ID key using #set_id_fld.
15
+ #
16
+ # The primary challenge here is to map the CRUDL methods (which interfaces
17
+ # contract to implement) to nebulous verbs. The programmer uses #set_verb for
18
+ # this purpose: the first parameter indicates the CRUDL method, the next is
19
+ # the verb name, and the rest are hash keys.
20
+ #
21
+ # In the case of the #create and #update methods, the list of keys controls
22
+ # which parts of the incoming hash end up in the verb parameters, and in what
23
+ # order. For #update, the list must include the ID key that you gave to
24
+ # #set_id_fld.
25
+ #
26
+ # Parameters for the #list method similarly constrain how its selection
27
+ # parameter is translated to a nebulous verb parameter string.
28
+ #
29
+ # Parameters for #read and #delete can be whatever you like, but since the
30
+ # only value passed to read is the ID, the only symbol there should be the
31
+ # same as the one in #set_id_fld.
32
+ #
33
+ # class CustomerInterface < SwingShift::NebulousInterface
34
+ # set_target 'accord'
35
+ # set_id_fld :id
36
+ # set_verb :read, 'customerread', :id, '100'
37
+ # set_verb :list, 'customerlist', :name
38
+ # set_verb :create, 'customerupdate', 'create', :name, :price
39
+ # set_verb :update, 'customerupdate', 'update', :name, :id, :price
40
+ #
41
+ # def update(id, name, price)
42
+ # super( id, name: name, price: price)
43
+ # end
44
+ # end
45
+ #
46
+ # In this example both the create and update methods point to the same
47
+ # nebulous verb. Note that only keys which are symbols are translated to the
48
+ # corresponding values in the record or selection hash; anything else is
49
+ # passed literally in the Nebulous parameter string.
50
+ #
51
+ # When you subclass NebulousInterfce, you may want to override some or all of
52
+ # the CRUDL methods so that your callers can pass specific parameters rather
53
+ # than a hash; the above example demonstrates this.
54
+ #
55
+ # We assume that the response to the #create message returns the ID as the
56
+ # parameter part of the success verb. If that's not true, then you will have
57
+ # to override #create and sort this out yourself.
58
+ #
59
+ # Finally, note that all values are returned as strings; there is no
60
+ # typecasting. This is a given limitation for Nebulous as a whole.
61
+ #
62
+ class NebulousInterface < Interface
63
+
64
+ attr_reader :id_fld
65
+
66
+ # The NebResponse object from the last message sent, or nil otherwise
67
+ attr_reader :response
68
+
69
+ # The status of the response from the last message:
70
+ # * nil - we didn't send a request yet
71
+ # * :off - Nebulous is turned off, so nothing happened
72
+ # * :timeout we sent the message but timed out waiting for a response
73
+ # * :verberror - we got an error verb in response
74
+ # * :verbsuccess - we got a success verb in response
75
+ # * :response - we got some response that doesn't follow The Protocol
76
+ #
77
+ # NB: if we got an exception sending the message, we raised it on the
78
+ # caller, so there is no status for that.
79
+ attr_reader :response_status
80
+
81
+
82
+ Verb = Struct.new(:name, :params)
83
+
84
+
85
+ class << self
86
+ #--
87
+ # These are set in the class because it keeps the model code cleaner: the
88
+ # definition of the interface stays in the interface, and doesn't leak
89
+ # out into the model.
90
+ #++
91
+
92
+ ##
93
+ # Set a verb.
94
+ # * action - must be one of CRUDL
95
+ # * verb - the name of the verb
96
+ # * parameters - array of symbols to order the hash passed to create, etc
97
+ #
98
+ def set_verb(action, verb, *paramKeys)
99
+ raise ArgumentError, "Bad action" \
100
+ unless Interface::ACTIONS.include? action
101
+
102
+ v = verbs.dup
103
+ v[action] = Verb.new( verb, paramKeys.flatten )
104
+
105
+ define_class_method(:verbs) {v}
106
+ end
107
+
108
+ def verbs; {}; end
109
+
110
+
111
+ ##
112
+ # Set the name of the Nebulous target in the interface definition
113
+ #
114
+ # a reference to the interface object.
115
+ def set_target(target)
116
+ define_class_method(:target) {target.to_s}
117
+ end
118
+
119
+ def target
120
+ raise Pod4Error, "You need to use set_target on your interface"
121
+ end
122
+
123
+
124
+ ##
125
+ # Set the name of the ID parameter (needs to be in the CRUD verbs param
126
+ # list)
127
+ def set_id_fld(idFld)
128
+ define_class_method(:id_fld) {idFld}
129
+ end
130
+
131
+ def id_fld
132
+ raise Pod4Error, "You need to use set_id_fld"
133
+ end
134
+
135
+
136
+ ##
137
+ # Make sure all of the above is consistent
138
+ #
139
+ def validate_params
140
+ raise Pod4Error, "You need to use set_verb" if verbs == {}
141
+
142
+ %i|create read update delete|.each do |action|
143
+ raise Pod4Error, "set_verb #{action} is missing a parameter list" \
144
+ if verbs[action] && !verbs[action].params == []
145
+
146
+ end
147
+
148
+ %i|read update delete|.each do |action|
149
+ raise Pod4Error, "#{action} verb doesn't have an #{id_fld} key" \
150
+ if verbs[action] && !verbs[action].params.include?(id_fld)
151
+
152
+ end
153
+
154
+ end
155
+
156
+
157
+ end
158
+ ##
159
+
160
+
161
+ ##
162
+ # In normal operation, takes no parameters.
163
+ #
164
+ # For testing purposes you may pass an instance of a class here. It must
165
+ # respond to a #send method with parameters (verb, parameter string, cache
166
+ # yes/no) by returning some kind of NebRequest (presumably either a double
167
+ # or an instance of NebRequestNull). This method will be called instead of
168
+ # creating a NebRequest directly.
169
+ #
170
+ def initialize(requestObj=nil)
171
+ @request_object = requestObj # might as well be a reference
172
+ @response = nil
173
+ @response_status = nil
174
+ @id_fld = self.class.id_fld
175
+
176
+ self.class.validate_params
177
+ end
178
+
179
+
180
+ ##
181
+ # Pass a parameter string or array (which will be taken as the literal
182
+ # Nebulous parameter) or a Hash or Octothorpe (which will be interpreted as
183
+ # per your list of keys set in add_verb :list).
184
+ #
185
+ # Returns an array of Octothorpes, or an empty array if the responder could
186
+ # not make any records out of our message.
187
+ #
188
+ def list(selection=nil)
189
+ sel =
190
+ case selection
191
+ when Array, Hash, Octothorpe then param_string(:list, selection)
192
+ else selection
193
+ end
194
+
195
+ send_message( verb_for(:list), sel )
196
+
197
+ @response.body_to_h # should be an array irrespective of the method name
198
+ .map{|e| Octothorpe.new(e) }
199
+
200
+ rescue => e
201
+ handle_error(e)
202
+ end
203
+
204
+
205
+ ##
206
+ # Pass a parameter string or an array as the record. returns the ID.
207
+ # We assume that the response to the create message returns the ID as the
208
+ # parameter part of the success verb. If that's not true, then you will
209
+ # have to override #create and sort this out yourself.
210
+ #
211
+ def create(record)
212
+ raise ArgumentError, 'create takes a Hash or an Octothorpe' \
213
+ unless hashy?(record)
214
+
215
+ send_message( verb_for(:create), param_string(:create, record) )
216
+
217
+ @response.params
218
+
219
+ rescue => e
220
+ handle_error(e)
221
+ end
222
+
223
+
224
+ ##
225
+ # Given the id, return an Octothorpe of the record.
226
+ #
227
+ # The actual parameters passed to nebulous depend on how you #set_verb
228
+ #
229
+ def read(id)
230
+ raise ArgumentError, 'You must pass an ID to read' unless id
231
+
232
+ send_message( verb_for(:read), param_string(:read, nil, id) )
233
+
234
+ Octothorpe.new( @response.body_to_h )
235
+ end
236
+
237
+
238
+ ##
239
+ # Given an id an a record (Octothorpe or Hash), update the record. Returns
240
+ # self.
241
+ #
242
+ def update(id, record)
243
+ raise ArgumentError, 'You must pass an ID to update' unless id
244
+ raise ArgumentError, 'update record takes a Hash or an Octothorpe' \
245
+ unless hashy?(record)
246
+
247
+ send_message( verb_for(:update),
248
+ param_string(:update, record, id),
249
+ false )
250
+
251
+ self
252
+ end
253
+
254
+
255
+ ##
256
+ # Given an ID, delete the record. Return self.
257
+ #
258
+ # The actual parameters passed to nebulous depend on how you #set_verb
259
+ #
260
+ def delete(id)
261
+ raise ArgumentError, 'You must pass an ID to delete' unless id
262
+
263
+ send_message( verb_for(:delete),
264
+ param_string(:delete, nil, id),
265
+ false )
266
+
267
+ self
268
+ end
269
+
270
+
271
+ ##
272
+ # Bonus method: chain this method before a CRUDL method to clear the cache
273
+ # for that parameter string:
274
+ # @interface.clearing_cache.read(14)
275
+ #
276
+ def clearing_cache
277
+ @clear_cache = true
278
+ self
279
+ end
280
+
281
+
282
+ ##
283
+ # Bonus method: send an arbitrary Nebulous message to the target and return
284
+ # the response object.
285
+ #
286
+ # We don't trap errors here - see #handle_error - but we raise extra ones
287
+ # if we think things look fishy.
288
+ #
289
+ def send_message(verb, paramStr, with_cache=true)
290
+ unless Nebulous.on?
291
+ @response_status = :off
292
+ raise Pod4::DatabaseError, "Nebulous is turned off!"
293
+ end
294
+
295
+ Pod4.logger.debug(__FILE__) do
296
+ "Sending v:#{verb} p:#{paramStr} c?: #{with_cache}"
297
+ end
298
+
299
+ @response = send_message_helper(verb, paramStr, with_cache)
300
+
301
+ raise Pod4::DatabaseError, "Null response" if @response.nil?
302
+
303
+ @response_status =
304
+ case @response.verb
305
+ when 'error' then :verberror
306
+ when 'success' then :verbsuccess
307
+ else :response
308
+ end
309
+
310
+ raise Pod4::CantContinue, "Nebulous returned an error verb" \
311
+ if @response_status == :verberror
312
+
313
+ self
314
+
315
+ rescue => err
316
+ handle_error(err)
317
+ end
318
+
319
+
320
+ private
321
+
322
+
323
+ ##
324
+ # Given :create, :read, :update, :delete or :list, return the Nebulous verb
325
+ #
326
+ def verb_for(action)
327
+ self.class.verbs[action].name
328
+ end
329
+
330
+
331
+ ##
332
+ # Work out the parameter string based on the corresponding #set_Verb call.
333
+ # Insert the ID value if given
334
+ #
335
+ def param_string(action, hashParam, id=nil)
336
+ hash = hashParam ? hashParam.dup : {}
337
+
338
+ hash[@id_fld] = id.to_s if id
339
+
340
+ para = self.class.verbs[action].params.map do |p|
341
+ p.kind_of?(Symbol) ? hash[p] : p
342
+ end
343
+
344
+ para.join(',')
345
+ end
346
+
347
+
348
+ ##
349
+ # Deal with any exceptions that are raised.
350
+ #
351
+ # Our contract says that we should throw errors to the model, but those
352
+ # errors should be Pod4 errors.
353
+ #
354
+ def handle_error(err, kaller=caller[1..-1])
355
+ Pod4.logger.error(__FILE__){ err.message }
356
+
357
+ case err
358
+ when ArgumentError, Pod4::Pod4Error
359
+ raise err.class, err.message, kaller
360
+
361
+ when Nebulous::NebulousTimeout
362
+ @response_status = :timeout
363
+ raise Pod4::CantContinue, err.message, kaller
364
+
365
+ when Nebulous::NebulousError
366
+ raise Pod4::DatabaseError, err.message, kaller
367
+
368
+ else
369
+ raise Pod4::Pod4Error, err.message, kaller
370
+
371
+ end
372
+
373
+ end
374
+
375
+
376
+ ##
377
+ # A little helper method to create a response object (unless we were given
378
+ # one for testing purposes), clear the cache if we are supposed to, and
379
+ # then send the message.
380
+ #
381
+ # returns the response to the request.
382
+ #
383
+ def send_message_helper(verb, paramStr, with_cache)
384
+ request =
385
+ if @request_object
386
+ @request_object.send(verb, paramStr, with_cache)
387
+ else
388
+ Nebulous::NebRequest.new(self.class.target, verb, paramStr)
389
+ end
390
+
391
+ if @clear_cache
392
+ request.clear_cache
393
+ @clear_cache = false
394
+ end
395
+
396
+ with_cache ? request.send : request.send_no_cache
397
+ end
398
+
399
+
400
+ def hashy?(obj)
401
+ obj.kind_of?(Hash) || obj.kind_of?(Octothorpe)
402
+ end
403
+
404
+
405
+ end
406
+
407
+
408
+ end
@@ -0,0 +1,148 @@
1
+ require 'octothorpe'
2
+
3
+ require_relative 'interface'
4
+ require_relative 'errors'
5
+
6
+
7
+ module Pod4
8
+
9
+
10
+ ##
11
+ # Pod4 Interface *for testing*. Fakes a table and records.
12
+ #
13
+ # Example:
14
+ # class TestModel < Pod4::Model
15
+ # attr_columns :one, :two
16
+ # set_interface NullInterface.new( :one, :two [ {one: 1, two: 2} ] )
17
+ # ...
18
+ #
19
+ # The first column passed is taken to be the ID.
20
+ # Note that ID is not auto-assigned; you need to specify it in the record.
21
+ #
22
+ class NullInterface < Interface
23
+
24
+ attr_reader :id_fld
25
+
26
+
27
+ ##
28
+ # Initialise the interface by passing it a list of columns and an array of
29
+ # hashes to fill them.
30
+ #
31
+ def initialize(*cols, data)
32
+ raise ArgumentError, "no columns" if cols.nil? || cols == []
33
+
34
+ @cols = cols.dup.map(&:to_sym)
35
+ @data = Array.new(data.dup).flatten
36
+ @id_fld = @cols.first
37
+
38
+ rescue => e
39
+ handle_error(e)
40
+ end
41
+
42
+
43
+ ##
44
+ # Selection is a hash, but only the first key/value pair is honoured.
45
+ #
46
+ def list(selection=nil)
47
+ if selection
48
+ key, value = selection.to_a.first
49
+ rows = @data.find_all {|r| r[key.to_sym] == value}
50
+ else
51
+ rows = @data
52
+ end
53
+
54
+ rows.map{|x| Octothorpe.new(x) }
55
+
56
+ rescue => e
57
+ handle_error(e)
58
+ end
59
+
60
+
61
+ ##
62
+ # Record is a hash of field: value
63
+ # Note that we will store any old crap, not just the fields you named in
64
+ # new().
65
+ #
66
+ def create(record)
67
+ raise(ArgumentError, "Create requires an ID") \
68
+ if record.nil? || ! record.respond_to?(:to_h)
69
+
70
+ @data << record.to_h
71
+ record[@id_fld]
72
+
73
+ rescue => e
74
+ handle_error(e)
75
+ end
76
+
77
+
78
+ ##
79
+ # ID is the first column you named in new()
80
+ #
81
+ def read(id)
82
+ raise(ArgumentError, "Read requires an ID") if id.nil?
83
+
84
+ rec = @data.find{|x| x[@id_fld] == id }
85
+ Octothorpe.new(rec)
86
+
87
+ rescue => e
88
+ handle_error(e)
89
+ end
90
+
91
+
92
+ ##
93
+ # ID is the first column you named in new()
94
+ # record should be a Hash or Octothorpe.
95
+ # Again, note that we don't care what columns you send us.
96
+ #
97
+ def update(id, record)
98
+ raise(ArgumentError, "Update requires an ID") if id.nil?
99
+
100
+ rec = @data.find{|x| x[@id_fld] == id }
101
+ raise Pod4::CantContinue, "No record found with ID '#{id}'" unless rec
102
+
103
+ rec.merge!(record.to_h)
104
+ self
105
+ rescue => e
106
+ handle_error(e)
107
+ end
108
+
109
+
110
+ ##
111
+ # ID is that first column
112
+ #
113
+ def delete(id)
114
+ raise(ArgumentError, "Delete requires an ID") if id.nil?
115
+
116
+ raise Pod4::CantContinue, "'No record found with ID '#{id}'" \
117
+ if read(id).empty?
118
+
119
+ @data.delete_if {|r| r[@id_fld] == id }
120
+ self
121
+ rescue => e
122
+ handle_error(e)
123
+ end
124
+
125
+
126
+ protected
127
+
128
+
129
+ def handle_error(err, kaller=nil)
130
+ kaller ||= caller[1..-1]
131
+
132
+ Pod4.logger.error(__FILE__){ err.message }
133
+
134
+ case err
135
+ when ArgumentError, Pod4Error, Pod4::CantContinue
136
+ raise err.class, err.message, kaller
137
+ else
138
+ raise Pod4::Pod4Error, err.message, kaller
139
+ end
140
+
141
+ end
142
+
143
+
144
+ end
145
+
146
+
147
+ end
148
+
data/lib/pod4/param.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'octothorpe'
2
+
3
+
4
+ module Pod4
5
+
6
+
7
+ ##
8
+ # This module implements the singleton pattern and is used internally to
9
+ # store parameters passed to it from outside of Pod4
10
+ #
11
+ module Param
12
+ extend self
13
+
14
+
15
+ def params; @params ||= {}; end
16
+
17
+ def set(p,v); params[p.to_s.to_sym] = v; end
18
+
19
+ def get(p); params[p.to_s.to_sym]; end
20
+
21
+ def get_all; Octothorpe.new(params.dup); end
22
+
23
+ def reset; @params = {}; end
24
+
25
+
26
+ end
27
+
28
+ end
29
+