restfulie 0.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|