restfulie 0.3 → 0.4.0

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