restfulie 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.textile +305 -26
  2. data/Rakefile +1 -1
  3. data/lib/restfulie.rb +88 -18
  4. metadata +2 -2
data/README.textile CHANGED
@@ -1,13 +1,21 @@
1
1
  h1. Quit pretending
2
2
 
3
- CRUD through HTTP is a good step forward to using resources and becoming RESTFul, another step further into is to make use of hypermedia based services and this gem allows you to do it really fast.
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
+
5
+ h2. Why would I use restfulie?
6
+
7
+ 1. Easy --> writing hypermedia aware resource based clients
8
+ 2. Easy --> hypermedia aware resource based services
9
+ 3. Small -> it's not a bloated solution with a huge list of APIs
10
+ 4. HATEOAS --> clients you are unaware of will not bother if you change your URIs
11
+ 5. HATEOAS --> services that you consume will not affect your software whenever they change part of their flow or URIs
4
12
 
5
13
  h1. Restfulie: client-side
6
14
 
7
- Here you can see how to access a resource and its services through the restfulie API:
15
+ Example on accessing a resource and its services through the restfulie API:
8
16
 
9
17
  <pre>
10
- order = Order.from_web resource_uri # retrieves order resource (xml/atom/json support)
18
+ order = Order.from_web resource_uri
11
19
 
12
20
  puts "Order price is #{order.price}"
13
21
 
@@ -21,14 +29,59 @@ h1. Restfulie: server-side
21
29
  This is a simple example how to make your state changes available to your resource consumers:
22
30
 
23
31
  <pre>
24
- class Order < ActiveRecord::Base
25
- def following_states
26
- states = [ {:controller => :orders, :action => :show } ]
27
- states << {:controller => :orders, :action => :destroy} if can_cancel?
28
- states << {:controller => :orders, :action => :pay, :id => id} if can_pay?
29
- states << {:controller => :payments, :action => :show, :payment_id => payment.id } if paied?
30
- states
31
- end
32
+ class Order < ActiveRecord::Base
33
+ def following_transitions
34
+ transitions = []
35
+ transitions << [:show, {}]
36
+ transitions << [:destroy, {}] if can_cancel?
37
+ transitions << [:pay, {:id => id}] if can_pay?
38
+ transitions << [:show, {:controller => :payments, :payment_id => payment.id }] if paid?
39
+ transitions
40
+ end
41
+ end
42
+ </pre>
43
+
44
+ *Do not forget to create a migration with a string field named status for your resource:*
45
+
46
+ <pre>
47
+ scripts/generate migration add_status_to_order
48
+ </pre>
49
+
50
+ Content:
51
+
52
+ <pre>
53
+ class AddStatusToOrder < ActiveRecord::Migration
54
+ def self.up
55
+ add_column :orders, :status, :string
56
+ Order.all.each do |order|
57
+ order.status = "unpaid"
58
+ order.save
59
+ end
60
+ end
61
+
62
+ def self.down
63
+ remove_column :orders, :status
64
+ end
65
+ end
66
+ </pre>
67
+
68
+ Or simply define a status reader and writer on your own.
69
+
70
+
71
+
72
+ h2. Restfulie server-side: state machine
73
+
74
+ For those willing to implement a more complex or advanced state machine, you can use the dsl-like api:
75
+
76
+ <pre>
77
+ class Order < ActiveRecord::Base
78
+ state :unpaid, :allow => [:latest, :pay, :cancel]
79
+ state :cancelled, :allow => :latest
80
+
81
+ transition :latest, {:action => :show}
82
+ transition :cancel, {:action => :destroy}, :cancelled
83
+ transition :pay, {}, :preparing
84
+ end
32
85
  </pre>
33
86
 
34
87
  h2. Installing
@@ -39,33 +92,35 @@ Just add in your environment.rb the following line:
39
92
  config.gem "restfulie", :source => "http://gemcutter.org"
40
93
  </pre>
41
94
 
42
- Execute:
95
+ And then execute:
43
96
  <pre>rake gems:install</pre>
44
97
 
45
- or, if you prefer to install as a plugin:
98
+ or, if you prefer to install it as a plugin:
46
99
 
47
- script/plugin install git://github.com/caelum/restfulie.git
100
+ <pre>script/plugin install git://github.com/caelum/restfulie.git</pre>
48
101
 
49
- h2. Typical Restful Example
102
+ h2. Typical hypermedia aware example
50
103
 
51
- Trying to follow the definition of a restful application supporting resources with hypermedia content, a typical restful resource would be:
104
+ Trying to follow the definition of a RESTful application supporting resources with hypermedia content, a resource would be:
52
105
 
53
106
  <pre>
54
107
  <order>
55
108
  <product>basic rails course</product>
56
- <product>restful training</product>
109
+ <product>RESTful training</product>
57
110
  <atom:link rel="refresh" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
58
111
  <atom:link rel="update" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
59
- <atom:link rel="pay" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
112
+ <atom:link rel="pay" href="http://www.caelum.com.br/orders/1/pay" xmlns:atom="http://www.w3.org/2005/Atom"/>
60
113
  <atom:link rel="destroy" href="http://www.caelum.com.br/orders/1" xmlns:atom="http://www.w3.org/2005/Atom"/>
61
114
  </order>
62
115
  </pre>
63
116
 
64
117
  h2. Client Usage
65
118
 
66
- One should first acquire the representation from the server through your common GET process and process it through the usual from_* methods:
119
+ One should first acquire the representation from the server through your common GET request and process it through the usual from_* methods:
67
120
  <pre>xml = Net::HTTP.get(URI.parse('http://www.caelum.com.br/orders/1'))
68
121
  order = Order.from_xml(xml)</pre>
122
+ or use the restfulie *from_web*:
123
+ <pre>order = Order.from_web 'http://www.caelum.com.br/orders/1'</pre>
69
124
 
70
125
  And now you can invoke all those actions in order to change your resource's state:
71
126
 
@@ -73,7 +128,7 @@ And now you can invoke all those actions in order to change your resource's stat
73
128
  order.refresh
74
129
  order.update
75
130
  order.destroy
76
- order.pay(payment)
131
+ order.pay
77
132
  </pre>
78
133
 
79
134
  Note that:
@@ -84,28 +139,252 @@ Note that:
84
139
 
85
140
  h2. Resource format support
86
141
 
87
- Restfulie currently supports xml+atom and will soon expand its support to xml+rel links and json+links supports. Those new formats will also support automatic http verb detection - if possible and reasonable.
142
+ Restfulie currently supports full xml+atom, partial xml+rel and will soon expand its support to json+links.
88
143
 
89
144
 
90
145
  h2. Help
91
146
 
92
- If you are looking for or want to help, let us know at the mailing list: http://groups.google.com/group/restfulie
147
+ If you are looking for or want to help, let us know at the mailing list:
148
+
149
+ "http://groups.google.com/group/restfulie":http://groups.google.com/group/restfulie
150
+
151
+ h2. Client-side configuration: how to customize your request
93
152
 
94
- h2. How to customize your request
153
+ h3. HTTP verbs
95
154
 
96
155
  By default, restfulie uses the following table:
97
156
 
98
157
  * destroy, cancel and delete send a DELETE request
99
- * update sends a POST request
100
- * refresh, reload, show sends a GET request
158
+ * update sends a PUT request
159
+ * refresh, reload, show, latest sends a GET request
101
160
  * other methods sends a POST request
102
161
 
103
162
  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':
104
163
 
164
+ <pre>order.update(:method=>"post")</pre>
165
+
166
+ h3. Request parameters
167
+
168
+ If you want to send extra parameters, you can do it through the *data* parameter:
169
+
170
+ <pre>order.pay(:data => {:payment => my_payment})</pre>
171
+
172
+ The parameters will be serialized either to xml or json according to which format was used to deserialize the order at first place.
173
+
174
+ h3. Executing another GET request
175
+
176
+ If your method executes another GET request, it will automatically deserialize its result as:
177
+
178
+ <pre>order = Order.from_web order_uri
179
+ payment = order.check_payment_info</pre>
180
+
181
+ 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
182
+
183
+ <pre>order = Order.from_web order_uri
184
+ successful = order.check_payment_info do |response|
185
+ return response.code==200
186
+ end</pre>
187
+
188
+ h2. Server-side configuration
189
+
190
+ 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
+
192
+ h3. The following available transitions method
193
+
194
+ The most easy way to use restfulie is to write the *following_transitions* method.
195
+ It should return a list of possible transitions, where each transition is identified by an array of its name and definition.
196
+
197
+ The next example shows how to define a transition which will map to the current controller, action show:
198
+
199
+ <pre>
200
+ def following_transitions
201
+ transitions = []
202
+ transitions << [:show, {}]
203
+ transitions
204
+ end
205
+ </pre>
206
+
207
+ Which will generate an hyperlink as
208
+
209
+ <pre><atom:link rel="show" rel="http://yourserver/orders/15" /></pre>
210
+
211
+ h3. Customizing the rel name
212
+
213
+ You can also override the action used, but still keep the rel
214
+
105
215
  <pre>
106
- order.update(:method=>"post")
216
+ def following_transitions
217
+ transitions = []
218
+ transitions << [:cancel, { :action => :destroy }]
219
+ transitions
220
+ end
107
221
  </pre>
108
222
 
223
+ Which will generate an hyperlink as
224
+
225
+ <pre><atom:link rel="cancel" rel="http://yourserver/orders/15" /></pre>
226
+
227
+ h3. Example
228
+
229
+ A full example showing all capabilities of this method follows:
230
+
231
+ <pre>
232
+ def following_transitions
233
+ transitions = []
234
+ transitions << [:show, {}]
235
+ transitions << [:destroy, {}] if can_cancel?
236
+ transitions << [:pay, {:id => id}] if can_pay?
237
+ transitions << [:show, {:controller => :payments, :payment_id => payment.id }] if paid?
238
+ transitions
239
+ end
240
+ </pre>
241
+
242
+ h2. Defining the state machine and its transitions
243
+
244
+ The second way of defining your available transitions is to explicitely define the states and transitions.
245
+
246
+ By using this approach, one has to define a new column named *status* in a database migration file.
247
+
248
+ The first step involves defining all your states, each one with its own name and possible transitions, as:
249
+
250
+ <pre>
251
+ state :state_name, :allow => [ :first_transition_name, :second_transition_name]
252
+ </pre>
253
+
254
+ The following example shows all possible states for an order:
255
+
256
+ <pre>
257
+ class Order < ActiveRecord::Base
258
+ state :unpaid, :allow => [:latest, :pay, :cancel]
259
+ state :cancelled, :allow => :latest
260
+ state :received, :allow => [:latest, :check_payment_info]
261
+ state :preparing, :allow => [:latest, :check_payment_info]
262
+ state :ready, :allow => [:latest, :receive, :check_payment_info]
263
+ end
264
+ </pre>
265
+
266
+ Now its time to define which controller and action each transition invokes, in a much similar way to
267
+ the transition definitions in the following_transitions method:
268
+
269
+ <pre>
270
+ class Order < ActiveRecord::Base
271
+ end
272
+ </pre>
273
+
274
+ Once a transition has been given a name, its name can be used in the following_transitions method also.
275
+ The next example does not configure the transition because it was already defined, only adding it to the
276
+ list of available transition whenever the *can_pay?* method returns true:
277
+
278
+ <pre>
279
+ class Order < ActiveRecord::Base
280
+ transition :pay, {:action => pay_this_order, :controller => :payments}, :preparing
281
+
282
+ def following_transitions
283
+ transitions = []
284
+ transitions << :pay if can_pay?
285
+ transitions
286
+ end
287
+ end
288
+ </pre>
289
+
290
+ Note that whenever one defines a transition, there is a third - optional - argument, this is the
291
+ transition's target's state. Whenever the method *order.pay* method is invoked in the *server*, it will
292
+ automatically change the order's status to *preparing*.
293
+
294
+ You can download the server side example to see the complete code.
295
+
296
+ The last usage of the transition definition involves passing a block which receives the element in which
297
+ 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:
298
+
299
+ <pre>
300
+ class Order < ActiveRecord::Base
301
+ transition :check_payment_info do |order|
302
+ {:controller => :payments, :action => :show, :order_id => order.id, :payment_id => order.payments[0].id, :rel => "check_payment_info"}
303
+ end
304
+ end
305
+ </pre>
306
+
307
+ h3. Using xml+rel links instead of atom links
308
+
309
+ Atom is everywhere and can be consumed by a number of existing tools but if your system wants to supply its
310
+ services through commons rel+link xml as
311
+
312
+ <pre>
313
+ <order>
314
+ <product>basic rails course</product>
315
+ <product>RESTful training</product>
316
+ <refresh>http://www.caelum.com.br/orders/1</refresh>
317
+ <update>http://www.caelum.com.br/orders/1</update>
318
+ <pay>http://www.caelum.com.br/orders/1/pay</pay>
319
+ <destroy>http://www.caelum.com.br/orders/1</destroy>
320
+ </order>
321
+ </pre>
322
+
323
+ You can do it by passing the *use_name_based_link* argument:
324
+
325
+ <pre>
326
+ order.to_xml(:controller => my_controller, :use_name_based_link => true)
327
+ </pre>
328
+
329
+ h2. Team
330
+
331
+ Restfulie was created and is maintained within Caelum by
332
+
333
+ Projetct Founder
334
+ * "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
335
+
336
+ Active Commiters
337
+ * "Caue Guerra":mailto:caue.guerra@gmail.com
338
+ * "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
339
+
340
+ Contributors
341
+ * Diego Carrion
342
+ * Leandro Silva
343
+ * Gavin-John Noonan
344
+
345
+ h2. Try it online
346
+
347
+ We have a live example of a server implementation using a resource+hypermedia course ordering system available.
348
+
349
+ Follow the steps below to try out the system:
350
+
351
+ * "Access the server system":http://restfulie-test.heroku.com
352
+ * Create a couple of trainings
353
+ * Create an order
354
+ * Access the order listing and retrieve its xml link
355
+
356
+ And now you can try the restfulie client api through a simple and generic resource+hypermedia client application:
357
+
358
+ * "Access the client system":http://restfulie-client.heroku.com
359
+ * Enter your order uri
360
+ * Check your order information which was retrieved and all available actions
361
+
362
+ Now you can either:
363
+
364
+ * *latest* - refresh your order information _order.latest_
365
+ * *cancel* - cancel your order (dead end!) _order.destroy_
366
+ * *pay* - pay for your order, and don't forget to send your (fake) credit card information _order.pay(payment)_
367
+ * *check_payment_info* - after paying you can check the payment information stored at the server _order.check_payment_info_
368
+
369
+ In order to pay do not forget to send the parameter *payment* with a value as
370
+
371
+ <pre>
372
+ <payment>
373
+ <amount>15</amount>
374
+ <cardholder_name>Guilherme Silveira</cardholder_name>
375
+ <card_number>123456789012</card_number>
376
+ <expiry_month>12</expiry_month>
377
+ <expiry_year>12</expiry_year>
378
+ </payment>
379
+ </pre>
380
+
381
+
382
+ h3. Sources
383
+
384
+ "Client":http://github.com/caelum/restfulie-client
385
+ "Server":http://github.com/caelum/restfulie-test
386
+
387
+
109
388
  h2. License
110
389
 
111
390
  /***
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.1"
8
+ GEM_VERSION = "0.2"
9
9
  SUMMARY = "This is a small cute plugin to show how to implement hypermedia based services in a easy way using rails."
10
10
  AUTHOR = "Guilherme Silveira, Caue Guerra"
11
11
  EMAIL = "guilherme.silveira@caelum.com.br"
data/lib/restfulie.rb CHANGED
@@ -5,28 +5,51 @@ module Restfulie
5
5
  def to_json
6
6
  super :methods => :following_states
7
7
  end
8
-
8
+
9
9
  def to_xml(options = {})
10
+ return super unless respond_to?(:status)
11
+
10
12
  controller = options[:controller]
11
13
  return super if controller.nil?
12
-
14
+
13
15
  options[:skip_types] = true
14
16
  super options do |xml|
15
- if respond_to?(:following_states)
16
- states = following_states
17
- states = [states] if states.class.to_s != 'Array'
18
- states.each do |action|
19
- rel = action[:action]
20
- if action[:rel]
21
- rel = action[:rel]
22
- action[:rel] = nil
23
- end
24
- translate_href = controller.url_for(action)
25
- if options[:use_name_based_link]
26
- xml.tag!(rel, translate_href)
27
- else
28
- xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => translate_href)
29
- end
17
+ possible_following = []
18
+ default_transitions_map = self.class._transitions_for(status.to_sym)
19
+ default_transitions = default_transitions_map[:allow] unless default_transitions_map.nil?
20
+
21
+ possible_following += default_transitions unless default_transitions.nil?
22
+ possible_following += self.following_transitions if self.respond_to?(:following_transitions)
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)
30
53
  end
31
54
  end
32
55
  end
@@ -35,7 +58,14 @@ module Restfulie
35
58
  def create_method(name, &block)
36
59
  self.class.send(:define_method, name, &block)
37
60
  end
38
-
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?
67
+ end
68
+
39
69
  end
40
70
 
41
71
  module ActiveRecord
@@ -44,6 +74,45 @@ module ActiveRecord
44
74
  include Restfulie
45
75
  attr_accessor :_possible_states
46
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
47
116
 
48
117
  def self.add_states(result, states)
49
118
  result._possible_states = {}
@@ -162,6 +231,7 @@ module ActiveRecord
162
231
  h[key] = reflect_on_association(key.to_sym ).klass.from_hash value
163
232
  end
164
233
  end
234
+ h.delete("xmlns") if key=="xmlns"
165
235
  end
166
236
  result = self.new h
167
237
  add_states(result, links) unless links.nil?
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.1"
4
+ version: "0.2"
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-03 00:00:00 -02:00
12
+ date: 2009-11-11 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15