restfulie 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ /***
2
+ * Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
3
+ * All rights reserved.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
data/README.textile CHANGED
@@ -1,3 +1,7 @@
1
+ h1. Web site
2
+
3
+ Restulie's website can be found at "http://restfulie.caelum.com.br":http://restfulie.caelum.com.br
4
+
1
5
  h1. Quit pretending
2
6
 
3
7
  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.
@@ -12,10 +16,6 @@ h2. Why would I use restfulie?
12
16
  4. HATEOAS --> clients you are unaware of will not bother if you change your URIs
13
17
  5. HATEOAS --> services that you consume will not affect your software whenever they change part of their flow or URIs
14
18
 
15
- h2. Restfulie
16
-
17
- The documentation is at "http://wiki.github.com/caelum/restfulie":http://wiki.github.com/caelum/restfulie
18
-
19
19
 
20
20
  <script type="text/javascript">
21
21
  var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
data/Rakefile CHANGED
@@ -5,11 +5,11 @@ require 'rake/gempackagetask'
5
5
  require 'spec/rake/spectask'
6
6
 
7
7
  GEM = "restfulie"
8
- GEM_VERSION = "0.4.0"
8
+ GEM_VERSION = "0.5.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"
12
- HOMEPAGE = "http://github.com/caelum/restfulie"
12
+ HOMEPAGE = "http://restfulie.caelumobjects.com"
13
13
 
14
14
  spec = Gem::Specification.new do |s|
15
15
  s.name = GEM
@@ -18,6 +18,8 @@ spec = Gem::Specification.new do |s|
18
18
  s.summary = SUMMARY
19
19
  s.require_paths = ['lib']
20
20
  s.files = FileList['lib/**/*.rb', '[A-Z]*'].to_a
21
+ # s.add_dependency("ratom", [">= 0.6.3"])
22
+ # s.add_dependency("jeokkarak", [">= 1.0.3"])
21
23
 
22
24
  # s.add_dependency(%q<rubigen>, [">= 1.3.4"])
23
25
 
@@ -0,0 +1,58 @@
1
+ module Restfulie
2
+
3
+ # TODO break media type registering for DECODING and ENCODING appart, so we can have two files
4
+ module Server
5
+
6
+ # a decoder from atom an feed
7
+ class AtomMediaType < Restfulie::MediaType::Type
8
+
9
+ def self.from_xml(xml)
10
+ hash = Hash.from_xml xml
11
+ AtomFeedDecoded.new(hash.values.first)
12
+ end
13
+
14
+ end
15
+
16
+ # an atom feed
17
+ class AtomFeedDecoded < Hashi::CustomHash
18
+
19
+ def initialize(hash)
20
+ super(hash)
21
+ end
22
+
23
+ # retrieves the nth element from an atom feed
24
+ def [](position)
25
+
26
+ hash = entry[position].content.hash
27
+ hash = hash.dup
28
+ hash.delete("type")
29
+ result = Restfulie::MediaType::DefaultMediaTypeDecoder.from_hash(hash)
30
+
31
+ add_links_to(result, entry[position]) if entry[position].respond_to?(:link)
32
+ result
33
+
34
+ end
35
+
36
+ private
37
+
38
+ def add_links_to(result, entry)
39
+ links = entry.link.hash
40
+ links = [links] if links.kind_of? Hash
41
+ self_definition = self_from(links)
42
+ links << {:rel => "destroy", :method => "delete", :href => self_definition["href"]} unless self_definition.nil?
43
+ result.add_transitions(links)
44
+ end
45
+
46
+ def self_from(links)
47
+ links.find do |link|
48
+ link["rel"] == "self" || link[:rel] == "self"
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ Restfulie::MediaType.register(Restfulie::MediaType.rendering_type('application/atom+xml', Server::AtomMediaType))
57
+
58
+ end
@@ -1,43 +1,50 @@
1
+ class Hash
2
+ def to_object(body)
3
+ if keys.length>1
4
+ raise "unable to parse an xml with more than one root element"
5
+ elsif keys.length == 0
6
+ self
7
+ else
8
+ type = keys[0].camelize.constantize
9
+ type.from_xml(body)
10
+ end
11
+ end
12
+ end
13
+
1
14
  module Restfulie
15
+
16
+ # will execute some action in a specific URI
17
+ def self.at(uri)
18
+ Client::RequestExecution.new(nil).at uri
19
+ end
20
+
2
21
  module Client
3
22
  module Base
4
23
 
5
24
  SELF_RETRIEVAL = [:latest, :refresh, :reload]
6
25
 
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
16
-
17
26
  # translates a response to an object
18
27
  def from_response(res, invoking_object)
19
28
 
20
29
  return invoking_object if res.code=="304"
21
30
 
22
31
  raise UnsupportedContentType.new("unsupported content type '#{res.content_type}' '#{res.code}'") unless res.content_type=="application/xml"
32
+
33
+ # TODO this method should use the RequestExecution process to parse the content type and body
34
+ # TODO add default html parser: do nothin
23
35
 
24
36
  body = res.body
25
37
  return {} if body.empty?
26
38
 
27
39
  hash = Hash.from_xml body
28
- return hash if hash.keys.length == 0
40
+ hash.to_object(body)
29
41
 
30
- raise "unable to parse an xml with more than one root element" if hash.keys.length>1
31
-
32
- type = hash.keys[0].camelize.constantize
33
- type.from_xml(body)
34
-
35
42
  end
36
43
 
37
44
  def requisition_method_for(overriden_option,name)
38
45
  basic_mapping = { :delete => Net::HTTP::Delete, :put => Net::HTTP::Put, :get => Net::HTTP::Get, :post => Net::HTTP::Post}
39
46
  defaults = {:destroy => Net::HTTP::Delete, :delete => Net::HTTP::Delete, :cancel => Net::HTTP::Delete,
40
- :refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get}
47
+ :refresh => Net::HTTP::Get, :reload => Net::HTTP::Get, :show => Net::HTTP::Get, :latest => Net::HTTP::Get, :self => Net::HTTP::Get}
41
48
 
42
49
  return basic_mapping[overriden_option.to_sym] if overriden_option
43
50
  defaults[name.to_sym] || Net::HTTP::Post
@@ -22,61 +22,16 @@ module Restfulie
22
22
 
23
23
  # retrieves a resource form a specific uri
24
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
-
25
+ RequestExecution.new(self).at(uri).get(options)
52
26
  end
53
27
 
54
28
  private
55
29
  def remote_post(content)
56
- remote_post_to(entry_point_for.create.uri, content)
30
+ RequestExecution.new(self).at(entry_point_for.create.uri).post(content)
57
31
  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
32
 
76
- end
77
-
78
33
  end
79
-
34
+
80
35
  class FollowConfig
81
36
  def initialize
82
37
  @entries = {
@@ -84,7 +39,7 @@ module Restfulie
84
39
  }
85
40
  end
86
41
  def method_missing(name, *args)
87
- return value_for name if name.to_s[-1,1]=="?"
42
+ return value_for(name) if name.to_s[-1,1] == "?"
88
43
  set_all_for name
89
44
  end
90
45
 
@@ -18,24 +18,31 @@ module Restfulie
18
18
  url = URI.parse(state["href"] || state[:href])
19
19
  req = method.new(url.path)
20
20
  req.body = options[:data] if options[:data]
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)
24
-
21
+ add_request_headers(req, name)
22
+
25
23
  response = Net::HTTP.new(url.host, url.port).request(req)
26
24
 
27
25
  return block.call(response) if block
28
26
  return response unless method == Net::HTTP::Get
29
27
  self.class.from_response response, self
30
28
  end
29
+
30
+ private
31
+ def add_request_headers(req, name)
32
+ req.add_field("Accept", "application/xml") if self._came_from == :xml
33
+ req.add_field("If-None-Match", self.etag) if self.class.is_self_retrieval?(name) && self.respond_to?(:etag)
34
+ req.add_field("If-Modified-Since", self.last_modified) if self.class.is_self_retrieval?(name) && self.respond_to?(:last_modified)
35
+ end
36
+
37
+ public
31
38
 
32
39
 
33
- # inserts all transitions from this object as can_xxx and xxx methods
34
- def add_transitions(transitions)
40
+ # inserts all links from this object as can_xxx and xxx methods
41
+ def add_transitions(links)
35
42
 
36
- transitions.each do |state|
37
- self._possible_states[state["rel"] || state[:rel]] = state
38
- self.add_state(state)
43
+ links.each do |t|
44
+ self._possible_states[t["rel"] || t[:rel]] = t
45
+ self.add_state(t)
39
46
  end
40
47
  self.extend Restfulie::Client::State
41
48
  end
@@ -0,0 +1,131 @@
1
+ module Restfulie
2
+ module Client
3
+
4
+ class RequestExecution
5
+
6
+ def initialize(type)
7
+ @type = type
8
+ @content_type = "application/xml"
9
+ @accepts = "application/xml"
10
+ end
11
+
12
+ def at(uri)
13
+ @uri = uri
14
+ self
15
+ end
16
+
17
+ # sets the Content-type AND Accept headers for this request
18
+ def as(content_type)
19
+ @content_type = content_type
20
+ @accepts = content_type
21
+ self
22
+ end
23
+
24
+ # sets the Accept header for this request
25
+ def accepts(content_type)
26
+ @accepts = content_type
27
+ self
28
+ end
29
+
30
+ # asks to create this content on the server (post it)
31
+ def create(content)
32
+ post(content)
33
+ end
34
+
35
+ # post this content to the server
36
+ def post(content)
37
+ remote_post_to(@uri, content)
38
+ end
39
+
40
+ # retrieves information from the server using a GET request
41
+ def get(options = {})
42
+ from_web(@uri, options)
43
+ end
44
+
45
+ private
46
+ def remote_post_to(uri, content)
47
+
48
+ url = URI.parse(uri)
49
+ req = Net::HTTP::Post.new(url.path)
50
+ req.body = content
51
+ add_basic_request_headers(req)
52
+ req.add_field("Content-type", @content_type)
53
+
54
+ response = Net::HTTP.new(url.host, url.port).request(req)
55
+ parse_post_response(response, content)
56
+ end
57
+
58
+ def parse_post_response(response, content)
59
+ code = response.code
60
+ if code=="301" && @type.follows.moved_permanently? == :all
61
+ remote_post_to(response["Location"], content)
62
+ elsif code=="201"
63
+ from_web(response["Location"], "Accept" => "application/xml")
64
+ else
65
+ response
66
+ end
67
+ end
68
+
69
+ def from_web(uri, options = {})
70
+ uri = URI.parse(uri)
71
+ req = Net::HTTP::Get.new(uri.path)
72
+ options.each do |key,value| req[key] = value end
73
+ add_basic_request_headers(req)
74
+
75
+ res = Net::HTTP.new(uri.host, uri.port).request(req)
76
+ parse_get_response(res)
77
+ end
78
+
79
+ private
80
+
81
+ def add_basic_request_headers(req)
82
+ req.add_field("Accept", @accepts) unless @accepts.nil?
83
+ end
84
+
85
+ # parses a get response.
86
+ # if the result code is 301, redirect
87
+ # otherwise, parses an ok response
88
+ def parse_get_response(res)
89
+
90
+ code = res.code
91
+ return from_web(res["Location"]) if code=="301"
92
+ parse_get_ok_response(res, code)
93
+
94
+ end
95
+
96
+ # parses a successful get response.
97
+ # parses the entity and add extra (response related) fields.
98
+ def parse_get_ok_response(res, code)
99
+ result = parse_get_entity(res, code)
100
+ add_extra_fields(result, res)
101
+ result
102
+ end
103
+
104
+ # add etag, last_modified and web_response fields to the resulting object
105
+ def add_extra_fields(result,res)
106
+ result.etag = res['Etag'] unless res['Etag'].nil?
107
+ result.last_modified = res['Last-Modified'] unless res['Last-Modified'].nil?
108
+ result.web_response = res
109
+ end
110
+
111
+ # returns an entity for a specific response
112
+ def parse_get_entity(res, code)
113
+ if code=="200"
114
+ content_type = res.content_type
115
+ type = Restfulie::MediaType.type_for(content_type)
116
+ if content_type[-3,3]=="xml"
117
+ result = type.from_xml res.body
118
+ elsif content_type[-4,4]=="json"
119
+ result = type.from_json res.body
120
+ else
121
+ raise Restfulie::UnsupportedContentType.new("unsupported content type '#{content_type}'")
122
+ end
123
+ result
124
+ else
125
+ res
126
+ end
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -1,10 +1,14 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
+ require 'vendor/jeokkarak/jeokkarak'
3
4
 
5
+ require 'restfulie/media_type'
6
+ require 'restfulie/client/atom_media_type'
4
7
  require 'restfulie/client/base'
5
8
  require 'restfulie/client/entry_point'
6
9
  require 'restfulie/client/helper'
7
10
  require 'restfulie/client/instance'
11
+ require 'restfulie/client/request_execution'
8
12
  require 'restfulie/client/state'
9
13
 
10
14
  module Restfulie
@@ -23,3 +27,9 @@ end
23
27
  Object.extend Restfulie
24
28
 
25
29
  require 'restfulie/unmarshalling'
30
+
31
+ module Hashi
32
+ class CustomHash
33
+ uses_restfulie
34
+ end
35
+ end
@@ -0,0 +1,124 @@
1
+ require 'restfulie/media_type_control'
2
+
3
+ module Restfulie
4
+
5
+ # represents an error when we are unable to support the desired content type
6
+ class UnsupportedContentType < Exception
7
+
8
+ attr_reader :msg
9
+ def initialize(msg)
10
+ @msg = msg
11
+ end
12
+
13
+ def to_s
14
+ @msg
15
+ end
16
+
17
+ end
18
+
19
+ module MediaType
20
+
21
+ # returns the decoder type for a specific content type
22
+ def self.type_for(content_type)
23
+ if Restfulie::MediaType.supports? content_type
24
+ Restfulie::MediaType.media_type(content_type)
25
+ else
26
+ Restfulie::MediaType::DefaultMediaTypeDecoder
27
+ end
28
+ end
29
+
30
+ # TODO removethis nasty method
31
+ def self.rendering_type(name, type)
32
+ Restfulie::MediaType.media_types[name] || Type.new(name,type)
33
+ end
34
+
35
+ # TODO remove this method
36
+ def self.custom_type(name, type, l)
37
+ Restfulie::MediaType.media_types[name] || CustomType.new(name, type, l)
38
+ end
39
+
40
+ class Type
41
+ attr_reader :name, :type
42
+ def initialize(name, type)
43
+ @name = name
44
+ @type = type
45
+ end
46
+ def short_name
47
+ name.gsub(/\//,'_').gsub(/\+/,'_').gsub(/\./,'_')
48
+ end
49
+
50
+ def format_name
51
+ name[/(.*[\+\/])?(.*)/,2]
52
+ end
53
+ def execute_for(controller, resource, options, render_options)
54
+ response = ["xml", "json"].include?(format_name) ? resource.send(:"to_#{format_name}", options) : resource
55
+ render(controller, response, render_options)
56
+ end
57
+ def render(controller, response, options)
58
+ options[:text] = response
59
+ options[:content_type] = name
60
+ controller.render options
61
+ end
62
+ end
63
+
64
+ class CustomType < Type
65
+ def initialize(name, type, l)
66
+ super(name, type)
67
+ @lambda = l
68
+ end
69
+ def execute_for(controller, resource, options, render_options)
70
+ @lambda.call
71
+ end
72
+ end
73
+
74
+ # from rails source code
75
+ def self.constantize(camel_cased_word)
76
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
77
+ raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
78
+ end
79
+
80
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
81
+ end
82
+
83
+ module DefaultMediaTypeDecoder
84
+
85
+ def self.from_xml(xml)
86
+ hash = Hash.from_xml xml
87
+ from_hash(hash)
88
+ end
89
+
90
+ def self.from_hash(hash)
91
+
92
+ # TODO atom media type from_xml on entry is not working correctly
93
+ # raise "there should be only one root element but got #{hash.keys}" unless hash.keys.size==1
94
+
95
+ type = Restfulie::MediaType.constantize(hash.keys.first.camelize) rescue Hashi
96
+
97
+ result = type.from_hash hash.values.first
98
+ return nil if result.nil?
99
+ if result.respond_to? :_came_from=
100
+ result._came_from = :xml
101
+ end
102
+ result
103
+
104
+ end
105
+
106
+ def self.from_json(json)
107
+ hash = safe_json_decode(json)
108
+ type = hash.keys.first.camelize.constantize
109
+ type.from_hash(hash.values.first)
110
+ end
111
+
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ def safe_json_decode(json)
118
+ return {} if !json
119
+ begin
120
+ ActiveSupport::JSON.decode json
121
+ rescue ; {} end
122
+ end
123
+
124
+ require 'restfulie/media_type_defaults'
@@ -0,0 +1,76 @@
1
+ module Restfulie
2
+
3
+ module MediaTypeControl
4
+
5
+ # defines a custom media_type for this type
6
+ def media_type(*args)
7
+ args.each do |name|
8
+ custom_representations << name
9
+ type = Restfulie::MediaType.rendering_type(name, self)
10
+ Restfulie::MediaType.register(type)
11
+ end
12
+ end
13
+
14
+ # returns the list of media types available for this resource to be deserialized from
15
+ def media_types
16
+ Restfulie::MediaType.default_types + MediaType.medias_for(self)
17
+ end
18
+
19
+ # returns a list of media types that this resource can be serialized to
20
+ def media_type_representations
21
+ custom_representations + Restfulie::MediaType.default_representations.dup
22
+ end
23
+
24
+ private
25
+
26
+ # this model's custom representations. those representations were added through media_type definitions
27
+ def custom_representations
28
+ @custom_representations ||= []
29
+ end
30
+
31
+ end
32
+
33
+
34
+ module MediaType
35
+
36
+ class << self
37
+
38
+ def register(type)
39
+ Mime::Type.register(type.name, type.short_name.to_sym)
40
+ media_types[type.name] = type
41
+ end
42
+
43
+ # TODO rename to type for mt
44
+ def media_type(name)
45
+ name = normalize(name)
46
+ raise Restfulie::UnsupportedContentType.new("unsupported content type '#{name}'") if media_types[name].nil?
47
+ media_types[name].type
48
+ end
49
+
50
+ def supports?(name)
51
+ name = normalize(name)
52
+ !media_types[name].nil?
53
+ end
54
+
55
+ def normalize(name)
56
+ name[/[^;]*/]
57
+ end
58
+
59
+ def media_types
60
+ @media_types ||= {}
61
+ end
62
+
63
+ # TODO move to MediaTypeControl.custom_media_types
64
+ def medias_for(type)
65
+ found = {}
66
+ type.media_type_representations.each do |key|
67
+ found[key] = media_types[key]
68
+ end
69
+ found.values
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,34 @@
1
+ module Restfulie
2
+
3
+ module MediaType
4
+ def self.HtmlType
5
+ custom_type('html', self, lambda {})
6
+ end
7
+ def self.TextHtmlType
8
+ custom_type('text/html', self, lambda {})
9
+ end
10
+
11
+ # TODO rename it and move it
12
+ def self.default_types
13
+ [Restfulie::MediaType.HtmlType,
14
+ Restfulie::MediaType.TextHtmlType,
15
+ rendering_type('application/xml', self),
16
+ rendering_type('application/json', self),
17
+ rendering_type('xml', self),
18
+ rendering_type('json', self)]
19
+ end
20
+
21
+ # Default representations: every object can be serialized to those types
22
+ def self.default_representations
23
+ ['html','text/html','application/xml','application/json','xml','json']
24
+ end
25
+
26
+ # TODO should allow aliases...
27
+ register(Restfulie::MediaType.HtmlType)
28
+ register(Restfulie::MediaType.TextHtmlType)
29
+ register(rendering_type('application/xml', DefaultMediaTypeDecoder))
30
+ register(rendering_type('application/json', DefaultMediaTypeDecoder))
31
+ register(rendering_type('xml', DefaultMediaTypeDecoder))
32
+ register(rendering_type('json', DefaultMediaTypeDecoder))
33
+ end
34
+ end
@@ -0,0 +1,102 @@
1
+ module Restfulie
2
+
3
+ module Server
4
+
5
+ class AtomMediaType < Restfulie::MediaType::Type
6
+ def initialize(name, type)
7
+ super(name, type)
8
+ end
9
+ def execute_for(controller, resource, options, render_options)
10
+ response = ["atom"].include?(format_name) ? resource.send(:"to_#{format_name}", options) : resource
11
+ render(controller, response, render_options)
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ Restfulie::MediaType.register(Restfulie::MediaType.rendering_type('application/atom+xml', Server::AtomMediaType))
18
+
19
+ end
20
+
21
+ class Array
22
+ extend Restfulie::MediaTypeControl
23
+ media_type "application/atom+xml"
24
+
25
+ def to_atom(options = {}, &block)
26
+ AtomFeed.new(self).title(options[:title]).to_atom(options[:controller], block)
27
+ end
28
+
29
+ end
30
+
31
+ class AtomFeed
32
+ def initialize(feed)
33
+ @feed = feed
34
+ @title = "feed"
35
+ end
36
+
37
+ def title(title)
38
+ @title = title
39
+ self
40
+ end
41
+
42
+ def to_atom(controller, block = nil)
43
+ last_modified = updated_at
44
+ id = id_for(controller)
45
+ xml = items_to_atom_xml(controller, block)
46
+ """<?xml version=\"1.0\"?>
47
+ <feed xmlns=\"http://www.w3.org/2005/Atom\">
48
+ <id>#{id}</id>
49
+ <title type=\"text\">#{@title}</title>
50
+ <updated>""" + last_modified.strftime("%Y-%m-%dT%H:%M:%S-08:00") + """</updated>
51
+ <author><name>#{@title}</name></author>
52
+ #{self_link(controller)}
53
+ #{xml}
54
+ </feed>"""
55
+ end
56
+
57
+ def id_for(controller)
58
+ @id || controller.url_for({})
59
+ end
60
+
61
+ def id(*id)
62
+ @id = id
63
+ self
64
+ end
65
+
66
+ def self_link(controller, what = {})
67
+ uri = controller.url_for(what)
68
+ "<link rel=\"self\" href=\"#{uri}\"/>"
69
+ end
70
+
71
+ def updated_at
72
+ last = nil
73
+ @feed.each do |item|
74
+ last = item.updated_at if item.respond_to?(:updated_at) && (last.nil? || item.updated_at > last)
75
+ end
76
+ last || Time.now
77
+ end
78
+
79
+ def items_to_atom_xml(controller, serializer = nil)
80
+ xml = ""
81
+ @feed.each do |item|
82
+ uri = controller.url_for(item)
83
+ media_type = item.class.respond_to?(:media_type_representations) ? item.class.media_type_representations[0] : 'application/xml'
84
+ item_xml = serializer.nil? ? item.to_xml(:controller => controller, :skip_instruct => true) : serializer.call(item)
85
+ xml += """ <entry>
86
+ <id>#{uri}</id>
87
+ <title type=\"text\">#{item.class.name}</title>
88
+ <updated>#{modification_for(item).strftime("%Y-%m-%dT%H:%M:%S-08:00")}</updated>
89
+ #{self_link(controller, item)}
90
+ <content type=\"#{media_type}\">
91
+ #{item_xml}
92
+ </content>
93
+ </entry>\n"""
94
+ end
95
+ xml
96
+ end
97
+
98
+ def modification_for(item)
99
+ item.respond_to?(:updated_at) ? item.updated_at : Time.now
100
+ end
101
+
102
+ end
@@ -3,7 +3,7 @@ module Restfulie
3
3
  module Base
4
4
 
5
5
  # returns the definition for the transaction
6
- def existing_transitions(name)
6
+ def existing_transition(name)
7
7
  transitions[name]
8
8
  end
9
9
 
@@ -45,9 +45,11 @@ module Restfulie
45
45
  @type.define_can_method(@type, name)
46
46
  self
47
47
  end
48
+
48
49
  def at(options)
49
50
  @transition.options = options
50
51
  end
52
+
51
53
  def results_in(result)
52
54
  @transition.result = result
53
55
  end
@@ -3,19 +3,33 @@ module ActionController
3
3
 
4
4
  # renders an specific resource to xml
5
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
6
+ def render_resource(resource, options = {}, render_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
+
12
+ respond_to do |format|
13
+ add_media_responses(format, resource, options, render_options)
14
+ end
15
+ end
16
+ end
17
+
18
+
19
+ def add_media_responses(format, resource, options, render_options)
20
+ types = Restfulie::MediaType.default_types
21
+ types = resource.class.media_types if resource.class.respond_to? :media_types
22
+ types.each do |media_type|
23
+ add_media_response(format, resource, media_type, options, render_options)
24
+ end
25
+ end
26
+
27
+ def add_media_response(format, resource, media_type, options, render_options)
28
+ controller = self
29
+ format.send media_type.short_name.to_sym do
30
+ media_type.execute_for(controller, resource, options, render_options)
31
+ end
32
+ end
19
33
 
20
34
  # adds support to rendering resources, i.e.:
21
35
  # render :resource => @order, :with => { :except => [:paid_at] }
@@ -29,5 +43,27 @@ module ActionController
29
43
  end
30
44
  end
31
45
 
46
+ # renders a created resource including its required headers:
47
+ # Location and 201
48
+ def render_created(resource, options = {})
49
+ location= url_for resource
50
+ render_resource resource, options, {:status => :created, :location => location}
51
+ end
52
+ end
53
+
54
+ module MimeResponds
55
+ class Responder
56
+ attr_reader :mime_type_priority
57
+ alias_method :old_respond, :respond unless method_defined?(:old_respond)
58
+ def respond
59
+ RestfulieResponder.new.respond(self)
60
+ end
61
+ end
62
+
63
+ class RestfulieResponder
64
+ def respond(instance)
65
+ instance.old_respond unless instance.mime_type_priority.include? "html"
66
+ end
67
+ end
32
68
  end
33
69
  end
@@ -25,6 +25,25 @@ module Restfulie
25
25
  raise "Current state #{status} is invalid in order to execute #{name}. It must be one of #{transitions}" unless available_transitions[:allow].include? name
26
26
  self.class.transitions[name].execute_at self
27
27
  end
28
+
29
+ # gets all the links for each transition
30
+ def links(controller)
31
+ links = []
32
+ unless controller.nil?
33
+ all_following_transitions.each do |transition|
34
+ rel, uri = link_for(transition, controller)
35
+ links << {:rel => rel, :uri => uri}
36
+ end
37
+ end
38
+ links
39
+ end
40
+
41
+ private
42
+ # gets a link for this transition
43
+ def link_for(transition, controller)
44
+ transition = self.class.existing_transition(transition.to_sym) unless transition.kind_of? Restfulie::Server::Transition
45
+ transition.link_for(self, controller)
46
+ end
28
47
 
29
48
  end
30
49
  end
@@ -5,36 +5,26 @@ module Restfulie
5
5
 
6
6
  module Marshalling
7
7
 
8
+ # marshalls your object to json.
9
+ # adds all links if there are any available.
8
10
  def to_json(options = {})
9
11
  Hash.from_xml(to_xml(options)).to_json
10
12
  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
- transition = self.class.existing_transitions(transition.to_sym) unless transition.kind_of? Restfulie::Server::Transition
22
- transition.add_link_to(xml, self, options)
23
- end
24
13
 
25
14
  # marshalls your object to xml.
26
15
  # adds all links if there are any available.
27
16
  def to_xml(options = {})
28
-
29
- transitions = all_following_transitions
30
- return super(options) if transitions.empty? || options[:controller].nil?
31
-
32
17
  options[:skip_types] = true
33
18
  super options do |xml|
34
- add_links xml, transitions, options
19
+ links(options[:controller]).each do |link|
20
+ if options[:use_name_based_link]
21
+ xml.tag!(link[:rel], link[:uri])
22
+ else
23
+ xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => link[:rel], :href => link[:uri])
24
+ end
25
+ end
35
26
  end
36
27
  end
37
-
38
28
  end
39
29
 
40
30
  end
@@ -0,0 +1,56 @@
1
+ module Restfulie
2
+
3
+ module Server
4
+
5
+ # Controller which adds default CRUD + search + other operations.
6
+ module Controller
7
+
8
+ # creates a model based on the request media-type extracted from its content-type
9
+ #
10
+ def create
11
+
12
+ type = model_type
13
+ return head 415 unless fits_content(type, request.headers['CONTENT_TYPE'])
14
+
15
+ @model = type.from_xml request.body.string
16
+ if @model.save
17
+ render_created @model
18
+ else
19
+ render :xml => @model.errors, :status => :unprocessable_entity
20
+ end
21
+
22
+ end
23
+
24
+ # renders this resource
25
+ def show
26
+ @model = model_type.find(params[:id])
27
+ instance_variable_set(model_variable_name, @model)
28
+ render_resource(@model)
29
+ end
30
+
31
+ def model_variable_name
32
+ ("@" + model_type.to_s.downcase).to_sym
33
+ end
34
+
35
+ # destroys this resource
36
+ def destroy
37
+ @model = model_type.find(params[:id])
38
+ @model.destroy
39
+ head :ok
40
+ end
41
+
42
+ # returns the model for this controller
43
+ def model_type
44
+ self.class.name[/(.*)Controller/,1].singularize.constantize
45
+ end
46
+
47
+ def fits_content(type, content_type)
48
+ Restfulie::MediaType.supports?(content_type) &&
49
+ type.media_type_representations.include?(content_type)
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -7,49 +7,53 @@ module Restfulie
7
7
  attr_reader :body, :name
8
8
  attr_writer :options
9
9
  attr_accessor :result
10
+
10
11
  def initialize(name, options = {}, result = nil, body = nil)
11
12
  @name = name
12
13
  @options = options
13
14
  @result = result
14
15
  @body = body
15
16
  end
17
+
16
18
  def action
17
19
  @options || {}
18
20
  end
19
21
 
20
22
  # executes this transition in a resource
21
23
  def execute_at(target_object)
22
- target_object.status = result.to_s unless result.nil?
24
+ target_object.status = @result.to_s unless @result.nil?
23
25
  end
24
26
 
25
- # adds a link to this transition's uri on a xml writer
26
- def add_link_to(xml, model, options)
27
- specific_action = action.dup
28
- specific_action = @body.call(model) if @body
27
+ # return the link to this transitions's uri
28
+ def link_for(model, controller)
29
+ specific_action = @body ? @body.call(model) : action.dup
29
30
 
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
31
+ specific_action = parse_specific_action(specific_action, model)
41
32
 
42
33
  rel = specific_action[:rel] || @name
43
34
  specific_action[:rel] = nil
44
35
 
45
36
  specific_action[:action] ||= @name
46
- uri = options[:controller].url_for(specific_action)
37
+ uri = controller.url_for(specific_action)
38
+
39
+ return rel, uri
40
+ end
47
41
 
48
- if options[:use_name_based_link]
49
- xml.tag!(rel, uri)
50
- else
51
- xml.tag!('atom:link', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', :rel => rel, :href => uri)
42
+ private
43
+
44
+ # if you use the class level DSL, you will need to add a lambda for instance level accessors:
45
+ # transition :show, {:action => :show, :foo_id => lambda { |model| model.id }}
46
+ # but you can replace it for a symbol and defer the model call
47
+ # transition :show, {:action => :show, :foo_id => :id}
48
+ def parse_specific_action(action, model)
49
+ action.inject({}) do |actions, pair|
50
+ if pair.last.is_a?( Symbol ) && model.attributes.include?(pair.last)
51
+ actions.merge!( pair.first => model.send(pair.last) )
52
+ else
53
+ actions.merge!( pair.first => pair.last )
54
+ end
52
55
  end
56
+ action
53
57
  end
54
58
 
55
59
  end
@@ -28,8 +28,7 @@ module Restfulie
28
28
  # found at http://www.xcombinator.com/2008/08/11/activerecord-from_xml-and-from_json-part-2/
29
29
  # addapted to support links
30
30
  def from_hash( hash )
31
- h = {}
32
- h = hash.dup if hash
31
+ h = hash ? hash.dup : {}
33
32
  links = nil
34
33
  h.each do |key,value|
35
34
  case value.class.to_s
@@ -62,7 +61,6 @@ end
62
61
  module ActiveRecord
63
62
  class Base
64
63
  extend Restfulie::Unmarshalling
65
- # acts_as_jeokkarak
66
64
 
67
65
  def self.from_json(json)
68
66
  from_hash(safe_json_decode(json).values.first)
data/lib/restfulie.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
+ require 'vendor/jeokkarak/jeokkarak'
4
+
5
+ require 'restfulie/media_type'
3
6
 
4
7
  require 'restfulie/client'
5
8
 
@@ -8,6 +11,8 @@ require 'restfulie/server/controller'
8
11
  require 'restfulie/server/instance'
9
12
  require 'restfulie/server/marshalling'
10
13
  require 'restfulie/server/transition'
14
+ require 'restfulie/server/restfulie_controller'
15
+ require 'restfulie/server/atom_media_type'
11
16
 
12
17
  module Restfulie
13
18
 
@@ -29,6 +34,7 @@ module Restfulie
29
34
  # the transition's uri.
30
35
  def acts_as_restfulie
31
36
  extend Restfulie::Server::Base
37
+ extend Restfulie::MediaTypeControl
32
38
  include Restfulie::Server::Instance
33
39
  include Restfulie::Server::Marshalling
34
40
 
@@ -38,7 +44,7 @@ module Restfulie
38
44
  transitions
39
45
  end
40
46
  end
41
-
47
+
42
48
  end
43
49
 
44
50
  Object.extend Restfulie
@@ -0,0 +1,61 @@
1
+ module Hashi
2
+ class UndefinedMethod < Exception
3
+ attr_reader :msg
4
+ def initialize(msg)
5
+ @msg = msg
6
+ end
7
+ def to_s
8
+ @msg
9
+ end
10
+ end
11
+
12
+ class CustomHash
13
+
14
+ attr_reader :hash
15
+
16
+ def initialize(h = {})
17
+ @hash = h
18
+ end
19
+
20
+ def method_missing(name, *args)
21
+ name = name.to_s if name.kind_of? Symbol
22
+ if name[-1,1] == "?"
23
+ parse(name, @hash[name.chop])
24
+ elsif name[-1,1] == "="
25
+ @hash[name.chop] = args[0]
26
+ else
27
+ return nil if @hash.has_key?(name) && @hash[name].nil?
28
+ parse(name, transform(@hash[name]))
29
+ end
30
+ end
31
+
32
+ def respond_to?(symbol)
33
+ super.respond_to?(symbol) || @hash.key?(symbol.to_s)
34
+ end
35
+
36
+ def [](x)
37
+ transform(@hash[x])
38
+ end
39
+
40
+ private
41
+ def transform(value)
42
+ return CustomHash.new(value) if value.kind_of?(Hash) || value.kind_of?(Array)
43
+ value
44
+ end
45
+
46
+ def parse(name, val)
47
+ raise Hashi::UndefinedMethod.new("undefined method '#{name}'") if val.nil?
48
+ val
49
+ end
50
+
51
+ end
52
+
53
+ def self.from_hash(h)
54
+ CustomHash.new(h)
55
+ end
56
+
57
+ def self.to_object(h)
58
+ CustomHash.new(h)
59
+ end
60
+
61
+ end
@@ -0,0 +1,65 @@
1
+ require 'vendor/jeokkarak/hashi'
2
+
3
+ module Jeokkarak
4
+ module Base
5
+
6
+ # defines that this type has a child element
7
+ def has_child(type, options={})
8
+ resource_children[options[:as]] = type
9
+ end
10
+
11
+ # checks what is the type element for this type (supports rails ActiveRecord, has_child and Hashi)
12
+ def child_type_for(name)
13
+ return reflect_on_association(name.to_sym ).klass if respond_to? :reflect_on_association
14
+ resource_children[name] || Hashi
15
+ end
16
+
17
+ # returns the registered children list for this resource
18
+ def resource_children
19
+ @children ||= {}
20
+ @children
21
+ end
22
+
23
+ # creates an instance of this type based on this hash
24
+ def from_hash(h)
25
+ h = {} if h.nil? # nasty required check
26
+ h = h.dup
27
+ result = self.new
28
+ result._internal_hash = h
29
+ h.each do |key,value|
30
+ from_hash_parse result, h, key, value
31
+ end
32
+ def result.method_missing(name, *args, &block)
33
+ Hashi.to_object(@_internal_hash).send(name, args[0], block)
34
+ end
35
+ result
36
+ end
37
+
38
+ # extension point to parse a value
39
+ def from_hash_parse(result,h,key,value)
40
+ case value.class.to_s
41
+ when 'Array'
42
+ h[key].map! { |e| child_type_for(key).from_hash e }
43
+ when /\AHash(WithIndifferentAccess)?\Z/
44
+ h[key] = child_type_for(key ).from_hash value
45
+ end
46
+ name = "#{key}="
47
+ result.send(name, value) if result.respond_to?(name)
48
+ end
49
+ end
50
+ end
51
+
52
+ module Jeokkarak
53
+ module Config
54
+
55
+ # entry point to define a jeokkarak type
56
+ def acts_as_jeokkarak
57
+ self.module_eval do
58
+ attr_accessor :_internal_hash
59
+ end
60
+ self.extend Jeokkarak::Base
61
+ end
62
+ end
63
+ end
64
+
65
+ Object.extend Jeokkarak::Config
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restfulie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.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-12-10 00:00:00 -02:00
12
+ date: 2010-01-06 00:00:00 -02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,23 +22,33 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
 
24
24
  files:
25
+ - lib/restfulie/client/atom_media_type.rb
25
26
  - lib/restfulie/client/base.rb
26
27
  - lib/restfulie/client/entry_point.rb
27
28
  - lib/restfulie/client/helper.rb
28
29
  - lib/restfulie/client/instance.rb
30
+ - lib/restfulie/client/request_execution.rb
29
31
  - lib/restfulie/client/state.rb
30
32
  - lib/restfulie/client.rb
33
+ - lib/restfulie/media_type.rb
34
+ - lib/restfulie/media_type_control.rb
35
+ - lib/restfulie/media_type_defaults.rb
36
+ - lib/restfulie/server/atom_media_type.rb
31
37
  - lib/restfulie/server/base.rb
32
38
  - lib/restfulie/server/controller.rb
33
39
  - lib/restfulie/server/instance.rb
34
40
  - lib/restfulie/server/marshalling.rb
41
+ - lib/restfulie/server/restfulie_controller.rb
35
42
  - lib/restfulie/server/transition.rb
36
43
  - lib/restfulie/unmarshalling.rb
37
44
  - lib/restfulie.rb
45
+ - lib/vendor/jeokkarak/hashi.rb
46
+ - lib/vendor/jeokkarak/jeokkarak.rb
47
+ - LICENSE
38
48
  - Rakefile
39
49
  - README.textile
40
50
  has_rdoc: true
41
- homepage: http://github.com/caelum/restfulie
51
+ homepage: http://restfulie.caelumobjects.com
42
52
  licenses: []
43
53
 
44
54
  post_install_message: