restfulie 0.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +11 -547
- data/Rakefile +2 -2
- data/lib/restfulie/client/base.rb +28 -51
- data/lib/restfulie/client/entry_point.rb +122 -0
- data/lib/restfulie/client/instance.rb +100 -5
- data/lib/restfulie/{server → client}/state.rb +1 -1
- data/lib/restfulie/client.rb +25 -0
- data/lib/restfulie/server/base.rb +31 -13
- data/lib/restfulie/server/controller.rb +31 -15
- data/lib/restfulie/server/instance.rb +0 -19
- data/lib/restfulie/server/marshalling.rb +5 -5
- data/lib/restfulie/server/transition.rb +18 -4
- data/lib/restfulie/unmarshalling.rb +40 -9
- data/lib/restfulie.rb +31 -15
- metadata +5 -3
data/README.textile
CHANGED
@@ -12,553 +12,17 @@ h2. Why would I use restfulie?
|
|
12
12
|
4. HATEOAS --> clients you are unaware of will not bother if you change your URIs
|
13
13
|
5. HATEOAS --> services that you consume will not affect your software whenever they change part of their flow or URIs
|
14
14
|
|
15
|
-
h2.
|
15
|
+
h2. Restfulie
|
16
16
|
|
17
|
-
|
18
|
-
as the way to lead your client's path through a business process.
|
17
|
+
The documentation is at "http://wiki.github.com/caelum/restfulie":http://wiki.github.com/caelum/restfulie
|
19
18
|
|
20
|
-
Therefore Restfulie is unique both in its feature set when compared to both Spring and JAX-RS based implementations, and its implementation: looking for simple code and favoring conventions over manual configurations.
|
21
|
-
|
22
|
-
h1. Short examples
|
23
|
-
|
24
|
-
h2. Restfulie: client-side
|
25
|
-
|
26
|
-
Example on accessing a resource and its services through the restfulie API:
|
27
|
-
|
28
|
-
<pre>
|
29
|
-
order = Order.from_web resource_uri
|
30
|
-
|
31
|
-
puts "Order price is #{order.price}"
|
32
|
-
|
33
|
-
order.pay payment # sends a post request to pay this order
|
34
|
-
|
35
|
-
order.cancel # sends a delete request
|
36
|
-
</pre>
|
37
|
-
|
38
|
-
h2. Restfulie: server-side
|
39
|
-
|
40
|
-
This is a simple example how to make your state changes available to your resource consumers:
|
41
|
-
|
42
|
-
<pre>
|
43
|
-
class Order < ActiveRecord::Base
|
44
|
-
|
45
|
-
acts_as_restfulie
|
46
|
-
|
47
|
-
def following_transitions
|
48
|
-
transitions = []
|
49
|
-
transitions << [:show, {}]
|
50
|
-
transitions << [:destroy, {}] if can_cancel?
|
51
|
-
transitions << [:pay, {:id => id}] if can_pay?
|
52
|
-
transitions
|
53
|
-
end
|
54
|
-
end
|
55
|
-
</pre>
|
56
|
-
|
57
|
-
*You might want to create a migration with a string field named status for your resource:*
|
58
|
-
|
59
|
-
<pre>
|
60
|
-
scripts/generate migration add_status_to_order
|
61
|
-
</pre>
|
62
|
-
|
63
|
-
Content:
|
64
|
-
|
65
|
-
<pre>
|
66
|
-
class AddStatusToOrder < ActiveRecord::Migration
|
67
|
-
def self.up
|
68
|
-
add_column :orders, :status, :string
|
69
|
-
Order.all.each do |order|
|
70
|
-
order.status = "unpaid"
|
71
|
-
order.save
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.down
|
76
|
-
remove_column :orders, :status
|
77
|
-
end
|
78
|
-
end
|
79
|
-
</pre>
|
80
|
-
|
81
|
-
Or simply define a status reader and writer on your own.
|
82
|
-
|
83
|
-
h2. Restfulie server-side: state machine
|
84
|
-
|
85
|
-
For those willing to implement a more complex or advanced state machine, you can use the dsl-like api:
|
86
|
-
|
87
|
-
<pre>
|
88
|
-
class Order < ActiveRecord::Base
|
89
|
-
acts_as_restfulie
|
90
|
-
|
91
|
-
state :unpaid, :allow => [:latest, :pay, :cancel]
|
92
|
-
state :cancelled, :allow => :latest
|
93
|
-
|
94
|
-
transition :latest, {:action => :show}
|
95
|
-
transition :cancel, {:action => :destroy}, :cancelled
|
96
|
-
transition :pay, {}, :preparing
|
97
|
-
end
|
98
|
-
</pre>
|
99
|
-
|
100
|
-
h1. Installing
|
101
|
-
|
102
|
-
Just add in your environment.rb the following line:
|
103
|
-
|
104
|
-
<pre>
|
105
|
-
config.gem "restfulie", :source => "http://gemcutter.org"
|
106
|
-
</pre>
|
107
|
-
|
108
|
-
And then execute:
|
109
|
-
<pre>rake gems:install</pre>
|
110
|
-
|
111
|
-
or, if you prefer to install it as a plugin:
|
112
|
-
|
113
|
-
<pre>script/plugin install git://github.com/caelum/restfulie.git</pre>
|
114
|
-
|
115
|
-
h2. Typical hypermedia aware example
|
116
|
-
|
117
|
-
Trying to follow the definition of a RESTful application supporting resources with hypermedia content, a resource would be:
|
118
|
-
|
119
|
-
<pre>
|
120
|
-
<order>
|
121
|
-
<product>basic rails course</product>
|
122
|
-
<product>RESTful training</product>
|
123
|
-
<atom:link rel="refresh" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
|
124
|
-
<atom:link rel="update" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
|
125
|
-
<atom:link rel="pay" href="http://www.caelum.com.br/orders/1/pay" xmlns:atom="http://www.w3.org/2005/Atom"/>
|
126
|
-
<atom:link rel="destroy" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
|
127
|
-
</order>
|
128
|
-
</pre>
|
129
|
-
|
130
|
-
h2. Client Usage
|
131
|
-
|
132
|
-
Create your class and invoke the *uses_restfulie* method:
|
133
|
-
|
134
|
-
<pre>class Order < ActiveRecord::Base
|
135
|
-
uses_restfulie
|
136
|
-
end
|
137
|
-
</pre>
|
138
|
-
|
139
|
-
One should first acquire the representation from the server through your common GET request and process it through the usual from_* methods:
|
140
|
-
<pre>xml = Net::HTTP.get(URI.parse('http://www.caelum.com.br/orders/1'))
|
141
|
-
order = Order.from_xml(xml)</pre>
|
142
|
-
or use the restfulie *from_web*:
|
143
|
-
<pre>order = Order.from_web 'http://www.caelum.com.br/orders/1'</pre>
|
144
|
-
|
145
|
-
And now you can invoke all those actions in order to change your resource's state:
|
146
|
-
|
147
|
-
<pre>
|
148
|
-
order.refresh
|
149
|
-
order.update
|
150
|
-
order.destroy
|
151
|
-
order.pay
|
152
|
-
</pre>
|
153
|
-
|
154
|
-
Note that:
|
155
|
-
* refresh is get
|
156
|
-
* update is put (and you have to put everything back)
|
157
|
-
* destroy is delete
|
158
|
-
* pay (unknown methods) is post
|
159
|
-
|
160
|
-
h2. Resource format support
|
161
|
-
|
162
|
-
Restfulie currently supports full xml+atom, partial xml+rel and will soon expand its support to json+links.
|
163
|
-
|
164
|
-
|
165
|
-
h2. Help
|
166
|
-
|
167
|
-
If you are looking for or want to help, let us know at the mailing list:
|
168
|
-
|
169
|
-
"http://groups.google.com/group/restfulie":http://groups.google.com/group/restfulie
|
170
|
-
|
171
|
-
h2. Client-side configuration: how to customize your request
|
172
|
-
|
173
|
-
h3. HTTP verbs
|
174
|
-
|
175
|
-
By default, restfulie uses the following table:
|
176
|
-
|
177
|
-
* destroy, cancel and delete send a DELETE request
|
178
|
-
* update sends a PUT request
|
179
|
-
* refresh, reload, show, latest sends a GET request
|
180
|
-
* other methods sends a POST request
|
181
|
-
|
182
|
-
If you want to use a custom http verb in order to send your request, you can do it by setting the optional string 'method':
|
183
|
-
|
184
|
-
<pre>order.update(:method=>"post")</pre>
|
185
|
-
|
186
|
-
h3. Request parameters
|
187
|
-
|
188
|
-
If you want to send extra parameters, you can do it through the *data* parameter:
|
189
|
-
|
190
|
-
<pre>order.pay(:data => {:payment => my_payment})</pre>
|
191
|
-
|
192
|
-
The parameters will be serialized either to xml or json according to which format was used to deserialize the order at first place.
|
193
|
-
|
194
|
-
h3. Executing another GET request
|
195
|
-
|
196
|
-
If your method executes another GET request, it will automatically deserialize its result as:
|
197
|
-
|
198
|
-
<pre>order = Order.from_web order_uri
|
199
|
-
payment = order.check_payment_info</pre>
|
200
|
-
|
201
|
-
If you want to parse the response yourself, instead of receiving just the final deserialized object, you can do it by passing a body to your method
|
202
|
-
|
203
|
-
<pre>order = Order.from_web order_uri
|
204
|
-
successful = order.check_payment_info do |response|
|
205
|
-
return response.code==200
|
206
|
-
end</pre>
|
207
|
-
|
208
|
-
h2. Server-side configuration
|
209
|
-
|
210
|
-
There are two different approaches that can be combined to create a full hypermedia aware resource based service, including awareness of its states and transitions.
|
211
|
-
|
212
|
-
h3. Simple usage: following transitions
|
213
|
-
|
214
|
-
The most easy way to use restfulie is to write the *following_transitions* method.
|
215
|
-
There are three easy steps to make it work:
|
216
|
-
|
217
|
-
1. Create your model (i.e. Order) with an *status* field
|
218
|
-
<pre>
|
219
|
-
script/generate scaffold Order status:string location:string
|
220
|
-
rake db:create
|
221
|
-
rake db:migrate
|
222
|
-
</pre>
|
223
|
-
|
224
|
-
Note that with this usage the status field is optional (from 0.3.0 onwards).
|
225
|
-
|
226
|
-
2. Add the *acts_as_restfulie* invocation and *following_transitions* method returning an array of possible transitions:
|
227
|
-
|
228
|
-
<pre>
|
229
|
-
acts_as_restfulie
|
230
|
-
|
231
|
-
def following_transitions
|
232
|
-
transitions = []
|
233
|
-
transitions << [:show, {}]
|
234
|
-
transitions
|
235
|
-
end
|
236
|
-
</pre>
|
237
|
-
|
238
|
-
3. Update your *show* method within the *OrdersController* to show the hypermedia content:
|
239
|
-
|
240
|
-
<pre>
|
241
|
-
def show
|
242
|
-
@order = Order.find(params[:id])
|
243
|
-
|
244
|
-
respond_to do |format|
|
245
|
-
format.html # show.html.erb
|
246
|
-
format.xml { render :xml => @order.to_xml(:controller=>self) }
|
247
|
-
end
|
248
|
-
end
|
249
|
-
</pre>
|
250
|
-
|
251
|
-
You are ready to go, create a new order and save it into the database:
|
252
|
-
|
253
|
-
<pre>
|
254
|
-
order = Order.new
|
255
|
-
order.location = "take away"
|
256
|
-
order.status = "unpaid"
|
257
|
-
order.save
|
258
|
-
puts "Order #{order.id} saved"
|
259
|
-
</pre>
|
260
|
-
|
261
|
-
Start up the server:
|
262
|
-
|
263
|
-
<pre>
|
264
|
-
script/server
|
265
|
-
</pre>
|
266
|
-
|
267
|
-
And now access your server at http://localhost:3000/orders/1.xml
|
268
|
-
|
269
|
-
<pre>
|
270
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
271
|
-
<order>
|
272
|
-
<created-at>2009-11-23T00:15:15Z</created-at>
|
273
|
-
<id>1</id>
|
274
|
-
<location>take away</location>
|
275
|
-
<status>unpaid</status>
|
276
|
-
<updated-at>2009-11-23T00:15:15Z</updated-at>
|
277
|
-
<atom:link rel="show" xmlns:atom="http://www.w3.org/2005/Atom" href="http://localhost:3000/orders/3"/>
|
278
|
-
</order>
|
279
|
-
</pre>
|
280
|
-
|
281
|
-
h3. Customizing the rel name
|
282
|
-
|
283
|
-
You can also override the action used, but still keep the rel
|
284
|
-
|
285
|
-
<pre>
|
286
|
-
def following_transitions
|
287
|
-
transitions = []
|
288
|
-
transitions << [:cancel, { :action => :destroy }]
|
289
|
-
transitions
|
290
|
-
end
|
291
|
-
</pre>
|
292
|
-
|
293
|
-
Which will generate an hyperlink as
|
294
|
-
|
295
|
-
<pre><atom:link rel="cancel" rel="http://yourserver/orders/15" /></pre>
|
296
|
-
|
297
|
-
h3. Example
|
298
|
-
|
299
|
-
A full example showing all capabilities of this method follows:
|
300
|
-
|
301
|
-
<pre>
|
302
|
-
def following_transitions
|
303
|
-
transitions = []
|
304
|
-
transitions << [:show, {}]
|
305
|
-
transitions << [:destroy, {}] if can_cancel?
|
306
|
-
transitions << [:pay, {:id => id}] if can_pay?
|
307
|
-
transitions << [:show, {:controller => :payments, :payment_id => payment.id }] if paid?
|
308
|
-
transitions
|
309
|
-
end
|
310
|
-
</pre>
|
311
|
-
|
312
|
-
h2. Advanced usage: Defining the state machine and its transitions
|
313
|
-
|
314
|
-
The second way of defining your available transitions is to explicitely define the states and transitions.
|
315
|
-
|
316
|
-
By using this approach, one has to define a new column named *status* in a database migration file.
|
317
|
-
|
318
|
-
The first step involves defining all your states, each one with its own name and possible transitions, as:
|
319
|
-
|
320
|
-
<pre>
|
321
|
-
state :state_name, :allow => [ :first_transition_name, :second_transition_name]
|
322
|
-
</pre>
|
323
|
-
|
324
|
-
The following example shows all possible states for an order:
|
325
|
-
|
326
|
-
<pre>
|
327
|
-
class Order < ActiveRecord::Base
|
328
|
-
|
329
|
-
acts_as_restfulie
|
330
|
-
|
331
|
-
state :unpaid, :allow => [:latest, :pay, :cancel]
|
332
|
-
state :cancelled, :allow => :latest
|
333
|
-
state :received, :allow => [:latest, :check_payment_info]
|
334
|
-
state :preparing, :allow => [:latest, :check_payment_info]
|
335
|
-
state :ready, :allow => [:latest, :receive, :check_payment_info]
|
336
|
-
end
|
337
|
-
</pre>
|
338
|
-
|
339
|
-
Now its time to define which controller and action each transition invokes, in a much similar way to
|
340
|
-
the transition definitions in the following_transitions method:
|
341
|
-
|
342
|
-
<pre>
|
343
|
-
class Order < ActiveRecord::Base
|
344
|
-
end
|
345
|
-
</pre>
|
346
|
-
|
347
|
-
Once a transition has been given a name, its name can be used in the following_transitions method also.
|
348
|
-
The next example does not configure the transition because it was already defined, only adding it to the
|
349
|
-
list of available transition whenever the *can_pay?* method returns true:
|
350
|
-
|
351
|
-
<pre>
|
352
|
-
class Order < ActiveRecord::Base
|
353
|
-
|
354
|
-
acts_as_restfulie
|
355
|
-
|
356
|
-
transition :pay, {:action => pay_this_order, :controller => :payments}, :preparing
|
357
|
-
|
358
|
-
def following_transitions
|
359
|
-
transitions = []
|
360
|
-
transitions << :pay if can_pay?
|
361
|
-
transitions
|
362
|
-
end
|
363
|
-
end
|
364
|
-
</pre>
|
365
|
-
|
366
|
-
Note that whenever one defines a transition, there is a third - optional - argument, this is the
|
367
|
-
transition's target's state. Whenever the method *order.pay* method is invoked in the *server*, it will
|
368
|
-
automatically change the order's status to *preparing*.
|
369
|
-
|
370
|
-
You can download the server side example to see the complete code.
|
371
|
-
|
372
|
-
The last usage of the transition definition involves passing a block which receives the element in which
|
373
|
-
the transition URI's is required. The block should return all the necessary information for retrieving the URI, now having access to your element's instance variables:
|
374
|
-
|
375
|
-
<pre>
|
376
|
-
class Order < ActiveRecord::Base
|
377
|
-
transition :check_payment_info do |order|
|
378
|
-
{:controller => :payments, :action => :show, :order_id => order.id, :payment_id => order.payments[0].id, :rel => "check_payment_info"}
|
379
|
-
end
|
380
|
-
end
|
381
|
-
</pre>
|
382
|
-
|
383
|
-
h3. Accessing all possible transitions
|
384
|
-
|
385
|
-
One can access all possible transitions for an object by invoking its available_transitions method:
|
386
|
-
|
387
|
-
<pre>
|
388
|
-
transitions = order.available_transitions
|
389
|
-
</pre>
|
390
|
-
|
391
|
-
h3. Checking the possibility of following transitions
|
392
|
-
|
393
|
-
By following the advanced usage, one receives also all *can_* method. i.e.:
|
394
|
-
|
395
|
-
<pre>
|
396
|
-
order.status = :unpaid
|
397
|
-
puts(order.can_pay?) # will print true
|
398
|
-
order.status = :paid
|
399
|
-
puts(order.can_pay?) # will print false
|
400
|
-
</pre>
|
401
|
-
|
402
|
-
You can use the *can_xxx* methods in your controllers to check if your current resource's state can be changed:
|
403
|
-
|
404
|
-
<pre>
|
405
|
-
def pay
|
406
|
-
@order = Order.find(params[:id])
|
407
|
-
raise "impossible to pay due to this order status #{order.status}" if !@order.can_pay?
|
408
|
-
|
409
|
-
# payment code
|
410
|
-
end
|
411
|
-
</pre>
|
412
|
-
|
413
|
-
h3. Using xml+rel links instead of atom links
|
414
|
-
|
415
|
-
Atom is everywhere and can be consumed by a number of existing tools but if your system wants to supply its
|
416
|
-
services through commons rel+link xml as
|
417
|
-
|
418
|
-
<pre>
|
419
|
-
<order>
|
420
|
-
<product>basic rails course</product>
|
421
|
-
<product>RESTful training</product>
|
422
|
-
<refresh>http://www.caelum.com.br/orders/1</refresh>
|
423
|
-
<update>http://www.caelum.com.br/orders/1</update>
|
424
|
-
<pay>http://www.caelum.com.br/orders/1/pay</pay>
|
425
|
-
<destroy>http://www.caelum.com.br/orders/1</destroy>
|
426
|
-
</order>
|
427
|
-
</pre>
|
428
|
-
|
429
|
-
You can do it by passing the *use_name_based_link* argument:
|
430
|
-
|
431
|
-
<pre>
|
432
|
-
order.to_xml(:controller => my_controller, :use_name_based_link => true)
|
433
|
-
</pre>
|
434
|
-
|
435
|
-
h2. Team
|
436
|
-
|
437
|
-
Restfulie was created and is maintained within Caelum by
|
438
|
-
|
439
|
-
Projetct Founder
|
440
|
-
* "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br - twitter:http://www.twitter.com/guilhermecaelum "http://guilhermesilveira.wordpress.com":http://guilhermesilveira.wordpress.com
|
441
|
-
|
442
|
-
Active Commiters
|
443
|
-
* "Caue Guerra":mailto:caue.guerra@gmail.com - "http://caueguerra.com/":http://caueguerra.com/
|
444
|
-
* "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
|
445
|
-
|
446
|
-
Contributors
|
447
|
-
* Diego Carrion
|
448
|
-
* Leandro Silva
|
449
|
-
* Gavin-John Noonan
|
450
|
-
|
451
|
-
h2. Try it online
|
452
|
-
|
453
|
-
We have a live example of a server implementation using a resource+hypermedia course ordering system available.
|
454
|
-
|
455
|
-
Follow the steps below to try out the system:
|
456
|
-
|
457
|
-
* "Access the server system":http://restfulie-test.heroku.com
|
458
|
-
* Create a couple of trainings
|
459
|
-
* Create an order
|
460
|
-
* Access the order listing and retrieve its xml link
|
461
|
-
|
462
|
-
And now you can try the restfulie client api through a simple and generic resource+hypermedia client application:
|
463
|
-
|
464
|
-
* "Access the client system":http://restfulie-client.heroku.com
|
465
|
-
* Enter your order uri
|
466
|
-
* Check your order information which was retrieved and all available actions
|
467
|
-
|
468
|
-
Now you can either:
|
469
|
-
|
470
|
-
* *latest* - refresh your order information _order.latest_
|
471
|
-
* *cancel* - cancel your order (dead end!) _order.destroy_
|
472
|
-
* *pay* - pay for your order, and don't forget to send your (fake) credit card information _order.pay(payment)_
|
473
|
-
* *check_payment_info* - after paying you can check the payment information stored at the server _order.check_payment_info_
|
474
|
-
|
475
|
-
In order to pay do not forget to send the parameter *payment* with a value as
|
476
|
-
|
477
|
-
<pre>
|
478
|
-
<payment>
|
479
|
-
<amount>15</amount>
|
480
|
-
<cardholder_name>Guilherme Silveira</cardholder_name>
|
481
|
-
<card_number>123456789012</card_number>
|
482
|
-
<expiry_month>12</expiry_month>
|
483
|
-
<expiry_year>12</expiry_year>
|
484
|
-
</payment>
|
485
|
-
</pre>
|
486
|
-
|
487
|
-
|
488
|
-
h3. Sources
|
489
|
-
|
490
|
-
You can see an application's source code here, both client and server side were implemented using *restfulie*:
|
491
|
-
|
492
|
-
"Client":http://github.com/caelum/restfulie-client
|
493
|
-
"Server":http://github.com/caelum/restfulie-test
|
494
|
-
|
495
|
-
h3. More tutorials
|
496
|
-
|
497
|
-
There is a "portuguese tutorial on the server-side support":http://wakethedead.com.br/blog/70-restfulie, "more on restfulie - portuguese":http://andersonleiteblog.wordpress.com/2009/11/23/mais-sobre-restfulie/ and a "blog post on the entire ecosystem in english":http://guilhermesilveira.wordpress.com/2009/11/03/quit-pretending-use-the-web-for-real-restfulie/
|
498
|
-
|
499
|
-
|
500
|
-
h2. What's new
|
501
|
-
|
502
|
-
h3. next release
|
503
|
-
* API change: you need to invoke "acts_as_restfulie" to your models
|
504
|
-
* implemented support to can_*** methods
|
505
|
-
* post data through http POST body
|
506
|
-
* no need for the *status* field if you use the following_transition approach on the server side
|
507
|
-
* bug fixed when *status* was nil
|
508
|
-
* lots of internal refactoring
|
509
|
-
* lots of new documentation
|
510
|
-
|
511
|
-
h3. 0.2
|
512
|
-
* support to state machine configuration
|
513
|
-
* resources must contain a status field
|
514
|
-
|
515
|
-
h3. 0.1
|
516
|
-
* first release
|
517
|
-
|
518
|
-
h2. Coming soon
|
519
|
-
|
520
|
-
* Generate and link to rubydoc
|
521
|
-
* release 0.3
|
522
|
-
* change parameter order for transition: last one is HASH, first is name, second (if existing) is target state
|
523
|
-
* allows pure String/byte array client side post and server side retrieval
|
524
|
-
* integration tests on orderserver/client api
|
525
|
-
* controller method should check if its an restfulie resource
|
526
|
-
* full support to extended json
|
527
|
-
* rel prepend suffix as http://iansrobinson.com/resources/link-relations/preceding
|
528
|
-
* automatically generate uri for this rel with its transition description
|
529
|
-
* pure href definition of link
|
530
|
-
* post entry point support
|
531
|
-
* remove client dependency from ActiveRecord
|
532
|
-
* remove server side dependency from ActiveRecord (its ok to use anything else)
|
533
|
-
* Set the correct media type instead of application/xml
|
534
|
-
* transitions << [:show] should work
|
535
|
-
* rails 3 easier support (no controller argument!)
|
536
|
-
* allow servers to define transitions by accessing other systems
|
537
|
-
* allow servers to define a state method instead of internal variable
|
538
|
-
* controller filtering and methods
|
539
|
-
* english tutorial
|
540
|
-
* when receiving a 201 + content, it should believe the content
|
541
|
-
* when receiving a 201 without any content, it should allow to redirect or not
|
542
|
-
* client side should allow withTimeStamp, withETag, withAuth
|
543
|
-
* is there is an etag, use it by default (maybe NOT use it by default)... modified since and so on (header tags)
|
544
|
-
* server side maybe allow hypermedia controls or not
|
545
|
-
|
546
|
-
h2. License
|
547
|
-
|
548
|
-
/***
|
549
|
-
* Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
|
550
|
-
* All rights reserved.
|
551
|
-
*
|
552
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
553
|
-
* you may not use this file except in compliance with the License.
|
554
|
-
* You may obtain a copy of the License at
|
555
|
-
*
|
556
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
557
|
-
*
|
558
|
-
* Unless required by applicable law or agreed to in writing, software
|
559
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
560
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
561
|
-
* See the License for the specific language governing permissions and
|
562
|
-
* limitations under the License.
|
563
|
-
*/
|
564
19
|
|
20
|
+
<script type="text/javascript">
|
21
|
+
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
22
|
+
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
23
|
+
</script>
|
24
|
+
<script type="text/javascript">
|
25
|
+
try {
|
26
|
+
var pageTracker = _gat._getTracker("UA-11770776-1");
|
27
|
+
pageTracker._trackPageview();
|
28
|
+
} catch(err) {}</script>
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
|
|
5
5
|
require 'spec/rake/spectask'
|
6
6
|
|
7
7
|
GEM = "restfulie"
|
8
|
-
GEM_VERSION = "0.
|
8
|
+
GEM_VERSION = "0.4.0"
|
9
9
|
SUMMARY = "Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side)."
|
10
10
|
AUTHOR = "Guilherme Silveira, Caue Guerra"
|
11
11
|
EMAIL = "guilherme.silveira@caelum.com.br"
|
@@ -37,7 +37,7 @@ end
|
|
37
37
|
|
38
38
|
desc "Install the gem locally"
|
39
39
|
task :install => [:package] do
|
40
|
-
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
40
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} -l}
|
41
41
|
end
|
42
42
|
|
43
43
|
desc "Create a gemspec file"
|
@@ -1,18 +1,36 @@
|
|
1
1
|
module Restfulie
|
2
2
|
module Client
|
3
|
-
module Base
|
4
|
-
|
5
|
-
|
6
|
-
def from_response(res)
|
3
|
+
module Base
|
4
|
+
|
5
|
+
SELF_RETRIEVAL = [:latest, :refresh, :reload]
|
7
6
|
|
8
|
-
|
7
|
+
class UnsupportedContentType < Exception
|
8
|
+
attr_reader :msg
|
9
|
+
def initialize(msg)
|
10
|
+
@msg = msg
|
11
|
+
end
|
12
|
+
def to_s
|
13
|
+
@msg
|
14
|
+
end
|
15
|
+
end
|
9
16
|
|
10
|
-
|
17
|
+
# translates a response to an object
|
18
|
+
def from_response(res, invoking_object)
|
19
|
+
|
20
|
+
return invoking_object if res.code=="304"
|
21
|
+
|
22
|
+
raise UnsupportedContentType.new("unsupported content type '#{res.content_type}' '#{res.code}'") unless res.content_type=="application/xml"
|
23
|
+
|
24
|
+
body = res.body
|
25
|
+
return {} if body.empty?
|
26
|
+
|
27
|
+
hash = Hash.from_xml body
|
11
28
|
return hash if hash.keys.length == 0
|
29
|
+
|
12
30
|
raise "unable to parse an xml with more than one root element" if hash.keys.length>1
|
13
31
|
|
14
32
|
type = hash.keys[0].camelize.constantize
|
15
|
-
type.from_xml(
|
33
|
+
type.from_xml(body)
|
16
34
|
|
17
35
|
end
|
18
36
|
|
@@ -24,53 +42,12 @@ module Restfulie
|
|
24
42
|
return basic_mapping[overriden_option.to_sym] if overriden_option
|
25
43
|
defaults[name.to_sym] || Net::HTTP::Post
|
26
44
|
end
|
27
|
-
|
28
|
-
def add_state(transition)
|
29
|
-
name = transition["rel"]
|
30
45
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
self.invoke_remote_transition(Restfulie::Client::Helper.current_method, options, block)
|
35
|
-
end
|
36
|
-
|
37
|
-
alias_method name, :temp_method
|
38
|
-
undef :temp_method
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# receives an object and inserts all necessary methods
|
43
|
-
# so it can answer to can_??? invocations
|
44
|
-
def add_transitions(result, states)
|
45
|
-
result._possible_states = {}
|
46
|
-
|
47
|
-
states.each do |state|
|
48
|
-
result._possible_states[state["rel"]] = state
|
49
|
-
add_state(state)
|
50
|
-
end
|
51
|
-
result.extend Restfulie::Server::State
|
52
|
-
|
53
|
-
result
|
46
|
+
def is_self_retrieval?(name)
|
47
|
+
name = name.to_sym if name.kind_of? String
|
48
|
+
SELF_RETRIEVAL.include? name
|
54
49
|
end
|
55
|
-
|
56
|
-
# retrieves a resource form a specific uri
|
57
|
-
def from_web(uri)
|
58
|
-
res = Net::HTTP.get_response(URI.parse(uri))
|
59
|
-
# TODO redirect... follow or not? (optional...)
|
60
|
-
raise "invalid request" if res.code != "200"
|
61
|
-
|
62
|
-
# TODO really support different content types
|
63
|
-
case res.content_type
|
64
|
-
when "application/xml"
|
65
|
-
self.from_xml res.body
|
66
|
-
when "application/json"
|
67
|
-
self.from_json res.body
|
68
|
-
else
|
69
|
-
raise "unknown content type"
|
70
|
-
end
|
71
50
|
|
72
|
-
end
|
73
|
-
|
74
51
|
end
|
75
52
|
end
|
76
53
|
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Client
|
3
|
+
module Base
|
4
|
+
|
5
|
+
# configures an entry point
|
6
|
+
def entry_point_for
|
7
|
+
@entry_points ||= EntryPointControl.new(self)
|
8
|
+
@entry_points
|
9
|
+
end
|
10
|
+
|
11
|
+
# executes a POST request to create this kind of resource at the server
|
12
|
+
def remote_create(content)
|
13
|
+
content = content.to_xml unless content.kind_of? String
|
14
|
+
remote_post content
|
15
|
+
end
|
16
|
+
|
17
|
+
# handles which types of responses should be automatically followed
|
18
|
+
def follows
|
19
|
+
@follower ||= FollowConfig.new
|
20
|
+
@follower
|
21
|
+
end
|
22
|
+
|
23
|
+
# retrieves a resource form a specific uri
|
24
|
+
def from_web(uri, options = {})
|
25
|
+
uri = URI.parse(uri)
|
26
|
+
req = Net::HTTP::Get.new(uri.path)
|
27
|
+
options.each do |key,value| req[key] = value end
|
28
|
+
res = Net::HTTP.start(uri.host, uri.port) {|http|
|
29
|
+
http.request(req) # canc hange to straight .request(req)
|
30
|
+
}
|
31
|
+
|
32
|
+
code = res.code
|
33
|
+
return from_web(res["Location"]) if code=="301"
|
34
|
+
|
35
|
+
if code=="200"
|
36
|
+
# TODO really support different content types
|
37
|
+
case res.content_type
|
38
|
+
when "application/xml"
|
39
|
+
result = self.from_xml res.body
|
40
|
+
when "application/json"
|
41
|
+
result = self.from_json res.body
|
42
|
+
else
|
43
|
+
raise "unknown content type: #{res.content_type}"
|
44
|
+
end
|
45
|
+
result.etag = res['Etag'] unless res['Etag'].nil?
|
46
|
+
result.last_modified = res['Last-Modified'] unless res['Last-Modified'].nil?
|
47
|
+
result
|
48
|
+
else
|
49
|
+
res
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def remote_post(content)
|
56
|
+
remote_post_to(entry_point_for.create.uri, content)
|
57
|
+
end
|
58
|
+
def remote_post_to(uri, content)
|
59
|
+
|
60
|
+
url = URI.parse(uri)
|
61
|
+
req = Net::HTTP::Post.new(url.path)
|
62
|
+
req.body = content
|
63
|
+
req.add_field("Accept", "application/xml")
|
64
|
+
|
65
|
+
response = Net::HTTP.new(url.host, url.port).request(req)
|
66
|
+
code = response.code
|
67
|
+
|
68
|
+
if code=="301" && follows.moved_permanently? == :all
|
69
|
+
remote_post_to(response["Location"], content)
|
70
|
+
elsif code=="201"
|
71
|
+
from_web(response["Location"], "Accept" => "application/xml")
|
72
|
+
else
|
73
|
+
response
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class FollowConfig
|
81
|
+
def initialize
|
82
|
+
@entries = {
|
83
|
+
:moved_permanently => [:get, :head]
|
84
|
+
}
|
85
|
+
end
|
86
|
+
def method_missing(name, *args)
|
87
|
+
return value_for name if name.to_s[-1,1]=="?"
|
88
|
+
set_all_for name
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def set_all_for(name)
|
93
|
+
@entries[name] = :all
|
94
|
+
end
|
95
|
+
def value_for(name)
|
96
|
+
return @entries[name.to_s.chop.to_sym]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class EntryPointControl
|
101
|
+
|
102
|
+
def initialize(type)
|
103
|
+
@type = type
|
104
|
+
@entries = {}
|
105
|
+
end
|
106
|
+
|
107
|
+
def method_missing(name, *args)
|
108
|
+
@entries[name] ||= EntryPoint.new
|
109
|
+
@entries[name]
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
class EntryPoint
|
115
|
+
attr_accessor :uri
|
116
|
+
def at(uri)
|
117
|
+
@uri = uri
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -3,28 +3,123 @@ module Restfulie
|
|
3
3
|
module Instance
|
4
4
|
|
5
5
|
# list of possible states to access
|
6
|
-
|
6
|
+
def _possible_states
|
7
|
+
@_possible_states ||= {}
|
8
|
+
end
|
7
9
|
|
8
10
|
# which content-type generated this data
|
9
11
|
attr_accessor :_came_from
|
10
12
|
|
11
|
-
|
12
13
|
def invoke_remote_transition(name, options, block)
|
13
14
|
|
14
15
|
method = self.class.requisition_method_for options[:method], name
|
15
16
|
|
16
|
-
|
17
|
+
state = self._possible_states[name]
|
18
|
+
url = URI.parse(state["href"] || state[:href])
|
17
19
|
req = method.new(url.path)
|
18
20
|
req.body = options[:data] if options[:data]
|
19
21
|
req.add_field("Accept", "application/xml") if self._came_from == :xml
|
22
|
+
req.add_field("If-None-Match", self.etag) if self.class.is_self_retrieval?(name) && self.respond_to?(:etag)
|
23
|
+
req.add_field("If-Modified-Since", self.last_modified) if self.class.is_self_retrieval?(name) && self.respond_to?(:last_modified)
|
20
24
|
|
21
25
|
response = Net::HTTP.new(url.host, url.port).request(req)
|
22
26
|
|
23
27
|
return block.call(response) if block
|
24
|
-
return response
|
25
|
-
self.class.from_response response
|
28
|
+
return response unless method == Net::HTTP::Get
|
29
|
+
self.class.from_response response, self
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# inserts all transitions from this object as can_xxx and xxx methods
|
34
|
+
def add_transitions(transitions)
|
35
|
+
|
36
|
+
transitions.each do |state|
|
37
|
+
self._possible_states[state["rel"] || state[:rel]] = state
|
38
|
+
self.add_state(state)
|
39
|
+
end
|
40
|
+
self.extend Restfulie::Client::State
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def add_state(transition)
|
45
|
+
name = transition["rel"] || transition[:rel]
|
46
|
+
|
47
|
+
# TODO: wrong, should be instance_eval
|
48
|
+
self.class.module_eval do
|
49
|
+
|
50
|
+
def temp_method(options = {}, &block)
|
51
|
+
self.invoke_remote_transition(Restfulie::Client::Helper.current_method, options, block)
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method name, :temp_method
|
55
|
+
undef :temp_method
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# returns a list of extended fields for this instance.
|
60
|
+
# extended fields are those unknown to this model but kept in a hash
|
61
|
+
# to allow forward-compatibility.
|
62
|
+
def extended_fields
|
63
|
+
@hash ||= {}
|
64
|
+
@hash
|
65
|
+
end
|
66
|
+
|
67
|
+
def method_missing(name, *args)
|
68
|
+
name = name.to_s if name.kind_of? Symbol
|
69
|
+
|
70
|
+
if name[-1,1] == "="
|
71
|
+
extended_fields[name.chop] = args[0]
|
72
|
+
elsif name[-1,1] == "?"
|
73
|
+
found = extended_fields[name.chop]
|
74
|
+
return super(name,args) if found.nil?
|
75
|
+
parse(found)
|
76
|
+
else
|
77
|
+
found = extended_fields[name]
|
78
|
+
return super(name,args) if found.nil?
|
79
|
+
parse(transform(found))
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO test this guy
|
85
|
+
def respond_to?(sym)
|
86
|
+
extended_fields[sym.to_s].nil? ? super(sym) : true
|
87
|
+
end
|
88
|
+
|
89
|
+
# redefines attribute definition allowing the invocation of method_missing
|
90
|
+
# when an attribute does not exist
|
91
|
+
def attributes=(values)
|
92
|
+
values.each do |key, value|
|
93
|
+
unless attributes.include? key
|
94
|
+
method_missing("#{key}=", value)
|
95
|
+
values.delete key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
super(values)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# serializes the extended fields with the existing fields
|
103
|
+
def to_xml(options={})
|
104
|
+
super(options) do |xml|
|
105
|
+
extended_fields.each do |key,value|
|
106
|
+
xml.tag! key, value
|
107
|
+
end
|
108
|
+
end
|
26
109
|
end
|
27
110
|
|
111
|
+
private
|
112
|
+
|
113
|
+
# transforms a value in a custom hash
|
114
|
+
def transform(value)
|
115
|
+
return CustomHash.new(value) if value.kind_of?(Hash) || value.kind_of?(Array)
|
116
|
+
value
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse(val)
|
120
|
+
raise "undefined method: '#{val}'" if val.nil?
|
121
|
+
val
|
122
|
+
end
|
28
123
|
|
29
124
|
|
30
125
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require 'restfulie/client/base'
|
5
|
+
require 'restfulie/client/entry_point'
|
6
|
+
require 'restfulie/client/helper'
|
7
|
+
require 'restfulie/client/instance'
|
8
|
+
require 'restfulie/client/state'
|
9
|
+
|
10
|
+
module Restfulie
|
11
|
+
|
12
|
+
# Extends your class to support restfulie-client side's code.
|
13
|
+
# This will extends Restfulie::Client::Base methods as class methods,
|
14
|
+
# Restfulie::Client::Instance as instance methods and Restfulie::Unmarshalling as class methods.
|
15
|
+
def uses_restfulie
|
16
|
+
extend Restfulie::Client::Base
|
17
|
+
include Restfulie::Client::Instance
|
18
|
+
extend Restfulie::Unmarshalling
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Object.extend Restfulie
|
24
|
+
|
25
|
+
require 'restfulie/unmarshalling'
|
@@ -22,31 +22,49 @@ module Restfulie
|
|
22
22
|
options[:allow] = [options[:allow]] unless options[:allow].kind_of? Array
|
23
23
|
states[name] = options
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# defines a new transition. the transition options works in the same way
|
27
27
|
# that following_transition definition does.
|
28
|
-
def transition(name, options = {}, result = nil, &body)
|
28
|
+
def transition(name = nil, options = {}, result = nil, &body)
|
29
|
+
return TransitionBuilder.new(self) if name.nil?
|
29
30
|
|
30
31
|
transition = Restfulie::Server::Transition.new(name, options, result, body)
|
31
32
|
transitions[name] = transition
|
32
33
|
|
33
|
-
|
34
|
-
|
34
|
+
define_execution_method(self, name, result)
|
35
|
+
define_can_method(self, name)
|
36
|
+
end
|
37
|
+
|
38
|
+
class TransitionBuilder
|
39
|
+
def initialize(type)
|
40
|
+
@type = type
|
41
|
+
end
|
42
|
+
def method_missing(name, *args)
|
43
|
+
@transition = Restfulie::Server::Transition.new(name)
|
44
|
+
@type.transitions[name] = @transition
|
45
|
+
@type.define_can_method(@type, name)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
def at(options)
|
49
|
+
@transition.options = options
|
50
|
+
end
|
51
|
+
def results_in(result)
|
52
|
+
@transition.result = result
|
53
|
+
end
|
54
|
+
|
35
55
|
end
|
36
56
|
|
37
|
-
def
|
38
|
-
|
39
|
-
return nil if type.respond_to?(name)
|
40
|
-
|
57
|
+
def define_execution_method(type, name, result)
|
41
58
|
type.send(:define_method, name) do |*args|
|
42
|
-
self.status = result.to_s unless result
|
43
|
-
end
|
44
|
-
|
59
|
+
self.status = result.to_s unless result.nil?
|
60
|
+
end unless type.respond_to?(name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_can_method(type, name)
|
45
64
|
type.send(:define_method, "can_#{name}?") do
|
46
65
|
transitions = self.available_transitions[:allow]
|
47
66
|
transitions.include? name
|
48
|
-
end
|
49
|
-
|
67
|
+
end unless type.respond_to?("can_#{name}?")
|
50
68
|
end
|
51
69
|
|
52
70
|
end
|
@@ -1,17 +1,33 @@
|
|
1
|
-
require 'restfulie'
|
2
|
-
|
3
1
|
module ActionController
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# renders an specific resource to xml
|
5
|
+
# using any extra options to render it (invoke to_xml).
|
6
|
+
def render_resource(resource, options = {})
|
7
|
+
cache_info = {:etag => resource}
|
8
|
+
cache_info[:last_modified] = resource.updated_at.utc if resource.respond_to? :updated_at
|
9
|
+
if stale? cache_info
|
10
|
+
options[:controller] = self
|
11
|
+
format = (self.params && self.params[:format]) || "xml"
|
12
|
+
if ["xml", "json"].include?(format)
|
13
|
+
render format.to_sym => resource.send(:"to_#{format}", options)
|
14
|
+
else
|
15
|
+
render format.to_sym => resource
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# adds support to rendering resources, i.e.:
|
21
|
+
# render :resource => @order, :with => { :except => [:paid_at] }
|
22
|
+
alias_method :old_render, :render
|
23
|
+
def render(options = nil, extra_options = {}, &block)
|
24
|
+
resource = options[:resource] unless options.nil?
|
25
|
+
unless resource.nil?
|
26
|
+
render_resource(resource, options[:with])
|
27
|
+
else
|
28
|
+
old_render(options, extra_options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
17
33
|
end
|
@@ -2,25 +2,6 @@ module Restfulie
|
|
2
2
|
module Server
|
3
3
|
module Instance
|
4
4
|
|
5
|
-
# Returns an array with extra possible transitions.
|
6
|
-
# Those transitions will be concatenated with any extra transitions provided by your resource through
|
7
|
-
# the use of state and transition definitions.
|
8
|
-
# For every transition its name is the only mandatory field:
|
9
|
-
# options = {}
|
10
|
-
# [:show, options] # will generate a link to your controller's show action
|
11
|
-
#
|
12
|
-
# The options can be used to override restfulie's conventions:
|
13
|
-
# options[:rel] = "refresh" # will create a rel named refresh
|
14
|
-
# options[:action] = "destroy" # will link to the destroy method
|
15
|
-
# options[:controller] = another controller # will use another controller's action
|
16
|
-
#
|
17
|
-
# Any extra options will be passed to the target controller url_for method in order to retrieve
|
18
|
-
# the transition's uri.
|
19
|
-
def following_transitions
|
20
|
-
[]
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
5
|
# returns a list of available transitions for this objects state
|
25
6
|
# TODO rename because it should never be used by the client...
|
26
7
|
def available_transitions
|
@@ -5,8 +5,8 @@ module Restfulie
|
|
5
5
|
|
6
6
|
module Marshalling
|
7
7
|
|
8
|
-
def to_json
|
9
|
-
|
8
|
+
def to_json(options = {})
|
9
|
+
Hash.from_xml(to_xml(options)).to_json
|
10
10
|
end
|
11
11
|
|
12
12
|
# adds a link for each transition to the current xml writer
|
@@ -18,17 +18,17 @@ module Restfulie
|
|
18
18
|
|
19
19
|
# adds a link for this transition to the current xml writer
|
20
20
|
def add_link(transition, xml, options)
|
21
|
-
|
22
21
|
transition = self.class.existing_transitions(transition.to_sym) unless transition.kind_of? Restfulie::Server::Transition
|
23
22
|
transition.add_link_to(xml, self, options)
|
24
|
-
|
25
23
|
end
|
26
24
|
|
25
|
+
# marshalls your object to xml.
|
26
|
+
# adds all links if there are any available.
|
27
27
|
def to_xml(options = {})
|
28
28
|
|
29
29
|
transitions = all_following_transitions
|
30
30
|
return super(options) if transitions.empty? || options[:controller].nil?
|
31
|
-
|
31
|
+
|
32
32
|
options[:skip_types] = true
|
33
33
|
super options do |xml|
|
34
34
|
add_links xml, transitions, options
|
@@ -4,8 +4,10 @@ module Restfulie
|
|
4
4
|
|
5
5
|
# represents a transition on the server side
|
6
6
|
class Transition
|
7
|
-
attr_reader :body, :name
|
8
|
-
|
7
|
+
attr_reader :body, :name
|
8
|
+
attr_writer :options
|
9
|
+
attr_accessor :result
|
10
|
+
def initialize(name, options = {}, result = nil, body = nil)
|
9
11
|
@name = name
|
10
12
|
@options = options
|
11
13
|
@result = result
|
@@ -25,6 +27,18 @@ module Restfulie
|
|
25
27
|
specific_action = action.dup
|
26
28
|
specific_action = @body.call(model) if @body
|
27
29
|
|
30
|
+
# if you use the class level DSL, you will need to add a lambda for instance level accessors:
|
31
|
+
# transition :show, {:action => :show, :foo_id => lambda { |model| model.id }}
|
32
|
+
# but you can replace it for a symbol and defer the model call
|
33
|
+
# transition :show, {:action => :show, :foo_id => :id}
|
34
|
+
specific_action = specific_action.inject({}) do |actions, pair|
|
35
|
+
if pair.last.is_a?( Symbol ) && model.attributes.include?(pair.last)
|
36
|
+
actions.merge!( pair.first => model.send(pair.last) )
|
37
|
+
else
|
38
|
+
actions.merge!( pair.first => pair.last )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
28
42
|
rel = specific_action[:rel] || @name
|
29
43
|
specific_action[:rel] = nil
|
30
44
|
|
@@ -37,8 +51,8 @@ module Restfulie
|
|
37
51
|
xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => uri)
|
38
52
|
end
|
39
53
|
end
|
40
|
-
|
54
|
+
|
41
55
|
end
|
42
56
|
end
|
43
57
|
|
44
|
-
end
|
58
|
+
end
|
@@ -1,10 +1,33 @@
|
|
1
|
-
module
|
2
|
-
|
1
|
+
# module Hashi
|
2
|
+
# class CustomHash
|
3
|
+
# # uses_restfulie
|
4
|
+
# def initialize(h)
|
5
|
+
# @hash = h
|
6
|
+
# link = h['link']
|
7
|
+
# add_transitions([link]) if link.kind_of? Hash
|
8
|
+
# add_transitions(link) if link.kind_of? Array
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# module Jeokkarak
|
14
|
+
# module Base
|
15
|
+
# alias_method :old_from_hash_parse, :from_hash_parse
|
16
|
+
# def from_hash_parse(result,h,key,value)
|
17
|
+
# return old_from_hash_parse(result, h, key, value) if key!='link'
|
18
|
+
# link = h[key]
|
19
|
+
# result.add_transitions([link]) if link.kind_of? Hash
|
20
|
+
# result.add_transitions(link) if link.kind_of? Array
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
3
24
|
|
25
|
+
module Restfulie
|
26
|
+
module Unmarshalling
|
4
27
|
# basic code from Matt Pulver
|
5
28
|
# found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
|
6
29
|
# addapted to support links
|
7
|
-
def
|
30
|
+
def from_hash( hash )
|
8
31
|
h = {}
|
9
32
|
h = hash.dup if hash
|
10
33
|
links = nil
|
@@ -29,18 +52,25 @@ module ActiveRecord
|
|
29
52
|
end
|
30
53
|
result = self.new h
|
31
54
|
if !(links.nil?) && self.include?(Restfulie::Client::Instance)
|
32
|
-
add_transitions(
|
55
|
+
result.add_transitions(links)
|
33
56
|
end
|
34
57
|
result
|
35
58
|
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ActiveRecord
|
63
|
+
class Base
|
64
|
+
extend Restfulie::Unmarshalling
|
65
|
+
# acts_as_jeokkarak
|
36
66
|
|
37
|
-
def self.from_json(
|
38
|
-
from_hash
|
67
|
+
def self.from_json(json)
|
68
|
+
from_hash(safe_json_decode(json).values.first)
|
39
69
|
end
|
40
70
|
|
41
71
|
# The xml has a surrounding class tag (e.g. ship-to),
|
42
72
|
# but the hash has no counterpart (e.g. 'ship_to' => {} )
|
43
|
-
def self.from_xml(
|
73
|
+
def self.from_xml(xml)
|
44
74
|
hash = Hash.from_xml xml
|
45
75
|
head = hash[self.to_s.underscore]
|
46
76
|
result = self.from_hash head
|
@@ -48,13 +78,14 @@ module ActiveRecord
|
|
48
78
|
result._came_from = :xml if self.include?(Restfulie::Client::Instance)
|
49
79
|
result
|
50
80
|
end
|
81
|
+
|
51
82
|
end
|
52
83
|
end
|
53
84
|
|
54
|
-
def safe_json_decode(
|
85
|
+
def safe_json_decode(json)
|
55
86
|
return {} if !json
|
56
87
|
begin
|
57
88
|
ActiveSupport::JSON.decode json
|
58
89
|
rescue ; {} end
|
59
90
|
end
|
60
|
-
# end of code based on Matt Pulver's
|
91
|
+
# end of code based on Matt Pulver's
|
data/lib/restfulie.rb
CHANGED
@@ -1,30 +1,46 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
|
-
require 'restfulie/unmarshalling'
|
4
3
|
|
5
|
-
require 'restfulie/client
|
6
|
-
require 'restfulie/client/helper'
|
7
|
-
require 'restfulie/client/instance'
|
4
|
+
require 'restfulie/client'
|
8
5
|
|
9
6
|
require 'restfulie/server/base'
|
10
7
|
require 'restfulie/server/controller'
|
11
8
|
require 'restfulie/server/instance'
|
12
9
|
require 'restfulie/server/marshalling'
|
13
|
-
require 'restfulie/server/state'
|
14
10
|
require 'restfulie/server/transition'
|
15
11
|
|
16
|
-
|
12
|
+
module Restfulie
|
13
|
+
|
14
|
+
# Sets this class as a restfulie class.
|
15
|
+
# You may pass a block defining your own transitions.
|
16
|
+
#
|
17
|
+
# The transitions added will be concatenated with any extra transitions provided by your resource through
|
18
|
+
# the use of state and transition definitions.
|
19
|
+
# For every transition its name is the only mandatory field:
|
20
|
+
# options = {}
|
21
|
+
# [:show, options] # will generate a link to your controller's show action
|
22
|
+
#
|
23
|
+
# The options can be used to override restfulie's conventions:
|
24
|
+
# options[:rel] = "refresh" # will create a rel named refresh
|
25
|
+
# options[:action] = "destroy" # will link to the destroy method
|
26
|
+
# options[:controller] = another controller # will use another controller's action
|
27
|
+
#
|
28
|
+
# Any extra options will be passed to the target controller url_for method in order to retrieve
|
29
|
+
# the transition's uri.
|
17
30
|
def acts_as_restfulie
|
18
|
-
|
19
|
-
include Restfulie::Server::Base
|
20
|
-
end
|
31
|
+
extend Restfulie::Server::Base
|
21
32
|
include Restfulie::Server::Instance
|
22
33
|
include Restfulie::Server::Marshalling
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
self.send :define_method, :following_transitions do
|
36
|
+
transitions = []
|
37
|
+
yield( self, transitions ) if block_given?
|
38
|
+
transitions
|
27
39
|
end
|
28
|
-
include Restfulie::Client::Instance
|
29
40
|
end
|
30
|
-
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
Object.extend Restfulie
|
45
|
+
|
46
|
+
require 'restfulie/unmarshalling'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restfulie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guilherme Silveira, Caue Guerra
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-12-10 00:00:00 -02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -23,13 +23,15 @@ extra_rdoc_files: []
|
|
23
23
|
|
24
24
|
files:
|
25
25
|
- lib/restfulie/client/base.rb
|
26
|
+
- lib/restfulie/client/entry_point.rb
|
26
27
|
- lib/restfulie/client/helper.rb
|
27
28
|
- lib/restfulie/client/instance.rb
|
29
|
+
- lib/restfulie/client/state.rb
|
30
|
+
- lib/restfulie/client.rb
|
28
31
|
- lib/restfulie/server/base.rb
|
29
32
|
- lib/restfulie/server/controller.rb
|
30
33
|
- lib/restfulie/server/instance.rb
|
31
34
|
- lib/restfulie/server/marshalling.rb
|
32
|
-
- lib/restfulie/server/state.rb
|
33
35
|
- lib/restfulie/server/transition.rb
|
34
36
|
- lib/restfulie/unmarshalling.rb
|
35
37
|
- lib/restfulie.rb
|