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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc0ac6c69b4368ab5869b6ce236e1929d83fb743
|
4
|
+
data.tar.gz: 2e7a203ae10b29cc7aaac3fd743851673d494a31
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: aecfa93561a918dffe716767f8fd2d60c08e64107ec5d3ddfce165af22efa9c3177e44812d58fb7ab0815febcea2a3bdb642a58167f0b1838554d70fc66ef0f9
|
7
|
+
data.tar.gz: 5be87522d0cd6726bbb2f3427c6923c19b69f0f2f35fd782511cdead8216f86e9e9ddb359fff9cb43b2c7bd38aabf2bdf18eafce19653162edf8eff64bf5a884
|
data/.hgignore
ADDED
data/.hgtags
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
04cffd78b3e0930c939f44a989269064d7d03215 0.0.1
|
2
|
+
287d2bb72137613beb6d823121665c7a1ced7bb1 0.0.02
|
3
|
+
8addfcadc2235aa7af8038c704fac2c17f73116a 0.1.1
|
4
|
+
33b88651c62b76862573eb4293b4e70db81a4b0c 0.1.2
|
5
|
+
e9204609efd7d401bdaa7f8a968f8584d70e0b48 0.1.3
|
6
|
+
24c3a9549ca5df4d6600ff68fec425d6ad1260b6 0.2.0
|
7
|
+
24c3a9549ca5df4d6600ff68fec425d6ad1260b6 0.2.0
|
8
|
+
c57a3e107c858f0e24d0c9ba11d411d405605b73 0.2.0
|
9
|
+
eecb1a60c825da7d4ff939dc1f170a1d37c0b32b 0.2.1
|
10
|
+
cd5e8689ef022710507836f94b3813bacba6a142 0.2.2
|
11
|
+
06be9d910c37970a239a192676aab6324d0d6657 0.2.3
|
12
|
+
e738477c1e40988c0a6b6b38443a1981f00612a3 0.3.0
|
13
|
+
c6b6c49f3ab363771da53cb3cdbd31e284332721 0.3.1
|
14
|
+
885291d30186092f87c62fd0225dec6fcdab2dd2 0.4.0
|
15
|
+
cea39858d97318fd8a7e32b41de7f259e367b920 0.4.1
|
16
|
+
c2743ca54c4b48c6ebe60b948d251b8d2978c90d 0.5.3
|
17
|
+
26738ea02d5384967320ef41fc743a009faf70b8 0.6.0
|
18
|
+
dc22541876279b47dd366ddb44d262775ccdb933 0.6.1
|
19
|
+
4a0e392be3499d9edc9cde66dd3b8d37136a0816 0.6.2
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Andrew Jones
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
10
|
+
so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,556 @@
|
|
1
|
+
Introduction
|
2
|
+
============
|
3
|
+
|
4
|
+
Pod4 is not an ORM. No, really it isn't. Because that would be ridiculous.
|
5
|
+
|
6
|
+
...okay, it kind of acts a _bit_ like an ORM...
|
7
|
+
|
8
|
+
Ah, well. Judge for yourself. Here's a model:
|
9
|
+
|
10
|
+
class CustomerModel < Pod4::Model
|
11
|
+
|
12
|
+
class CustomerInterface < Pod4::PgInterface
|
13
|
+
set_table :customer
|
14
|
+
set_id_fld :id
|
15
|
+
end
|
16
|
+
|
17
|
+
set_interface CustomerInterface.new(CONN_STRING)
|
18
|
+
|
19
|
+
attr_columns :cust_code, :name, :group
|
20
|
+
end
|
21
|
+
|
22
|
+
And here's a method that uses the model:
|
23
|
+
|
24
|
+
def change_customer_group(id, group)
|
25
|
+
customer = Customer.new(id).read.or_die
|
26
|
+
customer.group = group
|
27
|
+
customer.update.or_die
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
Seriously now
|
32
|
+
-------------
|
33
|
+
|
34
|
+
Pod4 is a very simple set of classes that sits on top of some other library
|
35
|
+
which gives access to data -- for example, pg, tds, or Sequel (which _is_ an
|
36
|
+
ORM...) It's relatively easy to get it to talk to a new sort of data access
|
37
|
+
library, and you're not limited to databases.
|
38
|
+
|
39
|
+
It provides a simple, common framework to talk to all these data sources, using
|
40
|
+
model classes which (to my mind at least) are clean, easy to understand and
|
41
|
+
maintain, using a bare minimum of DSL and vanilla Ruby inheritance.
|
42
|
+
|
43
|
+
This is the central motivation behind the project -- to provide a mechanism
|
44
|
+
that allows for model classes which actually represent your data to the rest of
|
45
|
+
your code in a way that you are fully in control of. Because it's your model
|
46
|
+
classes, not the database, which are the canonical representation of the data.
|
47
|
+
|
48
|
+
I don't want the people who maintain my code to have to know the differences
|
49
|
+
between ActiveRecord's `update` and `update_all`, or Sequel's `dataset[]` and
|
50
|
+
`dataset.where()`. Pod4::Model has a dozen or so methods you need to worry
|
51
|
+
about, and six of those are pretty much self-explanatory. Or, you can inherit
|
52
|
+
from Pod4::BasicModel instead, and do without even that.
|
53
|
+
|
54
|
+
I honestly don't think of it as an Object Relational Manager. I think of it as
|
55
|
+
a Way To Have Nice Models.
|
56
|
+
|
57
|
+
If you are looking for something with all the features of, say, ActiveRecord,
|
58
|
+
then this isn't for you. I provide basic access to and maintenance of records,
|
59
|
+
with validation. For anything more, you need to be willing to use a very well
|
60
|
+
established existing DSL within your model code -- SQL.
|
61
|
+
|
62
|
+
|
63
|
+
Thanks
|
64
|
+
======
|
65
|
+
|
66
|
+
This code was developed, by me, during working hours at [James Hall & Co.
|
67
|
+
Ltd](https://www.jameshall.co.uk/). I'm incredibly greatful that they have
|
68
|
+
permitted me to open-source it.
|
69
|
+
|
70
|
+
|
71
|
+
Installation
|
72
|
+
============
|
73
|
+
|
74
|
+
gem install pod4
|
75
|
+
|
76
|
+
Of course you will also need to install whatever other gems you need in order
|
77
|
+
to access the data you want Pod4 to see. Currently there are interfaces for:
|
78
|
+
|
79
|
+
* Sequel (which itself of course talks to all manner of databases)
|
80
|
+
* Tiny_tds
|
81
|
+
* Pg
|
82
|
+
* Nebulous (my own STOMP wrapper/protocol/what-have-you)
|
83
|
+
|
84
|
+
(But, you can add your own interfaces. It's not hard.)
|
85
|
+
|
86
|
+
|
87
|
+
A Short Tutorial
|
88
|
+
================
|
89
|
+
|
90
|
+
Pod4 uses my Octothorpe gem to pass information around. An Octothorpe is
|
91
|
+
basically a Hash, except the keys are always symbols, and it's read only.
|
92
|
+
|
93
|
+
But you don't really need to know that here. If you mentally substitute "Hash"
|
94
|
+
every time I say "Octothorpe", you'll be fine.
|
95
|
+
|
96
|
+
|
97
|
+
Model and Interface
|
98
|
+
-------------------
|
99
|
+
|
100
|
+
Note well that we distinguish between 'models' and 'interfaces':
|
101
|
+
|
102
|
+
The model represents the data to your application, in the format that makes
|
103
|
+
most sense to your application: that might be the same format that it is stored
|
104
|
+
in on the database, or it might not. The model doesn't care about where the
|
105
|
+
data comes from. Models are all subclasses of Pod4::Model (or Pod4::BasicModel,
|
106
|
+
but we'll leave that alone for now).
|
107
|
+
|
108
|
+
An interface encapsulates the connection to whatever is providing the data. It
|
109
|
+
might be a wrapper for calls to the Sequel ORM, for example. Or it could be a
|
110
|
+
making a series of calls to a set of Nebulous verbs. It only cares about
|
111
|
+
dealing with the data source, and it is only called by the model.
|
112
|
+
|
113
|
+
An interface is a seperate class, which is defined for each model. There are
|
114
|
+
parent classes for a number of the sources you will need, but failing that,
|
115
|
+
you can always create one from the ultimate parent, Pod4::Interface.
|
116
|
+
|
117
|
+
|
118
|
+
Simple Model Usage
|
119
|
+
------------------
|
120
|
+
|
121
|
+
# find record 14; raise error otherwise. Update and save.
|
122
|
+
x = ExampleModel.new(14).read.or_die
|
123
|
+
x.two = "new value"
|
124
|
+
x.update
|
125
|
+
|
126
|
+
# create a new record from the params hash -- unless validation fails.
|
127
|
+
y = ExampleModel.new
|
128
|
+
y.set(params)
|
129
|
+
y.create unless y.model_status == :error
|
130
|
+
|
131
|
+
A model is a class, each instance of which represents a single record. on that
|
132
|
+
instance you can call the following for basic operation:
|
133
|
+
|
134
|
+
* `create` -- tells the data source to store this new "record"
|
135
|
+
* `read` -- obtains the "record" from the data source
|
136
|
+
* `update` -- updates the "record" on the data source
|
137
|
+
* `delete` -- deletes the "record" on the data source.
|
138
|
+
* `set` -- set the column attributes of the object with a hash or Octothorpe
|
139
|
+
* `to_ot` -- output an Octothorpe of the object's column attributes
|
140
|
+
* `alerts` -- return an array of Alerts (which I'll explain later)
|
141
|
+
|
142
|
+
(Note that we say "record" not record. The data source might not be a database.
|
143
|
+
Your model instance might be represented on the data source as several records,
|
144
|
+
or something else entirely.)
|
145
|
+
|
146
|
+
There is one more operation - `list`. Call this on the model class itself,
|
147
|
+
and it will return an array of model instances that match the criteria you
|
148
|
+
pass. What you can pass to list depends on your model class (of course); by
|
149
|
+
default it also depends on the interface the model uses. But normally it should
|
150
|
+
except a hash, like so:
|
151
|
+
|
152
|
+
ExampleModel.list(:one => "a") #-> Array of ExampleModel where one = "a"
|
153
|
+
|
154
|
+
Additionally, you can chain `or_die` onto any model method to get it to raise
|
155
|
+
exceptions if something is wrong on the model. If you don't want exceptions,
|
156
|
+
you can check the model's model_status attribute, or just look at the alerts.
|
157
|
+
|
158
|
+
Those eight (nine) methods are _all_ the methods given by Pod4::Model that you
|
159
|
+
are normally going to want to use, outside of the code actually inside your
|
160
|
+
model.
|
161
|
+
|
162
|
+
|
163
|
+
A Simple Model
|
164
|
+
--------------
|
165
|
+
|
166
|
+
Here is the model and interface definition that goes with the above example:
|
167
|
+
|
168
|
+
require 'pod4'
|
169
|
+
require 'pod4/pg_interface'
|
170
|
+
|
171
|
+
class ExampleModel < Pod4::Model
|
172
|
+
|
173
|
+
class ExampleInterface < Pod4::PgInterface
|
174
|
+
set_table :example
|
175
|
+
set_id_fld :id
|
176
|
+
end
|
177
|
+
|
178
|
+
set_interface ExampleInterface.new($pg_conn)
|
179
|
+
attr_columns :one, :two, :three
|
180
|
+
end
|
181
|
+
|
182
|
+
In this example we have a model that relies on the Pg gem to talk to a
|
183
|
+
table 'example'. The table has a primary key field 'id' and columns which
|
184
|
+
correspond to our three attributes one, two and three. There is no validation
|
185
|
+
or error control.
|
186
|
+
|
187
|
+
Note that we have to require pg_interface seperately. I won't bother to show
|
188
|
+
this in any more model examples.
|
189
|
+
|
190
|
+
### Interface ###
|
191
|
+
|
192
|
+
Let's start with the interface definition. Remember, the interface class is
|
193
|
+
only there to represent the data source to the model. Yours will most likely be
|
194
|
+
no more complex than the one above. Since they are only accessed by the model,
|
195
|
+
my preference is to define them in an internal class, but if that makes you
|
196
|
+
back away slowly waving your hands placatingly, put it in another file. Pod4 is
|
197
|
+
fine with that.
|
198
|
+
|
199
|
+
Inside your interface class you must call some DSLish methods to tell the
|
200
|
+
interface how to talk to the data. What they are depends on the interface, but
|
201
|
+
the ones for PgInterface are pretty common:
|
202
|
+
|
203
|
+
* `set_schema` -- optional -- the name of the schema to find the table in
|
204
|
+
* `set_table` -- mandatory -- the name of the database table to use
|
205
|
+
* `set_id_fld` -- mandatory -- the name of the column that makes the record unique
|
206
|
+
|
207
|
+
Actually, _every_ interface defines `set_id_fld`. Instances of a model _must_ be
|
208
|
+
represented by a single ID field that provides a unique identifier. Pod4 does
|
209
|
+
not care what it's called or what data type it is -- if you say that's what
|
210
|
+
makes it unique, that's good enough.
|
211
|
+
|
212
|
+
Internally, Interfaces talk the same basic language of list / create / read /
|
213
|
+
update / delete that models do. But I'm not finding the need to subclass these
|
214
|
+
much. So that's probably going to be it for your Interface definition.
|
215
|
+
|
216
|
+
### Model ###
|
217
|
+
|
218
|
+
Models have two of their own DSLish methods:
|
219
|
+
|
220
|
+
* `set_interface` -- here is where you instantiate your Interface class
|
221
|
+
* `attr_columns` -- like `attr_accessor`, but letting the model know to care.
|
222
|
+
|
223
|
+
You can see that interfaces are instantiated when the model is required.
|
224
|
+
Exactly what you need to pass to the interface to instantiate it depends on the
|
225
|
+
interface. SequelInterface wants the Sequel DB object (which means you have to
|
226
|
+
require sequel, connect, and *then* require your models); the other interfaces
|
227
|
+
only want connection hashes.
|
228
|
+
|
229
|
+
Any attributes you define using `attr_columns` are treated specially by
|
230
|
+
Pod4::Model. You get all the effect of the standard Ruby `attr_accessor` call,
|
231
|
+
but in addition, the attribute will be passed to and from the interface, and to
|
232
|
+
and from your external code, by the standard model methods.
|
233
|
+
|
234
|
+
In addition to the ones above, we have:
|
235
|
+
|
236
|
+
* `validate` -- override this to provide validation
|
237
|
+
* `map_to_model` -- controls how the interface sets attributes on the model
|
238
|
+
* `map_to_interface` -- controls how the model sends data to the interface
|
239
|
+
* `add_alert` -- adds an alert to the model
|
240
|
+
|
241
|
+
A model also has some built-in attributes of its own:
|
242
|
+
|
243
|
+
* `model_id` -- this is the value of the ID column you set in the interface.
|
244
|
+
* `model_status` -- one of :error :warning :okay :deleted :empty
|
245
|
+
|
246
|
+
We'll deal with all these below.
|
247
|
+
|
248
|
+
|
249
|
+
Adding Validation
|
250
|
+
-----------------
|
251
|
+
|
252
|
+
Built into the model is an array of alerts (Pod4::Alert) which are messages
|
253
|
+
that have been raised against the instance of the model class. Each alert can
|
254
|
+
have a status of :error, :warning, :info or :success. If any alert has a status
|
255
|
+
of :error :warning or :success then that is reflected in the model's
|
256
|
+
`model_status` attribute.
|
257
|
+
|
258
|
+
(As to those other two possible statuses -- models are :empty when first created
|
259
|
+
and :deleted after a call to delete.)
|
260
|
+
|
261
|
+
You can raise alerts yourself, and you normally do so by overriding `validate`.
|
262
|
+
This method is called after a read, and before an update create or delete, so
|
263
|
+
that every model instance should have a model_status reflecting its
|
264
|
+
"correctness" regardless of whether it came from the data source or your
|
265
|
+
application.
|
266
|
+
|
267
|
+
Here's a model with some validation:
|
268
|
+
|
269
|
+
class Customer < Pod4::Model
|
270
|
+
|
271
|
+
class CustomerInterface < Pod4::PgInterface
|
272
|
+
set_schema :pod4example
|
273
|
+
set_table :customer
|
274
|
+
set_id_fld :id
|
275
|
+
end
|
276
|
+
|
277
|
+
set_interface CustomerInterface.new($pg_conn)
|
278
|
+
attr_columns :cust_code, :name, :group
|
279
|
+
|
280
|
+
def validate
|
281
|
+
super
|
282
|
+
|
283
|
+
add_alert(:error, :name, "Name cannot be empty") \
|
284
|
+
unless @name && @name =~ \^\s*$\
|
285
|
+
|
286
|
+
add_alert(:error, :cust_code, "invalid customer code") \
|
287
|
+
unless @cust_code && @cust_code.length == 6
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
(Note: as a general principal, you should always call super when overriding a
|
294
|
+
method in Pod4 model, unless you have good reason not to.)
|
295
|
+
|
296
|
+
If the model has a status of :error, then an update or create will fail. A
|
297
|
+
delete, however, will succeed -- if you want to create validation that aborts a
|
298
|
+
delete operation, you should override the `delete` method and only call super
|
299
|
+
if the validation passes.
|
300
|
+
|
301
|
+
Also, because validation is not called on `set`, it's entirely possible to set
|
302
|
+
a model to an invalid state and not raise any alerts against it until you go to
|
303
|
+
commit to the database. If you want to change the state of the model and then
|
304
|
+
validate it before that, you must call `validate` by hand.
|
305
|
+
|
306
|
+
|
307
|
+
Changing How a Model Represents Data
|
308
|
+
------------------------------------
|
309
|
+
|
310
|
+
Pod4 will do the basic work for you when it comes to data types. integers,
|
311
|
+
decimals, dates and datatimes should all end up as the right type in the model.
|
312
|
+
(It depends on the Interface. You're going to get tired of me saying that,
|
313
|
+
aren't you?) But maybe you want more than that.
|
314
|
+
|
315
|
+
Let's imagine you have a database table in PostreSQL with a column called cost
|
316
|
+
that uses the money type. And you want it to be a `BigDecimal` in the model.
|
317
|
+
Well, Pod4 won't do that for you -- for all I know someone might have a problem
|
318
|
+
with my requiring BigDecimal -- but it's not hard to do yourself.
|
319
|
+
|
320
|
+
class Product < Pod4::Model
|
321
|
+
|
322
|
+
class ProductInterface < Pod4::PgInterface
|
323
|
+
set_schema :pod4example
|
324
|
+
set_table :product
|
325
|
+
set_id_fld :product_id
|
326
|
+
end
|
327
|
+
|
328
|
+
set_interface ProductInterface.new($pg_conn)
|
329
|
+
attr_columns :description, :cost
|
330
|
+
|
331
|
+
def map_to_model(ot)
|
332
|
+
super
|
333
|
+
@cost = Bigdecimal.new(@cost)
|
334
|
+
end
|
335
|
+
|
336
|
+
def map_to_interface
|
337
|
+
super.merge(cost: @cost.to_f)
|
338
|
+
end
|
339
|
+
|
340
|
+
end
|
341
|
+
|
342
|
+
`map_to_model` gets called when the model wants to write data from the interface
|
343
|
+
on the model; it takes an Octothorpe from the interface as a parameter. By
|
344
|
+
default it behaves as `set` does.
|
345
|
+
|
346
|
+
`map_to_interface` is the opposite: it gets called when the model wants to write
|
347
|
+
data on the interface from the model. It _returns_ an Octothorpe to the
|
348
|
+
interface. By default it behaves as `to_ot` does. (Since OTs are read only, you
|
349
|
+
must modify it using merge.)
|
350
|
+
|
351
|
+
By the way: sometimes I need to validate on the data before I convert it. It's
|
352
|
+
fine to put a call to `add_alert` in `map_to_model`.
|
353
|
+
|
354
|
+
|
355
|
+
Relations
|
356
|
+
---------
|
357
|
+
|
358
|
+
Pod4 does not provide relations. But, I'm not sure that it needs to. Look:
|
359
|
+
|
360
|
+
class BlogPost < Pod4::Model
|
361
|
+
|
362
|
+
class BlogPostInterface < Pod4::PgInterface
|
363
|
+
set_table :blogpost
|
364
|
+
set_id_fld :id
|
365
|
+
end
|
366
|
+
|
367
|
+
set_interface BlogPostInterface.new($conn)
|
368
|
+
attr_columns :text
|
369
|
+
|
370
|
+
def comments; Comment.list(post: @id); end
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
class Comment < Pod4::Model
|
375
|
+
|
376
|
+
class COmmentInterface < Pod4::PgInterface
|
377
|
+
set_table :comment
|
378
|
+
set_id_fld :id
|
379
|
+
end
|
380
|
+
|
381
|
+
set_interface CommentInterface.new($conn)
|
382
|
+
attr_columns :post_id, :text
|
383
|
+
|
384
|
+
def blog_post; BlogPost.new(@post_id).read.or_die; end
|
385
|
+
end
|
386
|
+
|
387
|
+
So the BlogPost model has a comments method that returns an array of Comments,
|
388
|
+
and the Comments model has a blog_post method that returns the BlogPost. (You
|
389
|
+
would probably want to add validation to enforce relational integrity.)
|
390
|
+
|
391
|
+
Is this approach inefficient? Possibly. But if you don't like it, you can
|
392
|
+
always try:
|
393
|
+
|
394
|
+
|
395
|
+
Beyond CRUD (& List)
|
396
|
+
--------------------
|
397
|
+
|
398
|
+
Sooner or later you will want to do something more than Pod4::Model will give
|
399
|
+
you automatically. There is a perfectly well documented, very popular DSL with
|
400
|
+
lots of examples to solve this problem. It's called SQL.
|
401
|
+
|
402
|
+
If your interface is connected to a SQL database, it should provide two more
|
403
|
+
methods: `execute` and `select`.
|
404
|
+
|
405
|
+
class BlogPost < Pod4::Model
|
406
|
+
|
407
|
+
class BlogPostInterface < Pod4::PgInterface
|
408
|
+
set_table :blogpost
|
409
|
+
set_id_fld :id
|
410
|
+
end
|
411
|
+
|
412
|
+
set_interface BlogPostInterface.new($conn)
|
413
|
+
attr_columns :text
|
414
|
+
|
415
|
+
|
416
|
+
##
|
417
|
+
# return an array of hashes where each comment has the post joined to it
|
418
|
+
#
|
419
|
+
def post_and_comments
|
420
|
+
interface.select( %Q|select *
|
421
|
+
from blogpost b
|
422
|
+
join comments c on(c.post_id = b.id);| )
|
423
|
+
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
##
|
428
|
+
# delete all comments on this post
|
429
|
+
#
|
430
|
+
def delete_comments
|
431
|
+
interface.execute(
|
432
|
+
%Q|delete from comments where post_id = #{@model_id};| )
|
433
|
+
|
434
|
+
end
|
435
|
+
|
436
|
+
end
|
437
|
+
|
438
|
+
Neither `execute` nor `select` care about the table or ID field you passed to the
|
439
|
+
interface. They only run pure SQL. The only difference between them is that
|
440
|
+
select expects to return an array of results.
|
441
|
+
|
442
|
+
To my way of thinking, there is absolutely nothing wrong about using SQL in a
|
443
|
+
model. It will certainly need revisiting if you change database. But how often
|
444
|
+
does that happen, really? And if it ever does, you are likely to need to
|
445
|
+
revisit the effected models anyway...
|
446
|
+
|
447
|
+
|
448
|
+
BasicModel
|
449
|
+
----------
|
450
|
+
|
451
|
+
Sometimes your model needs to represent data in a way which is so radically
|
452
|
+
different from the data source that the whole list, create, read, update,
|
453
|
+
delete thing that Pod4::Model gives you is no use. Enter Pod4::BasicModel.
|
454
|
+
|
455
|
+
A real world example: at James Hall my intranet system has a User model, where
|
456
|
+
each attribute is a parameter that controls how the system behaves for that user
|
457
|
+
-- email address, security settings, etc. Having one object to represent the
|
458
|
+
user is the most sensible thing.
|
459
|
+
|
460
|
+
But I don't want to have to add a column to the database each time I change the
|
461
|
+
intranet system and add a user parameter. The logical place to change the
|
462
|
+
parameter is in the User model, not in the database, and certainly not both. So
|
463
|
+
on the database, I have a settings table where the key runs: userid, setting
|
464
|
+
name.
|
465
|
+
|
466
|
+
Pod4::BasicModel gives you:
|
467
|
+
|
468
|
+
* `set_interface`
|
469
|
+
* the `model_id`, `model_status` and `alerts` attributes
|
470
|
+
* `add_alert`
|
471
|
+
|
472
|
+
...and nothing else. But that's enough to make a model, your way, using the
|
473
|
+
methods on the interface. These are the same CRUDL methods that Pod4::Model
|
474
|
+
provides -- except that the CRUD methods take a record id as a key.
|
475
|
+
|
476
|
+
Here's a simplified version of my User model. This one is read only, but it's
|
477
|
+
hopefully enough to get the idea:
|
478
|
+
|
479
|
+
class User < Pod4::BasicModel
|
480
|
+
|
481
|
+
class UserInterface < ::Pod4::SequelInterface
|
482
|
+
set_table :settings
|
483
|
+
set_id_fld :id
|
484
|
+
end
|
485
|
+
|
486
|
+
|
487
|
+
# Here we set what settings always exist for a user
|
488
|
+
Setting = Struct.new(:setName, :default)
|
489
|
+
|
490
|
+
DefaultSettings = [ Setting.new( :depot, nil ),
|
491
|
+
Setting.new( :store, nil ),
|
492
|
+
Setting.new( :menu, nil ),
|
493
|
+
Setting.new( :roles, '' ),
|
494
|
+
Setting.new( :name, '' ),
|
495
|
+
Setting.new( :nick, '' ),
|
496
|
+
Setting.new( :mail, nil ) ]
|
497
|
+
|
498
|
+
set_interface UserInterface.new($db)
|
499
|
+
attr_reader :userid, :depot, :store, :menu, :roles, :name, :nick, :mail
|
500
|
+
|
501
|
+
|
502
|
+
class << self
|
503
|
+
|
504
|
+
def keys; DefaultSettings.map{|x| x.setName }; end
|
505
|
+
|
506
|
+
def list
|
507
|
+
array = interface.select(%Q|select distinct userid from settings;|)
|
508
|
+
array.map {|r| self.new( r[:userid] ).read }
|
509
|
+
end
|
510
|
+
|
511
|
+
end
|
512
|
+
##
|
513
|
+
|
514
|
+
|
515
|
+
def initialize(userid=nil)
|
516
|
+
super(userid)
|
517
|
+
|
518
|
+
self.class.keys.each do |key|
|
519
|
+
instance_variable_set( "@#{key}".to_sym, nil )
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
|
524
|
+
def read
|
525
|
+
lst = interface.list(userid: @model_id)
|
526
|
+
|
527
|
+
data = lst.each_with_object({}) do |ot,h|
|
528
|
+
h[ot.>>.setname] = ot.>>.setvalue
|
529
|
+
end
|
530
|
+
|
531
|
+
@userid = @model_id
|
532
|
+
set_merge( Octothorpe.new(data) )
|
533
|
+
validate; @model_status = :okay unless @model_status != :empty
|
534
|
+
|
535
|
+
self
|
536
|
+
end
|
537
|
+
|
538
|
+
|
539
|
+
def to_ot
|
540
|
+
hash = self.class.keys.each_with_object({}) do |k,m|
|
541
|
+
m[k] = instance_variable_get("@#{k}".to_sym)
|
542
|
+
end
|
543
|
+
|
544
|
+
Octothorpe.new(hash)
|
545
|
+
end
|
546
|
+
|
547
|
+
|
548
|
+
def set_merge(hash)
|
549
|
+
self.class.keys.each do |key|
|
550
|
+
value = hash[key]
|
551
|
+
instance_variable_set( "@#{key}".to_sym, value ) if value
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
end
|
556
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require 'rdoc/task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
namespace :rdoc do
|
8
|
+
RDoc::Task.new do |rdoc|
|
9
|
+
rdoc.main = "README.md"
|
10
|
+
rdoc.rdoc_files.include("*.md", "lib/*", "md/*")
|
11
|
+
rdoc.rdoc_dir = "doc"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Push doc to HARS"
|
15
|
+
task :hars do
|
16
|
+
sh "rsync -aP --delete doc/ /home/hars/hars/public/pod4"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Start Guard"
|
22
|
+
task :guard do
|
23
|
+
sh "bundle exec guard"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Update vim tag data"
|
27
|
+
task :retag do
|
28
|
+
sh "ripper-tags -R"
|
29
|
+
end
|
30
|
+
|