restfulie 0.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile 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. Could you compare it with Spring or JAX-RS based APIs?
15
+ h2. Restfulie
16
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.
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.3"
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
- # translates a response to an object
6
- def from_response(res)
3
+ module Base
4
+
5
+ SELF_RETRIEVAL = [:latest, :refresh, :reload]
7
6
 
8
- raise "unimplemented content type" if res.content_type!="application/xml"
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
- hash = Hash.from_xml res.body
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(res.body)
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
- 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
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
- attr_accessor :_possible_states
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
- url = URI.parse(_possible_states[name]["href"])
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 if method != Net::HTTP::Get
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
@@ -1,6 +1,6 @@
1
1
  module Restfulie
2
2
 
3
- module Server
3
+ module Client
4
4
 
5
5
  # adds respond_to and has_state methods to resources
6
6
  module State
@@ -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
- define_methods_for(self, name, result)
34
- controller_name = (self.name + "Controller")
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 define_methods_for(type, name, result)
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 == nil
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
- # 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
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
- super :methods => :following_states
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, :result
8
- def initialize(name, options, result, body)
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 ActiveRecord
2
- class Base
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 self.from_hash( hash )
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(result, links)
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( json )
38
- from_hash safe_json_decode( json )
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( 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( json )
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/base'
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
- class Class
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
- class << self
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
- end
24
- def uses_restfulie
25
- class << self
26
- include Restfulie::Client::Base
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
- end
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: "0.3"
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-11-25 00:00:00 -02:00
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