forked from harrikauhanen/hyperactiveresource
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathREADME
451 lines (287 loc) · 15.4 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
= HyperactiveResource
v0.2
Many have said that Active Resource is not really "complete". On the surface, this means that many standard Active Record features are not implemented.
This makes the concept of swapping Active Record for Active Resource immensely difficult (and cludgy, hand-built, buggy etc etc)
Arguably, a "complete" Active Resource would behave like Active Record or, as the rdoc for Activ eResource states "very similarly to Active Record".
Hyperactive Resource is an extension to ActiveResource::Base and goes a long way towards the goal of an Active Resource that behaves like Active Record.
It will slowly be updated with all the standard features of Active Record until (someday) it can be used almost interchangeably.
This code could indeed go directly into Active Resource - and a slow, drawn-out project to do this is currently underway (look out for some of these features arriving in Rails 3.0). This process understandably takes some time as some of the changes are fairly radical - and we don't want to go about upsetting existing systems.
Until then, this plugin serves as an alternative that can be used, under the proviso that anyone using it realises that it's still experimental and still under construction.
It's especially useful for people that don't want to wait until Rails is ready, but want to to start using the functionality right now.
HyRes is built for (and intended for use with) the latest stable release of Rails pre-3.0
== Features
These are the features that have been added to HyRes.
== Base functions
* update_attribute / update_attributes (actually exist now!)
* save! / update_attributes! / update_attribute! / create!
(raises HyperactiveResource::ResourceNotSaved on failure)
* ModelName.count (still experimental) - with optional finder-args
- override the default counter_path with your own (see example below)
* updated collection_path that allows suffix_options as well as
prefix_options = allows us to generate Rails-style named routes
* no explosion for delete_all/destroy_all on a 404
* Active Record-like attributes= (updates rather than replaces)
* Active Record-like #load that doesn't #dup attributes (stores direct reference)
* reload that does a full clear/fetch to reload (also clears associations
cache!)
== Callbacks
* Hooks for before_validate, before_save
* Callback chaining a la Active Record (still experimental - may have missed
some, but you can definitely use "validate :my_validation_method")
== Finders
* conditional finders eg
Widget.find(:all, :conditions => {:colour => 'blue'}, :limit => 5)
Depends on your API accepting the above filter fields and returning
something sensible. See "find" doco below for more info
* Dynamic finders: find_by_X, find_all_by_X, find_by_X, find_last_by_X
(relies on your API accepting filter fields. See "find" doco below for more info)
* Dynamic finders take any number of arguments using _and_: find_by_X_and_Y_and_Z
* Dynamic instantiators: find_or_create_by_X OR find_and_instantiate_by_X
(also takes any number of args)
* Dynamic finders/instantiators take ! eg: find_or_create_by_X! will throw
a ResourceNotValid if create fails
* no 404-explosion for collection-finders that don't return anything They
just return nil (just like Active Record). This includes finders for
associations eg User.posts will return nil if the user hasn't any.
=== Validations
* Client side validations (validates_uniqueness_of is still experimental!)
=== Associations
* Awareness of associations between resources: belongs_to, has_many, has_one & columns
* Patient.new.name returns nil instead of MethodMissing
* Patient.new.races returns [] instead of MethodMissing
* pat = Patient.new; pat.gender_id = 1; pat.gender #Will return find the gender obj
* Resources can be associated with records
* Records can be associated with records
* Supports saving resources that :include other resources via:
* Nested resource saving (creating a patient will create their associated addresses)
* Mapping associations ([:gender].id will serialize as :gender_id)
* Can fetch associations even with a nested route by using the ":nested"
option on the nested resource's class. This command automatically adds a
prefix-path, and will pre-populate the parent's id when you do an
association collection_fetch.
=== Other weird stuff
* raw_data(:format => X) - access the raw data sent by the remote
API (see explanation and use below)
== find and Your API
Find can be conditional (ie return only those resources that match your
given set of conditions)... ion the proviso that your API actually does
something sensible with any given set of filter_fields you pass in.
HyRes has been written with an assumption that your API will behave in a
Rails-like manner (eg will accept :conditions, :limit, :offset etc), but it
should not break if you use other filter-terms... though the "conditions"
key is assumed in several functions.
It's no longer necessary to pass in the extra "params" key - the finders now
automatically add that. If you pass a specified "from" URL it will still
fetch that.
It does mean you can pass arguments to your finder functions eg:
Widget.find(:all, :conditions => {:name => 'wodget'}, :limit => 5, :offset => 10)
Widget.first(:conditions => {:user_id => 42})
Widget.find_all_by_user_id(42, {:name => 'wodget'})
Currently:
* It will only accept conditions that are passed in as a hash (eg you can't
use the SQL-syntax forms such as: {:conditions => ['NAME = ?, 'blah']}
because (obviously) we're not using SQL...
* it's possible to still pass in :params => {:conditions => ...} (ie old
code shouldn't break) but it's not essential anymore (too complicated).
== Find-API : expected URL construction
HyRes conditional finders assume your API can consume a URL that contains
params formatted in a Railsy way. The way that Rails constructs these URLs
is not well advertised. It seems that Rails will take a hash such as:
{:conditions => {:user_id => 1, :name => 'wodget'}}
and construct a URL that looks like:
http://yourhost:3000/widgets.xml?conditions%5Bname%5D=wodget&conditions%5Buser_id%5D=1
Note the URL-encoding. It evaluates to:
http://yourhost:3000/widgets.xml?conditions[name]=wodget&conditions[user_id]=1
If your API is also written in Rails, this will be converted back into a
params hash that contains the original hash.
HyRes assumes that your API can consume parameters passed on the query
string in the above format and will return a set of resources that match
those parameters.
If you have a nested route for nested resources, you can use a prefix path
== Dynamic finders: find_[by|all_by|first_by|last_by]_X
HyRes supports dynamic finders/initiators as per Active Record eg :
find_all_by_name(<the_name>, opts)
is functionally equivalent to:
find(:all, opts.merge(:name => <the_name>))
You can pass in any number of arguments eg:
find_last_by_name_and_phone_and_email(the_name, the_phone, the_email)
Adding a bang on the end:
find_last_by_name_and_phone_and_email!(the_name, the_phone, the_email)
Will forcee it to raise an exception (ResourceNotFound) if there isn't one
to be found.
== Dynamic instantiators: find_[or_create|or_instantiate]_by_X
You can also create/instantiate using:
find_or_create_by_name(<the_name>, opts)
This will try to find the first resource matching the given attribute and
options, and will create it (using the options) if it doesn't exist)
If you end it in a bang:
find_or_create_by_name!(<the_name>, opts)
It will call create! instead of create and thus raise an exception if create
fails.
By contrast using:
find_or_instantiate_by_name(<the_name>, opts)
will work the same way - but will call "new" instead of "create" - which
allows you to do more to it before it is saved.
Obviously new! is meaningless so a bang on the end does nothing.
== count and Your API
There are several ways that HyRes.count can work with your API.
The simplest would be to implement a count action on your remote API that
returns a result including an attribute of "count".
If your API is implemented in Rails, this would be the equivalent of doing:
def count
@widgets = Widget.all(filters)
respond_to do |format|
format.xml { render :xml => { :count => @widgets.count } }
end
end
Note the filters - if you want to pass conditions to your API, it's a good
idea to filter based on them.
Even if your API is not implemented in rails - as long as it responds
appropriately to an action as per the above, count will "just work".
If you don't have the liberty of updating the remote API, and the API
implements a different path, you can pass the counter_path as an argument,
eg:
Widget.count(:counter_path => '/widgets/my_counter_path.xml')
Finally - if none of the above work for you - count will actually just pull
out all the items that match your arguments... and count the length of the
array.
== Nested Resource routes and associations.
If your remote API has a nested route that matches Rails-standard
nested-route naming conventions eg: '/users/:user_id/widgets.xml'
Your association 'widget' class will need to pass in the prefix-options when
doing a collection-fetch (eg '@user.widgets').
Standard ARes also requires that you setup a prefix-path for the Widget
class.
Now, the 'nested' option will allow you to do both of these tasks
automatically eg:
class User < HyRes
has_many :widgets
end
class Widget < HyRes
belongs_to :user, :nested => true
end
will mean that: @user.widgets will call the URL:
/users/<@user.id>/widgets.xml
At present this will only deal with one level of nesting... it will also
blow away any pre-existing prefix-path... so use at your own peril ;)
== Accessing the raw data objects from your remote API
If your remote API serves up multiple formats or if you just really want to
gets your hands dirty in the raw, un-decoded data that your remote API sends
back - use the raw_data function.
Without any arguments it'll just redo a self.get and return you the data in
the default format - without decoding it.
You can also use it to fetch the current resource in a chosen format by
passing it in eg:
raw_data(:format => :json)
this is especially useful if you need to fetch a data stream that you want
to send straight to the user eg:
respond_to do |format|
format.pdf do
file_data = @my_widget.raw_data(:format => :pdf)
send_data file_data, :type => :pdf, :filename => @my_widget.filename
end
end
HyRes currently supports: :xml, :json and :pdf
Note: PDF is not fully supported except in this raw format
(eg you can't set the default format to pdf and expect it to work... though
if you write a PdfFormat::encode function you can possibly do some neat
things)...
== Examples
1. Install the plugin via:
cd path/to/rails_root/vendor/plugins
git clone git://github.com/taryneast/hyperactiveresource.git
2. Create a HyperactiveResource where you would normally use Active Resource
and define the meta-data/associations that drive the dynamic magic:
NOTE: don't use HyperactiveResource::Base... there isn't one (..yet)!
class Address < HyperactiveResource
# declare API-access as per Active Resource
self.site = 'http://localhost:3001/'
self.primary_key = :uuid
# declare the columns we expect
self.columns = [ :street_address, :city, :postcode, :phone, :email]
# declare any HyRes special overrides
self.counter_path = 'address_count.xml' # override default count path
# add any associations
belongs_to :country
belongs_to :state
has_many :people
# add validations
validates_presence_of :postcode, :phone
validates_uniqueness_of :email
# add callbacks
before_create :generate_uuid
def generate_uuid
self.uuid = UUIDTools::UUID.timestamp_create.to_s
end
end
3. Enjoy the magic
Address.delete_all # should not raise a 404
Address.count # returns 0
bad_address = Address.new
bad_address.save! # raises ActiveResource::RecordNotSaved
Address.count # returns 0
address = Address.new(:postcode => '12345', :phone => '555 1234')
address.uuid # nil - uuid not generated yet
address.country # nil instead of method_missing
address.country_id = 5
address.country # Returns Country.find(5)
address.save! # returns true
address.uuid # a valid uuid!
Address.first.phone # should return '555 1234'
Address.count # returns 1
Address.count(:conditions => {:phone => '555 9876'}) # returns 0
bad_address = Address.create()
bad_address.errors.full_messages.inspect # => "[\"Postcode can't be blank\", \"Phone can't be blank\"]"
Address.count(:conditions => {:phone => '555 1234'}) # returns 1
# note - the sort will only work if your API accepts "sort" on the query string
l_addresses = Address.find_all_by_city('London', :sort => 'postcode')
l_postcodes = l_addresses.map(&:postcode).uniq
p "London postcodes: #{l_postcodes.map(&:to_s).to_sentence}"
# assuming you already have people set up in your remote API...
number_ten = Address.find(:first, :conditions => {
:street_address => "10 Downing street", :city => 'London'})
number_ten.people.each {|person| p "Living at #10 is: #{person.name}" }
etc..
== What has been pulled into Rail 3.0
Work is currently underway to pull some of this functionality back into
Rails itself. These features have made it into 3.0 already - with all the
others on the wishlist to follow.
* validations (using ActiveModel::Validations)
* validates callback (but not validates_on_create/update)
* save!
* all/first/last aliases for find(:all/:first/:last)
* nil instead an exception onf find_every
Currently in progress (ie awaiting discussion on RailsCore):
* 'columns' - feeding method_missing so we don't get a NoMethodError exception when a known attribute is nil
== TODOs
0) Testing!
1) proper callbacks for before/after save/create/validate etc rather than
bodgied-up functions called directly in the code
2) MyModel.with_scope
3) find(:include => ...)
4) attr_protected/attr_accessible
5) MyModel.calculate/average/minimum/maximum etc
6) reflections. There should be no reason why we can't re-use
Active Record-style reflections for our associations. They are not
SQL-specific. This will also allow a lot more code to automatically Just
Work (eg an Active Record could use "has_many :through" a HyRes)
7) Split HyRes into Base and other grouped functions as per AR
8) default_scope (as per AR)
9) validates_associated (as per AR)
10) write_attribute that actually hits the remoteAPI ???
11) a default format for when it doesn't understand how to deal with a given
mime-formats? One which will just pass back the raw data and let you play
with it?
12) cache the raw (un-decoded) data onto the object so we don't have to do a
second fetch? Or at least allow a universal attribute to be set that turns
on cacheing
13) HABTM - and the reverse (ie AR HABTM HyRes and HyRes HABTM AR)
14) has_many :through - and allowing AR's "through" to work as well
N) merge this stuff back into the real Active Resource (currently underway for Rails 3.0)
== Copyright and Authorship
Author:: Taryn East
Copyright (c) 2009:: White Label Dating [http://whitelabeldating.com]
Based on Work Done by Medical Decision Logic
Original copyright:
Copyright (c) 2008 Medical Decision Logic
Released under the MIT license (see attached file)