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.
- checksums.yaml +7 -0
- data/.hgignore +18 -0
- data/.hgtags +19 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +556 -0
- data/Rakefile +30 -0
- data/lib/pod4/alert.rb +87 -0
- data/lib/pod4/basic_model.rb +137 -0
- data/lib/pod4/errors.rb +80 -0
- data/lib/pod4/interface.rb +110 -0
- data/lib/pod4/metaxing.rb +66 -0
- data/lib/pod4/model.rb +347 -0
- data/lib/pod4/nebulous_interface.rb +408 -0
- data/lib/pod4/null_interface.rb +148 -0
- data/lib/pod4/param.rb +29 -0
- data/lib/pod4/pg_interface.rb +460 -0
- data/lib/pod4/sequel_interface.rb +303 -0
- data/lib/pod4/tds_interface.rb +394 -0
- data/lib/pod4/version.rb +3 -0
- data/lib/pod4.rb +54 -0
- data/md/fixme.md +32 -0
- data/md/roadmap.md +69 -0
- data/pod4.gemspec +49 -0
- data/spec/README.md +19 -0
- data/spec/alert_spec.rb +173 -0
- data/spec/basic_model_spec.rb +220 -0
- data/spec/doc_no_pending.rb +5 -0
- data/spec/fixtures/database.rb +13 -0
- data/spec/model_spec.rb +760 -0
- data/spec/nebulous_interface_spec.rb +286 -0
- data/spec/null_interface_spec.rb +153 -0
- data/spec/param_spec.rb +89 -0
- data/spec/pg_interface_spec.rb +452 -0
- data/spec/pod4_spec.rb +88 -0
- data/spec/sequel_interface_spec.rb +466 -0
- data/spec/shared_examples_for_interface.rb +160 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/tds_interface_spec.rb +494 -0
- data/tags +106 -0
- metadata +316 -0
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
|
+
|