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 +17 -0
- data/README.textile +4 -4
- data/Rakefile +4 -2
- data/lib/restfulie/client/atom_media_type.rb +58 -0
- data/lib/restfulie/client/base.rb +24 -17
- data/lib/restfulie/client/entry_point.rb +4 -49
- data/lib/restfulie/client/instance.rb +16 -9
- data/lib/restfulie/client/request_execution.rb +131 -0
- data/lib/restfulie/client.rb +10 -0
- data/lib/restfulie/media_type.rb +124 -0
- data/lib/restfulie/media_type_control.rb +76 -0
- data/lib/restfulie/media_type_defaults.rb +34 -0
- data/lib/restfulie/server/atom_media_type.rb +102 -0
- data/lib/restfulie/server/base.rb +3 -1
- data/lib/restfulie/server/controller.rb +49 -13
- data/lib/restfulie/server/instance.rb +19 -0
- data/lib/restfulie/server/marshalling.rb +9 -19
- data/lib/restfulie/server/restfulie_controller.rb +56 -0
- data/lib/restfulie/server/transition.rb +25 -21
- data/lib/restfulie/unmarshalling.rb +1 -3
- data/lib/restfulie.rb +7 -1
- data/lib/vendor/jeokkarak/hashi.rb +61 -0
- data/lib/vendor/jeokkarak/jeokkarak.rb +65 -0
- metadata +13 -3
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.
|
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://
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
22
|
-
|
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
|
34
|
-
def add_transitions(
|
40
|
+
# inserts all links from this object as can_xxx and xxx methods
|
41
|
+
def add_transitions(links)
|
35
42
|
|
36
|
-
|
37
|
-
self._possible_states[
|
38
|
-
self.add_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
|
data/lib/restfulie/client.rb
CHANGED
@@ -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
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
#
|
26
|
-
def
|
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
|
-
|
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 =
|
37
|
+
uri = controller.url_for(specific_action)
|
38
|
+
|
39
|
+
return rel, uri
|
40
|
+
end
|
47
41
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
+
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:
|
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://
|
51
|
+
homepage: http://restfulie.caelumobjects.com
|
42
52
|
licenses: []
|
43
53
|
|
44
54
|
post_install_message:
|