restfulie 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -2,6 +2,8 @@ h1. Quit pretending
2
2
 
3
3
  CRUD through HTTP is a good step forward to using resources and becoming RESTful, another step further into it is to make use of hypermedia based services and this gem allows you to do it really fast.
4
4
 
5
+ You can read the "article on using the web for real":http://guilhermesilveira.wordpress.com/2009/11/03/quit-pretending-use-the-web-for-real-restfulie/ which gives an introduction to hypermedia/resources/services.
6
+
5
7
  h2. Why would I use restfulie?
6
8
 
7
9
  1. Easy --> writing hypermedia aware resource based clients
@@ -10,7 +12,16 @@ h2. Why would I use restfulie?
10
12
  4. HATEOAS --> clients you are unaware of will not bother if you change your URIs
11
13
  5. HATEOAS --> services that you consume will not affect your software whenever they change part of their flow or URIs
12
14
 
13
- h1. Restfulie: client-side
15
+ h2. Could you compare it with Spring or JAX-RS based APIs?
16
+
17
+ Restfulie is the first API which tries to somehow implement "Jim Webber":http://jim.webber.name/ and "Ian Robinson":http://iansrobinson.com/ opinion on how RESTFul systems use hypermedia
18
+ as the way to lead your client's path through a business process.
19
+
20
+ Therefore Restfulie is unique both in its feature set when compared to both Spring and JAX-RS based implementations, and its implementation: looking for simple code and favoring conventions over manual configurations.
21
+
22
+ h1. Short examples
23
+
24
+ h2. Restfulie: client-side
14
25
 
15
26
  Example on accessing a resource and its services through the restfulie API:
16
27
 
@@ -24,24 +35,26 @@ order.pay payment # sends a post request to pay this orde
24
35
  order.cancel # sends a delete request
25
36
  </pre>
26
37
 
27
- h1. Restfulie: server-side
38
+ h2. Restfulie: server-side
28
39
 
29
40
  This is a simple example how to make your state changes available to your resource consumers:
30
41
 
31
42
  <pre>
32
43
  class Order < ActiveRecord::Base
44
+
45
+ acts_as_restfulie
46
+
33
47
  def following_transitions
34
48
  transitions = []
35
49
  transitions << [:show, {}]
36
50
  transitions << [:destroy, {}] if can_cancel?
37
51
  transitions << [:pay, {:id => id}] if can_pay?
38
- transitions << [:show, {:controller => :payments, :payment_id => payment.id }] if paid?
39
52
  transitions
40
53
  end
41
54
  end
42
55
  </pre>
43
56
 
44
- *Do not forget to create a migration with a string field named status for your resource:*
57
+ *You might want to create a migration with a string field named status for your resource:*
45
58
 
46
59
  <pre>
47
60
  scripts/generate migration add_status_to_order
@@ -67,14 +80,14 @@ end
67
80
 
68
81
  Or simply define a status reader and writer on your own.
69
82
 
70
-
71
-
72
83
  h2. Restfulie server-side: state machine
73
84
 
74
85
  For those willing to implement a more complex or advanced state machine, you can use the dsl-like api:
75
86
 
76
87
  <pre>
77
88
  class Order < ActiveRecord::Base
89
+ acts_as_restfulie
90
+
78
91
  state :unpaid, :allow => [:latest, :pay, :cancel]
79
92
  state :cancelled, :allow => :latest
80
93
 
@@ -84,7 +97,7 @@ class Order < ActiveRecord::Base
84
97
  end
85
98
  </pre>
86
99
 
87
- h2. Installing
100
+ h1. Installing
88
101
 
89
102
  Just add in your environment.rb the following line:
90
103
 
@@ -116,6 +129,13 @@ Trying to follow the definition of a RESTful application supporting resources wi
116
129
 
117
130
  h2. Client Usage
118
131
 
132
+ Create your class and invoke the *uses_restfulie* method:
133
+
134
+ <pre>class Order < ActiveRecord::Base
135
+ uses_restfulie
136
+ end
137
+ </pre>
138
+
119
139
  One should first acquire the representation from the server through your common GET request and process it through the usual from_* methods:
120
140
  <pre>xml = Net::HTTP.get(URI.parse('http://www.caelum.com.br/orders/1'))
121
141
  order = Order.from_xml(xml)</pre>
@@ -189,14 +209,25 @@ h2. Server-side configuration
189
209
 
190
210
  There are two different approaches that can be combined to create a full hypermedia aware resource based service, including awareness of its states and transitions.
191
211
 
192
- h3. The following available transitions method
212
+ h3. Simple usage: following transitions
193
213
 
194
214
  The most easy way to use restfulie is to write the *following_transitions* method.
195
- It should return a list of possible transitions, where each transition is identified by an array of its name and definition.
215
+ There are three easy steps to make it work:
196
216
 
197
- The next example shows how to define a transition which will map to the current controller, action show:
217
+ 1. Create your model (i.e. Order) with an *status* field
218
+ <pre>
219
+ script/generate scaffold Order status:string location:string
220
+ rake db:create
221
+ rake db:migrate
222
+ </pre>
223
+
224
+ Note that with this usage the status field is optional (from 0.3.0 onwards).
225
+
226
+ 2. Add the *acts_as_restfulie* invocation and *following_transitions* method returning an array of possible transitions:
198
227
 
199
228
  <pre>
229
+ acts_as_restfulie
230
+
200
231
  def following_transitions
201
232
  transitions = []
202
233
  transitions << [:show, {}]
@@ -204,9 +235,48 @@ def following_transitions
204
235
  end
205
236
  </pre>
206
237
 
207
- Which will generate an hyperlink as
238
+ 3. Update your *show* method within the *OrdersController* to show the hypermedia content:
239
+
240
+ <pre>
241
+ def show
242
+ @order = Order.find(params[:id])
243
+
244
+ respond_to do |format|
245
+ format.html # show.html.erb
246
+ format.xml { render :xml => @order.to_xml(:controller=>self) }
247
+ end
248
+ end
249
+ </pre>
250
+
251
+ You are ready to go, create a new order and save it into the database:
208
252
 
209
- <pre><atom:link rel="show" rel="http://yourserver/orders/15" /></pre>
253
+ <pre>
254
+ order = Order.new
255
+ order.location = "take away"
256
+ order.status = "unpaid"
257
+ order.save
258
+ puts "Order #{order.id} saved"
259
+ </pre>
260
+
261
+ Start up the server:
262
+
263
+ <pre>
264
+ script/server
265
+ </pre>
266
+
267
+ And now access your server at http://localhost:3000/orders/1.xml
268
+
269
+ <pre>
270
+ <?xml version="1.0" encoding="UTF-8"?>
271
+ <order>
272
+ <created-at>2009-11-23T00:15:15Z</created-at>
273
+ <id>1</id>
274
+ <location>take away</location>
275
+ <status>unpaid</status>
276
+ <updated-at>2009-11-23T00:15:15Z</updated-at>
277
+ <atom:link rel="show" xmlns:atom="http://www.w3.org/2005/Atom" href="http://localhost:3000/orders/3"/>
278
+ </order>
279
+ </pre>
210
280
 
211
281
  h3. Customizing the rel name
212
282
 
@@ -239,7 +309,7 @@ def following_transitions
239
309
  end
240
310
  </pre>
241
311
 
242
- h2. Defining the state machine and its transitions
312
+ h2. Advanced usage: Defining the state machine and its transitions
243
313
 
244
314
  The second way of defining your available transitions is to explicitely define the states and transitions.
245
315
 
@@ -255,6 +325,9 @@ The following example shows all possible states for an order:
255
325
 
256
326
  <pre>
257
327
  class Order < ActiveRecord::Base
328
+
329
+ acts_as_restfulie
330
+
258
331
  state :unpaid, :allow => [:latest, :pay, :cancel]
259
332
  state :cancelled, :allow => :latest
260
333
  state :received, :allow => [:latest, :check_payment_info]
@@ -277,6 +350,9 @@ list of available transition whenever the *can_pay?* method returns true:
277
350
 
278
351
  <pre>
279
352
  class Order < ActiveRecord::Base
353
+
354
+ acts_as_restfulie
355
+
280
356
  transition :pay, {:action => pay_this_order, :controller => :payments}, :preparing
281
357
 
282
358
  def following_transitions
@@ -303,6 +379,36 @@ class Order < ActiveRecord::Base
303
379
  end
304
380
  end
305
381
  </pre>
382
+
383
+ h3. Accessing all possible transitions
384
+
385
+ One can access all possible transitions for an object by invoking its available_transitions method:
386
+
387
+ <pre>
388
+ transitions = order.available_transitions
389
+ </pre>
390
+
391
+ h3. Checking the possibility of following transitions
392
+
393
+ By following the advanced usage, one receives also all *can_* method. i.e.:
394
+
395
+ <pre>
396
+ order.status = :unpaid
397
+ puts(order.can_pay?) # will print true
398
+ order.status = :paid
399
+ puts(order.can_pay?) # will print false
400
+ </pre>
401
+
402
+ You can use the *can_xxx* methods in your controllers to check if your current resource's state can be changed:
403
+
404
+ <pre>
405
+ def pay
406
+ @order = Order.find(params[:id])
407
+ raise "impossible to pay due to this order status #{order.status}" if !@order.can_pay?
408
+
409
+ # payment code
410
+ end
411
+ </pre>
306
412
 
307
413
  h3. Using xml+rel links instead of atom links
308
414
 
@@ -331,10 +437,10 @@ h2. Team
331
437
  Restfulie was created and is maintained within Caelum by
332
438
 
333
439
  Projetct Founder
334
- * "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
440
+ * "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br - twitter:http://www.twitter.com/guilhermecaelum "http://guilhermesilveira.wordpress.com":http://guilhermesilveira.wordpress.com
335
441
 
336
442
  Active Commiters
337
- * "Caue Guerra":mailto:caue.guerra@gmail.com
443
+ * "Caue Guerra":mailto:caue.guerra@gmail.com - "http://caueguerra.com/":http://caueguerra.com/
338
444
  * "Guilherme Silveira":mailto:guilherme.silveira@caelum.com.br
339
445
 
340
446
  Contributors
@@ -381,9 +487,61 @@ In order to pay do not forget to send the parameter *payment* with a value as
381
487
 
382
488
  h3. Sources
383
489
 
490
+ You can see an application's source code here, both client and server side were implemented using *restfulie*:
491
+
384
492
  "Client":http://github.com/caelum/restfulie-client
385
493
  "Server":http://github.com/caelum/restfulie-test
386
494
 
495
+ h3. More tutorials
496
+
497
+ There is a "portuguese tutorial on the server-side support":http://wakethedead.com.br/blog/70-restfulie, "more on restfulie - portuguese":http://andersonleiteblog.wordpress.com/2009/11/23/mais-sobre-restfulie/ and a "blog post on the entire ecosystem in english":http://guilhermesilveira.wordpress.com/2009/11/03/quit-pretending-use-the-web-for-real-restfulie/
498
+
499
+
500
+ h2. What's new
501
+
502
+ h3. next release
503
+ * API change: you need to invoke "acts_as_restfulie" to your models
504
+ * implemented support to can_*** methods
505
+ * post data through http POST body
506
+ * no need for the *status* field if you use the following_transition approach on the server side
507
+ * bug fixed when *status* was nil
508
+ * lots of internal refactoring
509
+ * lots of new documentation
510
+
511
+ h3. 0.2
512
+ * support to state machine configuration
513
+ * resources must contain a status field
514
+
515
+ h3. 0.1
516
+ * first release
517
+
518
+ h2. Coming soon
519
+
520
+ * Generate and link to rubydoc
521
+ * release 0.3
522
+ * change parameter order for transition: last one is HASH, first is name, second (if existing) is target state
523
+ * allows pure String/byte array client side post and server side retrieval
524
+ * integration tests on orderserver/client api
525
+ * controller method should check if its an restfulie resource
526
+ * full support to extended json
527
+ * rel prepend suffix as http://iansrobinson.com/resources/link-relations/preceding
528
+ * automatically generate uri for this rel with its transition description
529
+ * pure href definition of link
530
+ * post entry point support
531
+ * remove client dependency from ActiveRecord
532
+ * remove server side dependency from ActiveRecord (its ok to use anything else)
533
+ * Set the correct media type instead of application/xml
534
+ * transitions << [:show] should work
535
+ * rails 3 easier support (no controller argument!)
536
+ * allow servers to define transitions by accessing other systems
537
+ * allow servers to define a state method instead of internal variable
538
+ * controller filtering and methods
539
+ * english tutorial
540
+ * when receiving a 201 + content, it should believe the content
541
+ * when receiving a 201 without any content, it should allow to redirect or not
542
+ * client side should allow withTimeStamp, withETag, withAuth
543
+ * is there is an etag, use it by default (maybe NOT use it by default)... modified since and so on (header tags)
544
+ * server side maybe allow hypermedia controls or not
387
545
 
388
546
  h2. License
389
547
 
@@ -403,3 +561,4 @@ h2. License
403
561
  * See the License for the specific language governing permissions and
404
562
  * limitations under the License.
405
563
  */
564
+
data/Rakefile CHANGED
@@ -5,8 +5,8 @@ require 'rake/gempackagetask'
5
5
  require 'spec/rake/spectask'
6
6
 
7
7
  GEM = "restfulie"
8
- GEM_VERSION = "0.2"
9
- SUMMARY = "This is a small cute plugin to show how to implement hypermedia based services in a easy way using rails."
8
+ GEM_VERSION = "0.3"
9
+ SUMMARY = "Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side)."
10
10
  AUTHOR = "Guilherme Silveira, Caue Guerra"
11
11
  EMAIL = "guilherme.silveira@caelum.com.br"
12
12
  HOMEPAGE = "http://github.com/caelum/restfulie"
@@ -49,3 +49,6 @@ end
49
49
 
50
50
  desc "Builds the project"
51
51
  task :build => :spec
52
+
53
+ desc "Default build will run specs"
54
+ task :default => :spec
@@ -0,0 +1,76 @@
1
+ module Restfulie
2
+ module Client
3
+ module Base
4
+
5
+ # translates a response to an object
6
+ def from_response(res)
7
+
8
+ raise "unimplemented content type" if res.content_type!="application/xml"
9
+
10
+ hash = Hash.from_xml res.body
11
+ return hash if hash.keys.length == 0
12
+ raise "unable to parse an xml with more than one root element" if hash.keys.length>1
13
+
14
+ type = hash.keys[0].camelize.constantize
15
+ type.from_xml(res.body)
16
+
17
+ end
18
+
19
+ def requisition_method_for(overriden_option,name)
20
+ basic_mapping = { :delete => Net::HTTP::Delete, :put => Net::HTTP::Put, :get => Net::HTTP::Get, :post => Net::HTTP::Post}
21
+ defaults = {:destroy => Net::HTTP::Delete, :delete => Net::HTTP::Delete, :cancel => Net::HTTP::Delete,
22
+ :refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get}
23
+
24
+ return basic_mapping[overriden_option.to_sym] if overriden_option
25
+ defaults[name.to_sym] || Net::HTTP::Post
26
+ end
27
+
28
+ def add_state(transition)
29
+ name = transition["rel"]
30
+
31
+ self.module_eval do
32
+
33
+ def temp_method(options = {}, &block)
34
+ self.invoke_remote_transition(Restfulie::Client::Helper.current_method, options, block)
35
+ end
36
+
37
+ alias_method name, :temp_method
38
+ undef :temp_method
39
+ end
40
+ end
41
+
42
+ # receives an object and inserts all necessary methods
43
+ # so it can answer to can_??? invocations
44
+ def add_transitions(result, states)
45
+ result._possible_states = {}
46
+
47
+ states.each do |state|
48
+ result._possible_states[state["rel"]] = state
49
+ add_state(state)
50
+ end
51
+ result.extend Restfulie::Server::State
52
+
53
+ result
54
+ end
55
+
56
+ # retrieves a resource form a specific uri
57
+ def from_web(uri)
58
+ res = Net::HTTP.get_response(URI.parse(uri))
59
+ # TODO redirect... follow or not? (optional...)
60
+ raise "invalid request" if res.code != "200"
61
+
62
+ # TODO really support different content types
63
+ case res.content_type
64
+ when "application/xml"
65
+ self.from_xml res.body
66
+ when "application/json"
67
+ self.from_json res.body
68
+ else
69
+ raise "unknown content type"
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ module Restfulie
2
+ module Client
3
+ module Helper
4
+ # retrieves the invoking method's name
5
+ def self.current_method
6
+ caller[0]=~/`(.*?)'/
7
+ $1
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Restfulie
2
+ module Client
3
+ module Instance
4
+
5
+ # list of possible states to access
6
+ attr_accessor :_possible_states
7
+
8
+ # which content-type generated this data
9
+ attr_accessor :_came_from
10
+
11
+
12
+ def invoke_remote_transition(name, options, block)
13
+
14
+ method = self.class.requisition_method_for options[:method], name
15
+
16
+ url = URI.parse(_possible_states[name]["href"])
17
+ req = method.new(url.path)
18
+ req.body = options[:data] if options[:data]
19
+ req.add_field("Accept", "application/xml") if self._came_from == :xml
20
+
21
+ response = Net::HTTP.new(url.host, url.port).request(req)
22
+
23
+ return block.call(response) if block
24
+ return response if method != Net::HTTP::Get
25
+ self.class.from_response response
26
+ end
27
+
28
+
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ module Restfulie
2
+ module Server
3
+ module Base
4
+
5
+ # returns the definition for the transaction
6
+ def existing_transitions(name)
7
+ transitions[name]
8
+ end
9
+
10
+ # returns a hash of all possible transitions: Restfulie::Server::Transition
11
+ def transitions
12
+ @transitions ||= {}
13
+ end
14
+
15
+ # returns a hash of all possible states
16
+ def states
17
+ @states ||= {}
18
+ end
19
+
20
+ # adds a new state to the list of possible states
21
+ def state(name, options = {})
22
+ options[:allow] = [options[:allow]] unless options[:allow].kind_of? Array
23
+ states[name] = options
24
+ end
25
+
26
+ # defines a new transition. the transition options works in the same way
27
+ # that following_transition definition does.
28
+ def transition(name, options = {}, result = nil, &body)
29
+
30
+ transition = Restfulie::Server::Transition.new(name, options, result, body)
31
+ transitions[name] = transition
32
+
33
+ define_methods_for(self, name, result)
34
+ controller_name = (self.name + "Controller")
35
+ end
36
+
37
+ def define_methods_for(type, name, result)
38
+
39
+ return nil if type.respond_to?(name)
40
+
41
+ type.send(:define_method, name) do |*args|
42
+ self.status = result.to_s unless result == nil
43
+ end
44
+
45
+ type.send(:define_method, "can_#{name}?") do
46
+ transitions = self.available_transitions[:allow]
47
+ transitions.include? name
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ require 'restfulie'
2
+
3
+ module ActionController
4
+ # class Base
5
+ # alias_method :simple_render, :render
6
+ # def render(options={})
7
+ # debugger
8
+ # if options[:xml]
9
+ # resource = options[:xml]
10
+ # debugger
11
+ # return simple_render(options) if resource.kind_of?(String) || !resource.class.respond_to?(:is_acting_as_restfulie)
12
+ # options[:xml] = resource.to_xml(:controller => self)
13
+ # end
14
+ # simple_render(options)
15
+ # end
16
+ # end
17
+ end
@@ -0,0 +1,50 @@
1
+ module Restfulie
2
+ module Server
3
+ module Instance
4
+
5
+ # Returns an array with extra possible transitions.
6
+ # Those transitions will be concatenated with any extra transitions provided by your resource through
7
+ # the use of state and transition definitions.
8
+ # For every transition its name is the only mandatory field:
9
+ # options = {}
10
+ # [:show, options] # will generate a link to your controller's show action
11
+ #
12
+ # The options can be used to override restfulie's conventions:
13
+ # options[:rel] = "refresh" # will create a rel named refresh
14
+ # options[:action] = "destroy" # will link to the destroy method
15
+ # options[:controller] = another controller # will use another controller's action
16
+ #
17
+ # Any extra options will be passed to the target controller url_for method in order to retrieve
18
+ # the transition's uri.
19
+ def following_transitions
20
+ []
21
+ end
22
+
23
+
24
+ # returns a list of available transitions for this objects state
25
+ # TODO rename because it should never be used by the client...
26
+ def available_transitions
27
+ status_available = respond_to?(:status) && status!=nil
28
+ return {:allow => []} unless status_available
29
+ self.class.states[self.status.to_sym] || {:allow => []}
30
+ end
31
+
32
+ # returns a list containing all available transitions for this object's state
33
+ def all_following_transitions
34
+ all = [] + available_transitions[:allow]
35
+ following_transitions.each do |t|
36
+ t = Restfulie::Server::Transition.new(t[0], t[1], t[2], nil) if t.kind_of? Array
37
+ all << t
38
+ end
39
+ all
40
+ end
41
+
42
+ # checks if its possible to execute such transition and, if it is, executes it
43
+ def move_to(name)
44
+ raise "Current state #{status} is invalid in order to execute #{name}. It must be one of #{transitions}" unless available_transitions[:allow].include? name
45
+ self.class.transitions[name].execute_at self
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Restfulie
3
+
4
+ module Server
5
+
6
+ module Marshalling
7
+
8
+ def to_json
9
+ super :methods => :following_states
10
+ end
11
+
12
+ # adds a link for each transition to the current xml writer
13
+ def add_links(xml, all, options)
14
+ all.each do |transition|
15
+ add_link(transition, xml, options)
16
+ end
17
+ end
18
+
19
+ # adds a link for this transition to the current xml writer
20
+ def add_link(transition, xml, options)
21
+
22
+ transition = self.class.existing_transitions(transition.to_sym) unless transition.kind_of? Restfulie::Server::Transition
23
+ transition.add_link_to(xml, self, options)
24
+
25
+ end
26
+
27
+ def to_xml(options = {})
28
+
29
+ transitions = all_following_transitions
30
+ return super(options) if transitions.empty? || options[:controller].nil?
31
+
32
+ options[:skip_types] = true
33
+ super options do |xml|
34
+ add_links xml, transitions, options
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module Restfulie
2
+
3
+ module Server
4
+
5
+ # adds respond_to and has_state methods to resources
6
+ module State
7
+
8
+ # overrides the respond_to? method to check if this method is contained or was defined by a state
9
+ def respond_to?(sym)
10
+ has_state(sym.to_s) || super(sym)
11
+ end
12
+
13
+ # returns true if this resource has a state named name
14
+ def has_state(name)
15
+ !@_possible_states[name].nil?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module Restfulie
2
+
3
+ module Server
4
+
5
+ # represents a transition on the server side
6
+ class Transition
7
+ attr_reader :body, :name, :result
8
+ def initialize(name, options, result, body)
9
+ @name = name
10
+ @options = options
11
+ @result = result
12
+ @body = body
13
+ end
14
+ def action
15
+ @options || {}
16
+ end
17
+
18
+ # executes this transition in a resource
19
+ def execute_at(target_object)
20
+ target_object.status = result.to_s unless result.nil?
21
+ end
22
+
23
+ # adds a link to this transition's uri on a xml writer
24
+ def add_link_to(xml, model, options)
25
+ specific_action = action.dup
26
+ specific_action = @body.call(model) if @body
27
+
28
+ rel = specific_action[:rel] || @name
29
+ specific_action[:rel] = nil
30
+
31
+ specific_action[:action] ||= @name
32
+ uri = options[:controller].url_for(specific_action)
33
+
34
+ if options[:use_name_based_link]
35
+ xml.tag!(rel, uri)
36
+ else
37
+ xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => uri)
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,60 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ # basic code from Matt Pulver
5
+ # found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
6
+ # addapted to support links
7
+ def self.from_hash( hash )
8
+ h = {}
9
+ h = hash.dup if hash
10
+ links = nil
11
+ h.each do |key,value|
12
+ case value.class.to_s
13
+ when 'Array'
14
+ if key=="link"
15
+ links = h[key]
16
+ h.delete("link")
17
+ else
18
+ h[key].map! { |e| reflect_on_association(key.to_sym ).klass.from_hash e }
19
+ end
20
+ when /\AHash(WithIndifferentAccess)?\Z/
21
+ if key=="link"
22
+ links = [h[key]]
23
+ h.delete("link")
24
+ else
25
+ h[key] = reflect_on_association(key.to_sym ).klass.from_hash value
26
+ end
27
+ end
28
+ h.delete("xmlns") if key=="xmlns"
29
+ end
30
+ result = self.new h
31
+ if !(links.nil?) && self.include?(Restfulie::Client::Instance)
32
+ add_transitions(result, links)
33
+ end
34
+ result
35
+ end
36
+
37
+ def self.from_json( json )
38
+ from_hash safe_json_decode( json )
39
+ end
40
+
41
+ # The xml has a surrounding class tag (e.g. ship-to),
42
+ # but the hash has no counterpart (e.g. 'ship_to' => {} )
43
+ def self.from_xml( xml )
44
+ hash = Hash.from_xml xml
45
+ head = hash[self.to_s.underscore]
46
+ result = self.from_hash head
47
+ return nil if result.nil?
48
+ result._came_from = :xml if self.include?(Restfulie::Client::Instance)
49
+ result
50
+ end
51
+ end
52
+ end
53
+
54
+ def safe_json_decode( json )
55
+ return {} if !json
56
+ begin
57
+ ActiveSupport::JSON.decode json
58
+ rescue ; {} end
59
+ end
60
+ # end of code based on Matt Pulver's
data/lib/restfulie.rb CHANGED
@@ -1,264 +1,30 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
-
4
- module Restfulie
5
- def to_json
6
- super :methods => :following_states
7
- end
8
-
9
- def to_xml(options = {})
10
- return super unless respond_to?(:status)
11
-
12
- controller = options[:controller]
13
- return super if controller.nil?
14
-
15
- options[:skip_types] = true
16
- super options do |xml|
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)
53
- end
54
- end
55
- end
56
- end
57
-
58
- def create_method(name, &block)
59
- self.class.send(:define_method, name, &block)
60
- end
61
-
62
- def move_to(name)
63
- transitions = self.class._transitions_for(self.status.to_sym)[:allow]
64
- raise "Current state #{status} is invalid in order to execute #{name}. It must be one of #{transitions}" unless transitions.include? name
65
- result = self.class._transitions(name)[2]
66
- self.status = result.to_s unless result.nil?
3
+ require 'restfulie/unmarshalling'
4
+
5
+ require 'restfulie/client/base'
6
+ require 'restfulie/client/helper'
7
+ require 'restfulie/client/instance'
8
+
9
+ require 'restfulie/server/base'
10
+ require 'restfulie/server/controller'
11
+ require 'restfulie/server/instance'
12
+ require 'restfulie/server/marshalling'
13
+ require 'restfulie/server/state'
14
+ require 'restfulie/server/transition'
15
+
16
+ class Class
17
+ def acts_as_restfulie
18
+ class << self
19
+ include Restfulie::Server::Base
20
+ end
21
+ include Restfulie::Server::Instance
22
+ include Restfulie::Server::Marshalling
67
23
  end
68
-
69
- end
70
-
71
- module ActiveRecord
72
- class Base
73
-
74
- include Restfulie
75
- attr_accessor :_possible_states
76
- attr_accessor :_came_from
77
-
78
- def self._transitions_for(state)
79
- @@states[state]
80
- end
81
-
82
- def self._transitions(name)
83
- [@@transitions[name], @@bodies[name], @@results[name]]
84
- end
85
-
86
- @@states = {}
87
- @@transitions = {}
88
- @@bodies = {}
89
- @@results = {}
90
-
91
- def self.state(name, options)
92
- if name.class==Array
93
- name.each do |simple|
94
- self.state(simple, options)
95
- end
96
- else
97
- options[:allow] = [options[:allow]] unless options[:allow].class == Array
98
- @@states[name] = options
99
- end
100
- end
101
-
102
- def self.transition(name, options = {}, result = nil, &body)
103
- @@transitions[name] = options
104
- @@bodies[name] = body
105
- @@results[name] = result unless result == nil
106
- defined = self.respond_to?(name)
107
- if !defined
108
- self.send(:define_method, name) do |*args|
109
- self.status = result.to_s unless result == nil
110
- end
111
- self.send(:define_method, "can_#{name}2?") do
112
- puts "executing can_#{name}2?"
113
- end
114
- end
115
- end
116
-
117
- def self.add_states(result, states)
118
- result._possible_states = {}
119
- states.each do |state|
120
- result._possible_states[state["rel"]] = state
121
- end
122
- def result.respond_to?(sym)
123
- has_state(sym.to_s) || super(sym)
124
- end
125
-
126
- def result.has_state(name)
127
- !@_possible_states[name].nil?
128
- end
129
-
130
- states.each do |state|
131
- name = state["rel"]
132
- self.module_eval do
133
- def current_method
134
- caller[0]=~/`(.*?)'/
135
- $1
136
- end
137
- def temp_method(options = {}, &block)
138
- name = current_method
139
- state = _possible_states[name]
140
- data = options[:data] || {}
141
- url = URI.parse(state["href"])
142
- get = false
143
-
144
- # gs: i dont know how to meta play here! i suck
145
- if options[:method]=="delete"
146
- req = Net::HTTP::Delete.new(url.path)
147
- elsif options[:method]=="put"
148
- req = Net::HTTP::Put.new(url.path)
149
- elsif options[:method]=="get"
150
- req = Net::HTTP::Get.new(url.path)
151
- get = true
152
- elsif options[:method]=="post"
153
- req = Net::HTTP::Post.new(url.path)
154
- elsif ['destroy','delete','cancel'].include? name
155
- req = Net::HTTP::Delete.new(url.path)
156
- elsif ['refresh', 'reload', 'show', 'latest'].include? name
157
- req = Net::HTTP::Get.new(url.path)
158
- get = true
159
- else
160
- req = Net::HTTP::Post.new(url.path)
161
- end
162
-
163
- req.set_form_data(data)
164
- req.add_field("Accept", "text/xml") if _came_from == :xml
165
-
166
- http = Net::HTTP.new(url.host, url.port)
167
- response = http.request(req)
168
- return yield(response) if !block.nil?
169
- if get
170
- case response.content_type
171
- when "application/xml"
172
- content = response.body
173
- hash = Hash.from_xml content
174
- return hash if hash.keys.length == 0
175
- raise "unable to parse an xml with more than one root element" if hash.keys.length>1
176
- key = hash.keys[0]
177
- type = key.camelize.constantize
178
- return type.from_xml content
179
- else
180
- raise :unknown_content_type
181
- end
182
- end
183
- response
184
-
185
- end
186
- alias_method name, :temp_method
187
- undef :temp_method
188
- end
189
- end
190
-
191
- result
192
- end
193
-
194
- def self.from_web(uri)
195
- url = URI.parse(uri)
196
- req = Net::HTTP::Get.new(url.path)
197
- http = Net::HTTP.new(url.host, url.port)
198
- res = http.request(req)
199
- raise :invalid_request, res if res.code != "200"
200
- case res.content_type
201
- when "application/xml"
202
- self.from_xml res.body
203
- when "application/json"
204
- self.from_json res.body
205
- else
206
- raise :unknown_content_type
207
- end
208
- end
209
-
210
- # basic code from Matt Pulver
211
- # found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
212
- # addapted to support links
213
- def self.from_hash( hash )
214
- h = {}
215
- h = hash.dup if hash
216
- links = nil
217
- h.each do |key,value|
218
- case value.class.to_s
219
- when 'Array'
220
- if key=="link"
221
- links = h[key]
222
- h.delete("link")
223
- else
224
- h[key].map! { |e| reflect_on_association(key.to_sym ).klass.from_hash e }
225
- end
226
- when /\AHash(WithIndifferentAccess)?\Z/
227
- if key=="link"
228
- links = [h[key]]
229
- h.delete("link")
230
- else
231
- h[key] = reflect_on_association(key.to_sym ).klass.from_hash value
232
- end
233
- end
234
- h.delete("xmlns") if key=="xmlns"
235
- end
236
- result = self.new h
237
- add_states(result, links) unless links.nil?
238
- result
239
- end
240
-
241
- def self.from_json( json )
242
- from_hash safe_json_decode( json )
243
- end
244
-
245
- # The xml has a surrounding class tag (e.g. ship-to),
246
- # but the hash has no counterpart (e.g. 'ship_to' => {} )
247
- def self.from_xml( xml )
248
- hash = Hash.from_xml xml
249
- head = hash[self.to_s.underscore]
250
- result = self.from_hash head
251
- return nil if result.nil?
252
- result._came_from = :xml
253
- result
24
+ def uses_restfulie
25
+ class << self
26
+ include Restfulie::Client::Base
254
27
  end
28
+ include Restfulie::Client::Instance
255
29
  end
256
- end
257
-
258
- def safe_json_decode( json )
259
- return {} if !json
260
- begin
261
- ActiveSupport::JSON.decode json
262
- rescue ; {} end
263
- end
264
- # end of code based on Matt Pulver's
30
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restfulie
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.2"
4
+ version: "0.3"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guilherme Silveira, Caue Guerra
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-11 00:00:00 -02:00
12
+ date: 2009-11-25 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,6 +22,16 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
 
24
24
  files:
25
+ - lib/restfulie/client/base.rb
26
+ - lib/restfulie/client/helper.rb
27
+ - lib/restfulie/client/instance.rb
28
+ - lib/restfulie/server/base.rb
29
+ - lib/restfulie/server/controller.rb
30
+ - lib/restfulie/server/instance.rb
31
+ - lib/restfulie/server/marshalling.rb
32
+ - lib/restfulie/server/state.rb
33
+ - lib/restfulie/server/transition.rb
34
+ - lib/restfulie/unmarshalling.rb
25
35
  - lib/restfulie.rb
26
36
  - Rakefile
27
37
  - README.textile
@@ -52,6 +62,6 @@ rubyforge_project:
52
62
  rubygems_version: 1.3.5
53
63
  signing_key:
54
64
  specification_version: 3
55
- summary: This is a small cute plugin to show how to implement hypermedia based services in a easy way using rails.
65
+ summary: Hypermedia aware resource based library in ruby (client side) and ruby on rails (server side).
56
66
  test_files: []
57
67