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.
data/lib/pod4/model.rb ADDED
@@ -0,0 +1,347 @@
1
+ require 'octothorpe'
2
+
3
+ require_relative 'basic_model'
4
+ require_relative 'errors'
5
+ require_relative 'alert'
6
+
7
+
8
+ module Pod4
9
+
10
+
11
+ ##
12
+ # The parent of all models.
13
+ #
14
+ # Models & Interfaces
15
+ # -------------------
16
+ #
17
+ # Note that we distinguish between 'models' and 'interfaces':
18
+ #
19
+ # The model represents the data to your application, in the format that makes
20
+ # most sense to your application: that might be the same format that it is
21
+ # stored in on the database, or it might not. The model doesn't care about
22
+ # where the data comes from. Models are all subclasses of Pod4::Model.
23
+ #
24
+ # An interface encapsulates the connection to whatever is providing the data.
25
+ # it might be a wrapper for calls to the Sequel ORM, for example. Or it could
26
+ # be a making a series of calls to a set of Nebulous verbs. It only cares
27
+ # about dealing with the data source, and it is only called by the model.
28
+ #
29
+ # An interface is a seperate class, which is defined for each model. There
30
+ # are parent classes for most of the data sources you will need, but failing
31
+ # that, you can always create one from the ultimate parent, Pod4::Interface.
32
+ #
33
+ # Simple Example
34
+ # ---------------
35
+ #
36
+ # The most basic example model (and interface):
37
+ #
38
+ # class ExampleModel < Pod4::Model
39
+ #
40
+ # class ExampleInterface < Pod4::SequelInterface
41
+ # set_table :example
42
+ # set_id_fld :id
43
+ # end
44
+ #
45
+ # set_interface ExampleInterface.new($db)
46
+ # attr_columns :one, :two, :three
47
+ # end
48
+ #
49
+ # In this example we have a model that relies on the Sequel ORM to talk to a
50
+ # table 'example'. The table has a primary key field 'id' and columns which
51
+ # correspond to our three attributes one, two and three. There is no
52
+ # validation or error control.
53
+ #
54
+ # Here is an example of this model in use:
55
+ #
56
+ # # find record 14; raise error otherwise. Update and save.
57
+ # x = ExampleModel.new(14).read.or_die
58
+ # x.two = "new value"
59
+ # x.update
60
+ #
61
+ # # create a new record from the params hash -- unless validation fails.
62
+ # y = ExampleModel.new
63
+ # y.set(params)
64
+ # y.create unless y.model_status == :error
65
+ #
66
+ # Overriding Column Representation
67
+ # --------------------------------
68
+ #
69
+ # If you want to represent information differently on the model than it is
70
+ # stored on the data source, there are four methods you potentially need to
71
+ # know about and override:
72
+ #
73
+ # * set -- used by you to set model column values
74
+ # * to_ot -- used by you to get model column values
75
+ # * map_to_model -- used by the model to set column values from the interface
76
+ # * map_to_interface -- used by the model to set interface values
77
+ #
78
+ # See the methods themselves for more detail.
79
+ #
80
+ class Model < Pod4::BasicModel
81
+
82
+ class << self
83
+
84
+ ##
85
+ # You should call this in your model definition to define model 'columns'
86
+ # -- it gives you exactly the functionality of `attr_accessor` but also
87
+ # registers the attribute as one that `to_ot`, `map_to_model` and
88
+ # `map_to_interface` will try to help you with.
89
+ #
90
+ def attr_columns(*cols)
91
+ c = columns.dup
92
+ c += cols
93
+ define_class_method(:columns) {c}
94
+ attr_accessor *cols
95
+ end
96
+
97
+
98
+ ##
99
+ # Returns the list of columns from attr_columns
100
+ #
101
+ def columns
102
+ []
103
+ end
104
+
105
+
106
+ ##
107
+ # Call this to return an array of record information.
108
+ #
109
+ # What you actually get depends on the interface, but it must include a
110
+ # recognisable record ID in each array element.
111
+ #
112
+ # For the purposes of Model we assume that we can make an instance out of
113
+ # each array element, and we return an array of instances of the model.
114
+ # Override this method if that is not true for your Interface.
115
+ #
116
+ # Note that list should ALWAYS return an array, and array elements should
117
+ # always respond to :id -- otherwise we raise a Pod4Error.
118
+ #
119
+ def list(params=nil)
120
+ fail_no_id_fld unless interface.id_fld
121
+
122
+ interface.list(params).map do |ot|
123
+ key = ot[interface.id_fld]; fail_no_id unless key
124
+
125
+ rec = self.new(key)
126
+
127
+ rec.map_to_model(ot) # seperately, in case model forgot to return self
128
+ rec
129
+ end
130
+
131
+ end
132
+
133
+ end
134
+ ##
135
+
136
+
137
+ ##
138
+ # Syntactic sugar; pretty much the same as self.class.columns, which
139
+ # returns the `attr_columns` array.
140
+ #
141
+ def columns; self.class.columns.dup; end
142
+
143
+
144
+ ##
145
+ # Call this to write a new record to the data source.
146
+ # Note: create needs to set @id. But interface.create should return it, so
147
+ # that's okay.
148
+ #
149
+ def create
150
+ validate
151
+ @model_id = interface.create(map_to_interface) \
152
+ unless @model_status == :error
153
+
154
+ @model_status = :okay if @model_status == :empty
155
+ self
156
+ end
157
+
158
+
159
+ ##
160
+ # Call this to fetch the data for this instance from the data source
161
+ #
162
+ def read
163
+ r = interface.read(@model_id)
164
+
165
+ if r.empty?
166
+ add_alert(:error, "Record ID '#@model_id' not found on the data source")
167
+ else
168
+ map_to_model(r)
169
+ @model_status = :okay if @model_status == :empty
170
+ end
171
+
172
+ self
173
+ end
174
+
175
+
176
+ ##
177
+ # Call this to update the data source with the current attribute values
178
+ #
179
+ def update
180
+ fail_invalid_status(:update) if [:empty, :deleted].include? @model_status
181
+
182
+ clear_alerts; validate
183
+ interface.update(@model_id, map_to_interface) \
184
+ unless @model_status == :error
185
+
186
+ self
187
+ end
188
+
189
+
190
+ ##
191
+ # Call this to delete the record on the data source.
192
+ #
193
+ # Note: does not delete the instance...
194
+ #
195
+ def delete
196
+ fail_invalid_status(:delete) if [:empty, :deleted].include? @model_status
197
+
198
+ clear_alerts; validate
199
+ interface.delete(@model_id)
200
+ @model_status = :deleted
201
+ self
202
+ end
203
+
204
+
205
+ ##
206
+ # Call this to validate the model.
207
+ #
208
+ # Override this to add validation - calling `add_alert` for each problem.
209
+ #
210
+ # Note that you can only validate what is actually stored on the model. If
211
+ # you want to check the data being passed to the model in `set`, you need
212
+ # to override that routine.
213
+ #
214
+ # Also, you don't have any way of telling whether you are currently
215
+ # creating a new record or updating an old one: override `create` and
216
+ # `update` respectively.
217
+ #
218
+ def validate
219
+ # Holding pattern. All models should use super, in principal
220
+ end
221
+
222
+
223
+ ##
224
+ # Set instance values on the model from a Hash or Octothorpe.
225
+ #
226
+ # This is what your code calls when it wants to update the model. Override
227
+ # it if you need it to set anything not in attr_columns, or to
228
+ # control data types, etc.
229
+ #
230
+ # You might want to put validation here, too, if what you are validating is
231
+ # something that isn't actually stored on the model. You can call add_alert
232
+ # from here just fine.
233
+ #
234
+ # See also: `to_ot`, `map_to_model`, `map_to_interface`
235
+ #
236
+ def set(ot)
237
+ merge(ot)
238
+ self
239
+ end
240
+
241
+
242
+ ##
243
+ # Return an Octothorpe of all the attr_columns attributes.
244
+ #
245
+ # Override if you want to return any extra data. (You will need to create a
246
+ # new Octothorpe.)
247
+ #
248
+ # See also: `set`, `map_to_model', 'map_to_interface'
249
+ #
250
+ def to_ot
251
+ Octothorpe.new(to_h)
252
+ end
253
+
254
+
255
+ ##
256
+ # Used by the interface to set the column values on the model.
257
+ #
258
+ # Don't use this to set model attributes from your code; use `set`,
259
+ # instead.
260
+ #
261
+ # By default this does exactly the same as `set`. Override it if you want
262
+ # the model to represent data differently than the data source does --
263
+ # but then you will have to override `map_to_interface`, too, to convert
264
+ # the data back.
265
+ #
266
+ # See also: `to_ot`, `map_to_model'
267
+ #
268
+ def map_to_model(ot)
269
+ merge(ot)
270
+ validate
271
+ self
272
+ end
273
+
274
+
275
+ ##
276
+ # used by the model to get an OT of column values for the interface.
277
+ #
278
+ # Don't use this to get model values in your code; use `to_ot`, instead.
279
+ # This is called by model.create and model.update when it needs to write to
280
+ # the data source.
281
+ #
282
+ # By default this behaves exactly the same as to_ot. Override it if you
283
+ # want the model to represent data differently than the data source -- in
284
+ # which case you also need to override `map_to_model`.
285
+ #
286
+ # Bear in mind that any attribute could be nil, and likely will be when
287
+ # `map_to_interface` is called from the create method.
288
+ #
289
+ # See also: `to_ot`, `set`.
290
+ #
291
+ def map_to_interface
292
+ Octothorpe.new(to_h)
293
+ end
294
+
295
+
296
+ private
297
+
298
+
299
+ ##
300
+ # Output a hash of the columns
301
+ #
302
+ def to_h
303
+ columns.each_with_object({}) do |col, hash|
304
+ hash[col] = instance_variable_get("@#{col}".to_sym)
305
+ end
306
+ end
307
+
308
+
309
+ ##
310
+ # Merge an OT with our columns
311
+ #
312
+ def merge(ot)
313
+ test_for_octo(ot)
314
+
315
+ columns.each do |col|
316
+ instance_variable_set("@#{col}".to_sym, ot[col]) if ot.has_key?(col)
317
+ end
318
+ end
319
+
320
+
321
+ def test_for_octo(param)
322
+ raise ArgumentError, 'Parameter must be a Hash or Octothorpe' \
323
+ unless param.kind_of?(Hash) || param.kind_of?(Octothorpe)
324
+
325
+ end
326
+
327
+
328
+ def fail_no_id_fld
329
+ raise POd4Error, "No ID field defined in interface"
330
+ end
331
+
332
+
333
+ def fail_no_id
334
+ raise Pod4Error, "ID field missing from record"
335
+ end
336
+
337
+
338
+ def fail_invalid_status(action)
339
+ raise Pod4Error, "Invalid model status for an action of #{action}"
340
+ end
341
+
342
+ end
343
+ ##
344
+
345
+
346
+ end
347
+