restfulie 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +174 -15
- data/Rakefile +5 -2
- data/lib/restfulie/client/base.rb +76 -0
- data/lib/restfulie/client/helper.rb +11 -0
- data/lib/restfulie/client/instance.rb +32 -0
- data/lib/restfulie/server/base.rb +54 -0
- data/lib/restfulie/server/controller.rb +17 -0
- data/lib/restfulie/server/instance.rb +50 -0
- data/lib/restfulie/server/marshalling.rb +41 -0
- data/lib/restfulie/server/state.rb +19 -0
- data/lib/restfulie/server/transition.rb +44 -0
- data/lib/restfulie/unmarshalling.rb +60 -0
- data/lib/restfulie.rb +25 -259
- metadata +13 -3
data/README.textile
CHANGED
@@ -2,6 +2,8 @@ h1. Quit pretending
|
|
2
2
|
|
3
3
|
CRUD through HTTP is a good step forward to using resources and becoming RESTful, another step further into it is to make use of hypermedia based services and this gem allows you to do it really fast.
|
4
4
|
|
5
|
+
You can read the "article on using the web for real":http://guilhermesilveira.wordpress.com/2009/11/03/quit-pretending-use-the-web-for-real-restfulie/ which gives an introduction to hypermedia/resources/services.
|
6
|
+
|
5
7
|
h2. Why would I use restfulie?
|
6
8
|
|
7
9
|
1. Easy --> writing hypermedia aware resource based clients
|
@@ -10,7 +12,16 @@ h2. Why would I use restfulie?
|
|
10
12
|
4. HATEOAS --> clients you are unaware of will not bother if you change your URIs
|
11
13
|
5. HATEOAS --> services that you consume will not affect your software whenever they change part of their flow or URIs
|
12
14
|
|
13
|
-
|
15
|
+
h2. Could you compare it with Spring or JAX-RS based APIs?
|
16
|
+
|
17
|
+
Restfulie is the first API which tries to somehow implement "Jim Webber":http://jim.webber.name/ and "Ian Robinson":http://iansrobinson.com/ opinion on how RESTFul systems use hypermedia
|
18
|
+
as the way to lead your client's path through a business process.
|
19
|
+
|
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
|
14
25
|
|
15
26
|
Example on accessing a resource and its services through the restfulie API:
|
16
27
|
|
@@ -24,24 +35,26 @@ order.pay payment # sends a post request to pay this orde
|
|
24
35
|
order.cancel # sends a delete request
|
25
36
|
</pre>
|
26
37
|
|
27
|
-
|
38
|
+
h2. Restfulie: server-side
|
28
39
|
|
29
40
|
This is a simple example how to make your state changes available to your resource consumers:
|
30
41
|
|
31
42
|
<pre>
|
32
43
|
class Order < ActiveRecord::Base
|
44
|
+
|
45
|
+
acts_as_restfulie
|
46
|
+
|
33
47
|
def following_transitions
|
34
48
|
transitions = []
|
35
49
|
transitions << [:show, {}]
|
36
50
|
transitions << [:destroy, {}] if can_cancel?
|
37
51
|
transitions << [:pay, {:id => id}] if can_pay?
|
38
|
-
transitions << [:show, {:controller => :payments, :payment_id => payment.id }] if paid?
|
39
52
|
transitions
|
40
53
|
end
|
41
54
|
end
|
42
55
|
</pre>
|
43
56
|
|
44
|
-
*
|
57
|
+
*You might want to create a migration with a string field named status for your resource:*
|
45
58
|
|
46
59
|
<pre>
|
47
60
|
scripts/generate migration add_status_to_order
|
@@ -67,14 +80,14 @@ end
|
|
67
80
|
|
68
81
|
Or simply define a status reader and writer on your own.
|
69
82
|
|
70
|
-
|
71
|
-
|
72
83
|
h2. Restfulie server-side: state machine
|
73
84
|
|
74
85
|
For those willing to implement a more complex or advanced state machine, you can use the dsl-like api:
|
75
86
|
|
76
87
|
<pre>
|
77
88
|
class Order < ActiveRecord::Base
|
89
|
+
acts_as_restfulie
|
90
|
+
|
78
91
|
state :unpaid, :allow => [:latest, :pay, :cancel]
|
79
92
|
state :cancelled, :allow => :latest
|
80
93
|
|
@@ -84,7 +97,7 @@ class Order < ActiveRecord::Base
|
|
84
97
|
end
|
85
98
|
</pre>
|
86
99
|
|
87
|
-
|
100
|
+
h1. Installing
|
88
101
|
|
89
102
|
Just add in your environment.rb the following line:
|
90
103
|
|
@@ -116,6 +129,13 @@ Trying to follow the definition of a RESTful application supporting resources wi
|
|
116
129
|
|
117
130
|
h2. Client Usage
|
118
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
|
+
|
119
139
|
One should first acquire the representation from the server through your common GET request and process it through the usual from_* methods:
|
120
140
|
<pre>xml = Net::HTTP.get(URI.parse('http://www.caelum.com.br/orders/1'))
|
121
141
|
order = Order.from_xml(xml)</pre>
|
@@ -189,14 +209,25 @@ h2. Server-side configuration
|
|
189
209
|
|
190
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.
|
191
211
|
|
192
|
-
h3.
|
212
|
+
h3. Simple usage: following transitions
|
193
213
|
|
194
214
|
The most easy way to use restfulie is to write the *following_transitions* method.
|
195
|
-
|
215
|
+
There are three easy steps to make it work:
|
196
216
|
|
197
|
-
|
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:
|
198
227
|
|
199
228
|
<pre>
|
229
|
+
acts_as_restfulie
|
230
|
+
|
200
231
|
def following_transitions
|
201
232
|
transitions = []
|
202
233
|
transitions << [:show, {}]
|
@@ -204,9 +235,48 @@ def following_transitions
|
|
204
235
|
end
|
205
236
|
</pre>
|
206
237
|
|
207
|
-
|
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:
|
208
252
|
|
209
|
-
<pre
|
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>
|
210
280
|
|
211
281
|
h3. Customizing the rel name
|
212
282
|
|
@@ -239,7 +309,7 @@ def following_transitions
|
|
239
309
|
end
|
240
310
|
</pre>
|
241
311
|
|
242
|
-
h2. Defining the state machine and its transitions
|
312
|
+
h2. Advanced usage: Defining the state machine and its transitions
|
243
313
|
|
244
314
|
The second way of defining your available transitions is to explicitely define the states and transitions.
|
245
315
|
|
@@ -255,6 +325,9 @@ The following example shows all possible states for an order:
|
|
255
325
|
|
256
326
|
<pre>
|
257
327
|
class Order < ActiveRecord::Base
|
328
|
+
|
329
|
+
acts_as_restfulie
|
330
|
+
|
258
331
|
state :unpaid, :allow => [:latest, :pay, :cancel]
|
259
332
|
state :cancelled, :allow => :latest
|
260
333
|
state :received, :allow => [:latest, :check_payment_info]
|
@@ -277,6 +350,9 @@ list of available transition whenever the *can_pay?* method returns true:
|
|
277
350
|
|
278
351
|
<pre>
|
279
352
|
class Order < ActiveRecord::Base
|
353
|
+
|
354
|
+
acts_as_restfulie
|
355
|
+
|
280
356
|
transition :pay, {:action => pay_this_order, :controller => :payments}, :preparing
|
281
357
|
|
282
358
|
def following_transitions
|
@@ -303,6 +379,36 @@ class Order < ActiveRecord::Base
|
|
303
379
|
end
|
304
380
|
end
|
305
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>
|
306
412
|
|
307
413
|
h3. Using xml+rel links instead of atom links
|
308
414
|
|
@@ -331,10 +437,10 @@ h2. Team
|
|
331
437
|
Restfulie was created and is maintained within Caelum by
|
332
438
|
|
333
439
|
Projetct Founder
|
334
|
-
* "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
|
440
|
+
* "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br - twitter:http://www.twitter.com/guilhermecaelum "http://guilhermesilveira.wordpress.com":http://guilhermesilveira.wordpress.com
|
335
441
|
|
336
442
|
Active Commiters
|
337
|
-
* "Caue Guerra":mailto:caue.guerra@gmail.com
|
443
|
+
* "Caue Guerra":mailto:caue.guerra@gmail.com - "http://caueguerra.com/":http://caueguerra.com/
|
338
444
|
* "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
|
339
445
|
|
340
446
|
Contributors
|
@@ -381,9 +487,61 @@ In order to pay do not forget to send the parameter *payment* with a value as
|
|
381
487
|
|
382
488
|
h3. Sources
|
383
489
|
|
490
|
+
You can see an application's source code here, both client and server side were implemented using *restfulie*:
|
491
|
+
|
384
492
|
"Client":http://github.com/caelum/restfulie-client
|
385
493
|
"Server":http://github.com/caelum/restfulie-test
|
386
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
|
387
545
|
|
388
546
|
h2. License
|
389
547
|
|
@@ -403,3 +561,4 @@ h2. License
|
|
403
561
|
* See the License for the specific language governing permissions and
|
404
562
|
* limitations under the License.
|
405
563
|
*/
|
564
|
+
|
data/Rakefile
CHANGED
@@ -5,8 +5,8 @@ require 'rake/gempackagetask'
|
|
5
5
|
require 'spec/rake/spectask'
|
6
6
|
|
7
7
|
GEM = "restfulie"
|
8
|
-
GEM_VERSION = "0.
|
9
|
-
SUMMARY = "
|
8
|
+
GEM_VERSION = "0.3"
|
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"
|
12
12
|
HOMEPAGE = "http://github.com/caelum/restfulie"
|
@@ -49,3 +49,6 @@ end
|
|
49
49
|
|
50
50
|
desc "Builds the project"
|
51
51
|
task :build => :spec
|
52
|
+
|
53
|
+
desc "Default build will run specs"
|
54
|
+
task :default => :spec
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Client
|
3
|
+
module Base
|
4
|
+
|
5
|
+
# translates a response to an object
|
6
|
+
def from_response(res)
|
7
|
+
|
8
|
+
raise "unimplemented content type" if res.content_type!="application/xml"
|
9
|
+
|
10
|
+
hash = Hash.from_xml res.body
|
11
|
+
return hash if hash.keys.length == 0
|
12
|
+
raise "unable to parse an xml with more than one root element" if hash.keys.length>1
|
13
|
+
|
14
|
+
type = hash.keys[0].camelize.constantize
|
15
|
+
type.from_xml(res.body)
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def requisition_method_for(overriden_option,name)
|
20
|
+
basic_mapping = { :delete => Net::HTTP::Delete, :put => Net::HTTP::Put, :get => Net::HTTP::Get, :post => Net::HTTP::Post}
|
21
|
+
defaults = {:destroy => Net::HTTP::Delete, :delete => Net::HTTP::Delete, :cancel => Net::HTTP::Delete,
|
22
|
+
:refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get}
|
23
|
+
|
24
|
+
return basic_mapping[overriden_option.to_sym] if overriden_option
|
25
|
+
defaults[name.to_sym] || Net::HTTP::Post
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_state(transition)
|
29
|
+
name = transition["rel"]
|
30
|
+
|
31
|
+
self.module_eval do
|
32
|
+
|
33
|
+
def temp_method(options = {}, &block)
|
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
|
54
|
+
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
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Client
|
3
|
+
module Instance
|
4
|
+
|
5
|
+
# list of possible states to access
|
6
|
+
attr_accessor :_possible_states
|
7
|
+
|
8
|
+
# which content-type generated this data
|
9
|
+
attr_accessor :_came_from
|
10
|
+
|
11
|
+
|
12
|
+
def invoke_remote_transition(name, options, block)
|
13
|
+
|
14
|
+
method = self.class.requisition_method_for options[:method], name
|
15
|
+
|
16
|
+
url = URI.parse(_possible_states[name]["href"])
|
17
|
+
req = method.new(url.path)
|
18
|
+
req.body = options[:data] if options[:data]
|
19
|
+
req.add_field("Accept", "application/xml") if self._came_from == :xml
|
20
|
+
|
21
|
+
response = Net::HTTP.new(url.host, url.port).request(req)
|
22
|
+
|
23
|
+
return block.call(response) if block
|
24
|
+
return response if method != Net::HTTP::Get
|
25
|
+
self.class.from_response response
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Server
|
3
|
+
module Base
|
4
|
+
|
5
|
+
# returns the definition for the transaction
|
6
|
+
def existing_transitions(name)
|
7
|
+
transitions[name]
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns a hash of all possible transitions: Restfulie::Server::Transition
|
11
|
+
def transitions
|
12
|
+
@transitions ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns a hash of all possible states
|
16
|
+
def states
|
17
|
+
@states ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# adds a new state to the list of possible states
|
21
|
+
def state(name, options = {})
|
22
|
+
options[:allow] = [options[:allow]] unless options[:allow].kind_of? Array
|
23
|
+
states[name] = options
|
24
|
+
end
|
25
|
+
|
26
|
+
# defines a new transition. the transition options works in the same way
|
27
|
+
# that following_transition definition does.
|
28
|
+
def transition(name, options = {}, result = nil, &body)
|
29
|
+
|
30
|
+
transition = Restfulie::Server::Transition.new(name, options, result, body)
|
31
|
+
transitions[name] = transition
|
32
|
+
|
33
|
+
define_methods_for(self, name, result)
|
34
|
+
controller_name = (self.name + "Controller")
|
35
|
+
end
|
36
|
+
|
37
|
+
def define_methods_for(type, name, result)
|
38
|
+
|
39
|
+
return nil if type.respond_to?(name)
|
40
|
+
|
41
|
+
type.send(:define_method, name) do |*args|
|
42
|
+
self.status = result.to_s unless result == nil
|
43
|
+
end
|
44
|
+
|
45
|
+
type.send(:define_method, "can_#{name}?") do
|
46
|
+
transitions = self.available_transitions[:allow]
|
47
|
+
transitions.include? name
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'restfulie'
|
2
|
+
|
3
|
+
module ActionController
|
4
|
+
# class Base
|
5
|
+
# alias_method :simple_render, :render
|
6
|
+
# def render(options={})
|
7
|
+
# debugger
|
8
|
+
# if options[:xml]
|
9
|
+
# resource = options[:xml]
|
10
|
+
# debugger
|
11
|
+
# return simple_render(options) if resource.kind_of?(String) || !resource.class.respond_to?(:is_acting_as_restfulie)
|
12
|
+
# options[:xml] = resource.to_xml(:controller => self)
|
13
|
+
# end
|
14
|
+
# simple_render(options)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Restfulie
|
2
|
+
module Server
|
3
|
+
module Instance
|
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
|
+
# returns a list of available transitions for this objects state
|
25
|
+
# TODO rename because it should never be used by the client...
|
26
|
+
def available_transitions
|
27
|
+
status_available = respond_to?(:status) && status!=nil
|
28
|
+
return {:allow => []} unless status_available
|
29
|
+
self.class.states[self.status.to_sym] || {:allow => []}
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns a list containing all available transitions for this object's state
|
33
|
+
def all_following_transitions
|
34
|
+
all = [] + available_transitions[:allow]
|
35
|
+
following_transitions.each do |t|
|
36
|
+
t = Restfulie::Server::Transition.new(t[0], t[1], t[2], nil) if t.kind_of? Array
|
37
|
+
all << t
|
38
|
+
end
|
39
|
+
all
|
40
|
+
end
|
41
|
+
|
42
|
+
# checks if its possible to execute such transition and, if it is, executes it
|
43
|
+
def move_to(name)
|
44
|
+
raise "Current state #{status} is invalid in order to execute #{name}. It must be one of #{transitions}" unless available_transitions[:allow].include? name
|
45
|
+
self.class.transitions[name].execute_at self
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
module Restfulie
|
3
|
+
|
4
|
+
module Server
|
5
|
+
|
6
|
+
module Marshalling
|
7
|
+
|
8
|
+
def to_json
|
9
|
+
super :methods => :following_states
|
10
|
+
end
|
11
|
+
|
12
|
+
# adds a link for each transition to the current xml writer
|
13
|
+
def add_links(xml, all, options)
|
14
|
+
all.each do |transition|
|
15
|
+
add_link(transition, xml, options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# adds a link for this transition to the current xml writer
|
20
|
+
def add_link(transition, xml, options)
|
21
|
+
|
22
|
+
transition = self.class.existing_transitions(transition.to_sym) unless transition.kind_of? Restfulie::Server::Transition
|
23
|
+
transition.add_link_to(xml, self, options)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_xml(options = {})
|
28
|
+
|
29
|
+
transitions = all_following_transitions
|
30
|
+
return super(options) if transitions.empty? || options[:controller].nil?
|
31
|
+
|
32
|
+
options[:skip_types] = true
|
33
|
+
super options do |xml|
|
34
|
+
add_links xml, transitions, options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Restfulie
|
2
|
+
|
3
|
+
module Server
|
4
|
+
|
5
|
+
# adds respond_to and has_state methods to resources
|
6
|
+
module State
|
7
|
+
|
8
|
+
# overrides the respond_to? method to check if this method is contained or was defined by a state
|
9
|
+
def respond_to?(sym)
|
10
|
+
has_state(sym.to_s) || super(sym)
|
11
|
+
end
|
12
|
+
|
13
|
+
# returns true if this resource has a state named name
|
14
|
+
def has_state(name)
|
15
|
+
!@_possible_states[name].nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Restfulie
|
2
|
+
|
3
|
+
module Server
|
4
|
+
|
5
|
+
# represents a transition on the server side
|
6
|
+
class Transition
|
7
|
+
attr_reader :body, :name, :result
|
8
|
+
def initialize(name, options, result, body)
|
9
|
+
@name = name
|
10
|
+
@options = options
|
11
|
+
@result = result
|
12
|
+
@body = body
|
13
|
+
end
|
14
|
+
def action
|
15
|
+
@options || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# executes this transition in a resource
|
19
|
+
def execute_at(target_object)
|
20
|
+
target_object.status = result.to_s unless result.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
# adds a link to this transition's uri on a xml writer
|
24
|
+
def add_link_to(xml, model, options)
|
25
|
+
specific_action = action.dup
|
26
|
+
specific_action = @body.call(model) if @body
|
27
|
+
|
28
|
+
rel = specific_action[:rel] || @name
|
29
|
+
specific_action[:rel] = nil
|
30
|
+
|
31
|
+
specific_action[:action] ||= @name
|
32
|
+
uri = options[:controller].url_for(specific_action)
|
33
|
+
|
34
|
+
if options[:use_name_based_link]
|
35
|
+
xml.tag!(rel, uri)
|
36
|
+
else
|
37
|
+
xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => uri)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# basic code from Matt Pulver
|
5
|
+
# found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
|
6
|
+
# addapted to support links
|
7
|
+
def self.from_hash( hash )
|
8
|
+
h = {}
|
9
|
+
h = hash.dup if hash
|
10
|
+
links = nil
|
11
|
+
h.each do |key,value|
|
12
|
+
case value.class.to_s
|
13
|
+
when 'Array'
|
14
|
+
if key=="link"
|
15
|
+
links = h[key]
|
16
|
+
h.delete("link")
|
17
|
+
else
|
18
|
+
h[key].map! { |e| reflect_on_association(key.to_sym ).klass.from_hash e }
|
19
|
+
end
|
20
|
+
when /\AHash(WithIndifferentAccess)?\Z/
|
21
|
+
if key=="link"
|
22
|
+
links = [h[key]]
|
23
|
+
h.delete("link")
|
24
|
+
else
|
25
|
+
h[key] = reflect_on_association(key.to_sym ).klass.from_hash value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
h.delete("xmlns") if key=="xmlns"
|
29
|
+
end
|
30
|
+
result = self.new h
|
31
|
+
if !(links.nil?) && self.include?(Restfulie::Client::Instance)
|
32
|
+
add_transitions(result, links)
|
33
|
+
end
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.from_json( json )
|
38
|
+
from_hash safe_json_decode( json )
|
39
|
+
end
|
40
|
+
|
41
|
+
# The xml has a surrounding class tag (e.g. ship-to),
|
42
|
+
# but the hash has no counterpart (e.g. 'ship_to' => {} )
|
43
|
+
def self.from_xml( xml )
|
44
|
+
hash = Hash.from_xml xml
|
45
|
+
head = hash[self.to_s.underscore]
|
46
|
+
result = self.from_hash head
|
47
|
+
return nil if result.nil?
|
48
|
+
result._came_from = :xml if self.include?(Restfulie::Client::Instance)
|
49
|
+
result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def safe_json_decode( json )
|
55
|
+
return {} if !json
|
56
|
+
begin
|
57
|
+
ActiveSupport::JSON.decode json
|
58
|
+
rescue ; {} end
|
59
|
+
end
|
60
|
+
# end of code based on Matt Pulver's
|
data/lib/restfulie.rb
CHANGED
@@ -1,264 +1,30 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require 'uri'
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return super if possible_following.empty?
|
25
|
-
|
26
|
-
possible_following.each do |possible|
|
27
|
-
if possible.class.name=="Array"
|
28
|
-
name = possible[0]
|
29
|
-
result = [possible[1], nil]
|
30
|
-
else
|
31
|
-
name = possible
|
32
|
-
result = self.class._transitions(name.to_sym)
|
33
|
-
end
|
34
|
-
|
35
|
-
if result[0]
|
36
|
-
action = result[0]
|
37
|
-
body = result[1]
|
38
|
-
action = body.call(self) if body
|
39
|
-
|
40
|
-
rel = action[:rel] || name || action[:action]
|
41
|
-
action[:rel] = nil
|
42
|
-
else
|
43
|
-
action = {}
|
44
|
-
rel = name
|
45
|
-
end
|
46
|
-
|
47
|
-
action[:action] ||= name
|
48
|
-
translate_href = controller.url_for(action)
|
49
|
-
if options[:use_name_based_link]
|
50
|
-
xml.tag!(rel, translate_href)
|
51
|
-
else
|
52
|
-
xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => translate_href)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def create_method(name, &block)
|
59
|
-
self.class.send(:define_method, name, &block)
|
60
|
-
end
|
61
|
-
|
62
|
-
def move_to(name)
|
63
|
-
transitions = self.class._transitions_for(self.status.to_sym)[:allow]
|
64
|
-
raise "Current state #{status} is invalid in order to execute #{name}. It must be one of #{transitions}" unless transitions.include? name
|
65
|
-
result = self.class._transitions(name)[2]
|
66
|
-
self.status = result.to_s unless result.nil?
|
3
|
+
require 'restfulie/unmarshalling'
|
4
|
+
|
5
|
+
require 'restfulie/client/base'
|
6
|
+
require 'restfulie/client/helper'
|
7
|
+
require 'restfulie/client/instance'
|
8
|
+
|
9
|
+
require 'restfulie/server/base'
|
10
|
+
require 'restfulie/server/controller'
|
11
|
+
require 'restfulie/server/instance'
|
12
|
+
require 'restfulie/server/marshalling'
|
13
|
+
require 'restfulie/server/state'
|
14
|
+
require 'restfulie/server/transition'
|
15
|
+
|
16
|
+
class Class
|
17
|
+
def acts_as_restfulie
|
18
|
+
class << self
|
19
|
+
include Restfulie::Server::Base
|
20
|
+
end
|
21
|
+
include Restfulie::Server::Instance
|
22
|
+
include Restfulie::Server::Marshalling
|
67
23
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
module ActiveRecord
|
72
|
-
class Base
|
73
|
-
|
74
|
-
include Restfulie
|
75
|
-
attr_accessor :_possible_states
|
76
|
-
attr_accessor :_came_from
|
77
|
-
|
78
|
-
def self._transitions_for(state)
|
79
|
-
@@states[state]
|
80
|
-
end
|
81
|
-
|
82
|
-
def self._transitions(name)
|
83
|
-
[@@transitions[name], @@bodies[name], @@results[name]]
|
84
|
-
end
|
85
|
-
|
86
|
-
@@states = {}
|
87
|
-
@@transitions = {}
|
88
|
-
@@bodies = {}
|
89
|
-
@@results = {}
|
90
|
-
|
91
|
-
def self.state(name, options)
|
92
|
-
if name.class==Array
|
93
|
-
name.each do |simple|
|
94
|
-
self.state(simple, options)
|
95
|
-
end
|
96
|
-
else
|
97
|
-
options[:allow] = [options[:allow]] unless options[:allow].class == Array
|
98
|
-
@@states[name] = options
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def self.transition(name, options = {}, result = nil, &body)
|
103
|
-
@@transitions[name] = options
|
104
|
-
@@bodies[name] = body
|
105
|
-
@@results[name] = result unless result == nil
|
106
|
-
defined = self.respond_to?(name)
|
107
|
-
if !defined
|
108
|
-
self.send(:define_method, name) do |*args|
|
109
|
-
self.status = result.to_s unless result == nil
|
110
|
-
end
|
111
|
-
self.send(:define_method, "can_#{name}2?") do
|
112
|
-
puts "executing can_#{name}2?"
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def self.add_states(result, states)
|
118
|
-
result._possible_states = {}
|
119
|
-
states.each do |state|
|
120
|
-
result._possible_states[state["rel"]] = state
|
121
|
-
end
|
122
|
-
def result.respond_to?(sym)
|
123
|
-
has_state(sym.to_s) || super(sym)
|
124
|
-
end
|
125
|
-
|
126
|
-
def result.has_state(name)
|
127
|
-
!@_possible_states[name].nil?
|
128
|
-
end
|
129
|
-
|
130
|
-
states.each do |state|
|
131
|
-
name = state["rel"]
|
132
|
-
self.module_eval do
|
133
|
-
def current_method
|
134
|
-
caller[0]=~/`(.*?)'/
|
135
|
-
$1
|
136
|
-
end
|
137
|
-
def temp_method(options = {}, &block)
|
138
|
-
name = current_method
|
139
|
-
state = _possible_states[name]
|
140
|
-
data = options[:data] || {}
|
141
|
-
url = URI.parse(state["href"])
|
142
|
-
get = false
|
143
|
-
|
144
|
-
# gs: i dont know how to meta play here! i suck
|
145
|
-
if options[:method]=="delete"
|
146
|
-
req = Net::HTTP::Delete.new(url.path)
|
147
|
-
elsif options[:method]=="put"
|
148
|
-
req = Net::HTTP::Put.new(url.path)
|
149
|
-
elsif options[:method]=="get"
|
150
|
-
req = Net::HTTP::Get.new(url.path)
|
151
|
-
get = true
|
152
|
-
elsif options[:method]=="post"
|
153
|
-
req = Net::HTTP::Post.new(url.path)
|
154
|
-
elsif ['destroy','delete','cancel'].include? name
|
155
|
-
req = Net::HTTP::Delete.new(url.path)
|
156
|
-
elsif ['refresh', 'reload', 'show', 'latest'].include? name
|
157
|
-
req = Net::HTTP::Get.new(url.path)
|
158
|
-
get = true
|
159
|
-
else
|
160
|
-
req = Net::HTTP::Post.new(url.path)
|
161
|
-
end
|
162
|
-
|
163
|
-
req.set_form_data(data)
|
164
|
-
req.add_field("Accept", "text/xml") if _came_from == :xml
|
165
|
-
|
166
|
-
http = Net::HTTP.new(url.host, url.port)
|
167
|
-
response = http.request(req)
|
168
|
-
return yield(response) if !block.nil?
|
169
|
-
if get
|
170
|
-
case response.content_type
|
171
|
-
when "application/xml"
|
172
|
-
content = response.body
|
173
|
-
hash = Hash.from_xml content
|
174
|
-
return hash if hash.keys.length == 0
|
175
|
-
raise "unable to parse an xml with more than one root element" if hash.keys.length>1
|
176
|
-
key = hash.keys[0]
|
177
|
-
type = key.camelize.constantize
|
178
|
-
return type.from_xml content
|
179
|
-
else
|
180
|
-
raise :unknown_content_type
|
181
|
-
end
|
182
|
-
end
|
183
|
-
response
|
184
|
-
|
185
|
-
end
|
186
|
-
alias_method name, :temp_method
|
187
|
-
undef :temp_method
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
result
|
192
|
-
end
|
193
|
-
|
194
|
-
def self.from_web(uri)
|
195
|
-
url = URI.parse(uri)
|
196
|
-
req = Net::HTTP::Get.new(url.path)
|
197
|
-
http = Net::HTTP.new(url.host, url.port)
|
198
|
-
res = http.request(req)
|
199
|
-
raise :invalid_request, res if res.code != "200"
|
200
|
-
case res.content_type
|
201
|
-
when "application/xml"
|
202
|
-
self.from_xml res.body
|
203
|
-
when "application/json"
|
204
|
-
self.from_json res.body
|
205
|
-
else
|
206
|
-
raise :unknown_content_type
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
# basic code from Matt Pulver
|
211
|
-
# found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
|
212
|
-
# addapted to support links
|
213
|
-
def self.from_hash( hash )
|
214
|
-
h = {}
|
215
|
-
h = hash.dup if hash
|
216
|
-
links = nil
|
217
|
-
h.each do |key,value|
|
218
|
-
case value.class.to_s
|
219
|
-
when 'Array'
|
220
|
-
if key=="link"
|
221
|
-
links = h[key]
|
222
|
-
h.delete("link")
|
223
|
-
else
|
224
|
-
h[key].map! { |e| reflect_on_association(key.to_sym ).klass.from_hash e }
|
225
|
-
end
|
226
|
-
when /\AHash(WithIndifferentAccess)?\Z/
|
227
|
-
if key=="link"
|
228
|
-
links = [h[key]]
|
229
|
-
h.delete("link")
|
230
|
-
else
|
231
|
-
h[key] = reflect_on_association(key.to_sym ).klass.from_hash value
|
232
|
-
end
|
233
|
-
end
|
234
|
-
h.delete("xmlns") if key=="xmlns"
|
235
|
-
end
|
236
|
-
result = self.new h
|
237
|
-
add_states(result, links) unless links.nil?
|
238
|
-
result
|
239
|
-
end
|
240
|
-
|
241
|
-
def self.from_json( json )
|
242
|
-
from_hash safe_json_decode( json )
|
243
|
-
end
|
244
|
-
|
245
|
-
# The xml has a surrounding class tag (e.g. ship-to),
|
246
|
-
# but the hash has no counterpart (e.g. 'ship_to' => {} )
|
247
|
-
def self.from_xml( xml )
|
248
|
-
hash = Hash.from_xml xml
|
249
|
-
head = hash[self.to_s.underscore]
|
250
|
-
result = self.from_hash head
|
251
|
-
return nil if result.nil?
|
252
|
-
result._came_from = :xml
|
253
|
-
result
|
24
|
+
def uses_restfulie
|
25
|
+
class << self
|
26
|
+
include Restfulie::Client::Base
|
254
27
|
end
|
28
|
+
include Restfulie::Client::Instance
|
255
29
|
end
|
256
|
-
end
|
257
|
-
|
258
|
-
def safe_json_decode( json )
|
259
|
-
return {} if !json
|
260
|
-
begin
|
261
|
-
ActiveSupport::JSON.decode json
|
262
|
-
rescue ; {} end
|
263
|
-
end
|
264
|
-
# end of code based on Matt Pulver's
|
30
|
+
end
|
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: "0.
|
4
|
+
version: "0.3"
|
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-11-
|
12
|
+
date: 2009-11-25 00:00:00 -02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -22,6 +22,16 @@ extensions: []
|
|
22
22
|
extra_rdoc_files: []
|
23
23
|
|
24
24
|
files:
|
25
|
+
- lib/restfulie/client/base.rb
|
26
|
+
- lib/restfulie/client/helper.rb
|
27
|
+
- lib/restfulie/client/instance.rb
|
28
|
+
- lib/restfulie/server/base.rb
|
29
|
+
- lib/restfulie/server/controller.rb
|
30
|
+
- lib/restfulie/server/instance.rb
|
31
|
+
- lib/restfulie/server/marshalling.rb
|
32
|
+
- lib/restfulie/server/state.rb
|
33
|
+
- lib/restfulie/server/transition.rb
|
34
|
+
- lib/restfulie/unmarshalling.rb
|
25
35
|
- lib/restfulie.rb
|
26
36
|
- Rakefile
|
27
37
|
- README.textile
|
@@ -52,6 +62,6 @@ rubyforge_project:
|
|
52
62
|
rubygems_version: 1.3.5
|
53
63
|
signing_key:
|
54
64
|
specification_version: 3
|
55
|
-
summary:
|
65
|
+
summary: Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side).
|
56
66
|
test_files: []
|
57
67
|
|