restfully 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6 -0
- data/README.rdoc +82 -92
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/restfully +10 -46
- data/examples/grid5000.rb +5 -5
- data/lib/restfully.rb +3 -2
- data/lib/restfully/collection.rb +64 -139
- data/lib/restfully/extensions.rb +0 -17
- data/lib/restfully/http/adapters/abstract_adapter.rb +1 -1
- data/lib/restfully/http/adapters/rest_client_adapter.rb +25 -3
- data/lib/restfully/http/request.rb +20 -3
- data/lib/restfully/http/response.rb +21 -3
- data/lib/restfully/link.rb +4 -3
- data/lib/restfully/resource.rb +144 -74
- data/lib/restfully/session.rb +36 -22
- data/restfully.gemspec +9 -4
- data/spec/collection_spec.rb +24 -25
- data/spec/http/request_spec.rb +6 -2
- data/spec/http/response_spec.rb +4 -0
- data/spec/http/rest_client_adapter_spec.rb +3 -2
- data/spec/link_spec.rb +7 -4
- data/spec/resource_spec.rb +31 -56
- data/spec/session_spec.rb +77 -41
- metadata +13 -2
data/lib/restfully/extensions.rb
CHANGED
@@ -4,23 +4,6 @@ end unless defined?(BasicObject)
|
|
4
4
|
|
5
5
|
# monkey patching:
|
6
6
|
class Hash
|
7
|
-
# Taken from ActiveSupport
|
8
|
-
def symbolize_keys
|
9
|
-
inject({}) do |options, (key, value)|
|
10
|
-
options[(key.to_sym rescue key) || key] = value
|
11
|
-
options
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
# Taken from ActiveSupport
|
16
|
-
def stringify_keys
|
17
|
-
inject({}) do |options, (key, value)|
|
18
|
-
options[key.to_s] = value
|
19
|
-
options
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
7
|
# This is by no means the standard way to transform ruby objects into query parameters
|
25
8
|
# but it is targeted at our own needs
|
26
9
|
def to_params
|
@@ -21,7 +21,7 @@ module Restfully
|
|
21
21
|
raise NotImplementedError, "PUT is not supported by your adapter."
|
22
22
|
end
|
23
23
|
def delete(request)
|
24
|
-
raise NotImplementedError, "
|
24
|
+
raise NotImplementedError, "DELETE is not supported by your adapter."
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
@@ -5,14 +5,35 @@ module Restfully
|
|
5
5
|
module HTTP
|
6
6
|
module Adapters
|
7
7
|
class RestClientAdapter < AbstractAdapter
|
8
|
+
|
8
9
|
def initialize(base_uri, options = {})
|
9
10
|
super(base_uri, options)
|
10
11
|
@options[:user] = @options.delete(:username)
|
11
|
-
end
|
12
|
+
end # def initialize
|
13
|
+
|
14
|
+
def head(request)
|
15
|
+
in_order_to_get_the_response_to(request) do |resource|
|
16
|
+
resource.head(request.headers)
|
17
|
+
end
|
18
|
+
end # def get
|
19
|
+
|
12
20
|
def get(request)
|
21
|
+
in_order_to_get_the_response_to(request) do |resource|
|
22
|
+
resource.get(request.headers)
|
23
|
+
end
|
24
|
+
end # def get
|
25
|
+
|
26
|
+
def post(request)
|
27
|
+
in_order_to_get_the_response_to(request) do |resource|
|
28
|
+
resource.post(request.raw_body, request.headers)
|
29
|
+
end
|
30
|
+
end # def post
|
31
|
+
|
32
|
+
protected
|
33
|
+
def in_order_to_get_the_response_to(request, &block)
|
13
34
|
begin
|
14
35
|
resource = RestClient::Resource.new(request.uri.to_s, @options)
|
15
|
-
response =
|
36
|
+
response = block.call(resource)
|
16
37
|
headers = response.headers
|
17
38
|
body = response.to_s
|
18
39
|
headers.delete(:status)
|
@@ -23,7 +44,8 @@ module Restfully
|
|
23
44
|
status = e.http_code
|
24
45
|
end
|
25
46
|
Response.new(status, headers, body)
|
26
|
-
end
|
47
|
+
end # def in_order_to_get_the_response_to
|
48
|
+
|
27
49
|
end
|
28
50
|
|
29
51
|
end
|
@@ -2,8 +2,8 @@ require 'uri'
|
|
2
2
|
module Restfully
|
3
3
|
module HTTP
|
4
4
|
class Request
|
5
|
-
include Headers
|
6
|
-
attr_reader :headers, :
|
5
|
+
include Headers, Restfully::Parsing
|
6
|
+
attr_reader :headers, :uri
|
7
7
|
attr_accessor :retries
|
8
8
|
def initialize(url, options = {})
|
9
9
|
options = options.symbolize_keys
|
@@ -12,10 +12,27 @@ module Restfully
|
|
12
12
|
if query = options.delete(:query)
|
13
13
|
@uri.query = [@uri.query, query.to_params].compact.join("&")
|
14
14
|
end
|
15
|
-
@body = body
|
15
|
+
@body = options.delete(:body)
|
16
16
|
@retries = 0
|
17
17
|
end
|
18
18
|
|
19
|
+
|
20
|
+
def body
|
21
|
+
if @body.kind_of?(String)
|
22
|
+
@unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
|
23
|
+
else
|
24
|
+
@body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw_body
|
29
|
+
if @body.kind_of?(String)
|
30
|
+
@body
|
31
|
+
else
|
32
|
+
@serialized_body ||= serialize(@body, :content_type => @headers['Content-Type'])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
19
36
|
def add_headers(headers = {})
|
20
37
|
@headers.merge!(sanitize_http_headers(headers || {}))
|
21
38
|
end
|
@@ -1,19 +1,37 @@
|
|
1
1
|
module Restfully
|
2
2
|
module HTTP
|
3
3
|
# Container for an HTTP Response. Has <tt>status</tt>, <tt>headers</tt> and <tt>body</tt> properties.
|
4
|
-
# The body is automatically parsed into a ruby object based on the response's <tt>Content-Type</tt> header.
|
5
4
|
class Response
|
6
5
|
include Headers, Restfully::Parsing
|
7
6
|
attr_reader :status, :headers
|
7
|
+
|
8
|
+
# <tt>body</tt>:: may be a string (that will be parsed when calling the #body function), or an object (that will be returned as is when calling the #body function)
|
8
9
|
def initialize(status, headers, body)
|
9
10
|
@status = status.to_i
|
10
11
|
@headers = sanitize_http_headers(headers)
|
11
|
-
@body =
|
12
|
+
@body = body
|
12
13
|
end
|
13
14
|
|
14
15
|
def body
|
15
|
-
|
16
|
+
if @body.kind_of?(String)
|
17
|
+
if @body.empty?
|
18
|
+
nil
|
19
|
+
else
|
20
|
+
@unserialized_body ||= unserialize(@body, :content_type => @headers['Content-Type'])
|
21
|
+
end
|
22
|
+
else
|
23
|
+
@body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def raw_body
|
28
|
+
if @body.kind_of?(String)
|
29
|
+
@body
|
30
|
+
else
|
31
|
+
@serialized_body ||= serialize(@body, :content_type => @headers['Content-Type']) unless @body.nil?
|
32
|
+
end
|
16
33
|
end
|
34
|
+
|
17
35
|
end
|
18
36
|
end
|
19
37
|
end
|
data/lib/restfully/link.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'uri'
|
1
2
|
module Restfully
|
2
3
|
class Link
|
3
4
|
|
@@ -9,7 +10,7 @@ module Restfully
|
|
9
10
|
def initialize(attributes = {})
|
10
11
|
@rel = attributes['rel']
|
11
12
|
@title = attributes['title']
|
12
|
-
@href = attributes['href']
|
13
|
+
@href = URI.parse(attributes['href'].to_s)
|
13
14
|
@resolvable = attributes['resolvable'] || false
|
14
15
|
@resolved = attributes['resolved'] || false
|
15
16
|
end
|
@@ -20,8 +21,8 @@ module Restfully
|
|
20
21
|
|
21
22
|
def valid?
|
22
23
|
@errors = []
|
23
|
-
if href.nil?
|
24
|
-
errors << "href cannot be
|
24
|
+
if href.nil?
|
25
|
+
errors << "href cannot be nil."
|
25
26
|
end
|
26
27
|
unless VALID_RELATIONSHIPS.include?(rel)
|
27
28
|
errors << "#{rel} is not a valid link relationship."
|
data/lib/restfully/resource.rb
CHANGED
@@ -1,111 +1,180 @@
|
|
1
|
-
require 'delegate'
|
2
|
-
|
3
1
|
module Restfully
|
4
2
|
|
5
|
-
#
|
6
|
-
|
7
|
-
|
3
|
+
# This class represents a Resource, which can be accessed and manipulated
|
4
|
+
# via HTTP methods.
|
5
|
+
#
|
6
|
+
# The <tt>#load</tt> method must have been called on the resource before
|
7
|
+
# trying to access its attributes or links.
|
8
|
+
#
|
9
|
+
class Resource
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
attr_reader :uri,
|
12
|
+
:session,
|
13
|
+
:links,
|
14
|
+
:title,
|
15
|
+
:properties,
|
16
|
+
:executed_requests
|
17
|
+
|
18
|
+
# == Description
|
19
|
+
# Creates a new Resource.
|
20
|
+
# <tt>uri</tt>:: a URI object representing the URI of the resource
|
21
|
+
# (complete, absolute or relative URI)
|
22
|
+
# <tt>session</tt>:: an instantiated Restfully::Session object
|
23
|
+
# <tt>options</tt>:: a hash of options (see below)
|
24
|
+
# == Options
|
25
|
+
# <tt>:title</tt>:: an optional title for the resource
|
12
26
|
def initialize(uri, session, options = {})
|
13
27
|
options = options.symbolize_keys
|
14
|
-
@uri = uri
|
28
|
+
@uri = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
|
15
29
|
@session = session
|
16
|
-
@
|
17
|
-
|
18
|
-
super(@attributes)
|
19
|
-
@associations = {}
|
30
|
+
@title = options[:title]
|
31
|
+
reset
|
20
32
|
end
|
21
33
|
|
22
|
-
|
34
|
+
# Resets all the inner objects of the resource
|
35
|
+
# (you must call <tt>#load</tt> if you want to repopulate the resource).
|
36
|
+
def reset
|
37
|
+
@executed_requests = Hash.new
|
38
|
+
@links = Hash.new
|
39
|
+
@properties = Hash.new
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# == Description
|
44
|
+
# Returns the value corresponding to the specified key,
|
45
|
+
# among the list of resource properties
|
46
|
+
#
|
47
|
+
# == Usage
|
48
|
+
# resource["uid"]
|
49
|
+
# => "rennes"
|
50
|
+
def [](key)
|
51
|
+
@properties[key]
|
52
|
+
end
|
23
53
|
|
24
54
|
def method_missing(method, *args)
|
25
|
-
if
|
26
|
-
session.logger.debug "Loading
|
27
|
-
|
55
|
+
if link = @links[method.to_s]
|
56
|
+
session.logger.debug "Loading link #{method}, args=#{args.inspect}"
|
57
|
+
link.load(*args)
|
28
58
|
else
|
29
59
|
super(method, *args)
|
30
60
|
end
|
31
61
|
end
|
32
|
-
|
62
|
+
|
63
|
+
# == Description
|
64
|
+
# Executes a GET request on the resource, and populate the list of its
|
65
|
+
# properties and links
|
66
|
+
# <tt>options</tt>:: list of options to pass to the request (see below)
|
67
|
+
# == Options
|
68
|
+
# <tt>:reload</tt>:: if set to true, a GET request will be triggered even if the resource has already been loaded [default=false]
|
69
|
+
# <tt>:query</tt>:: a hash of query parameters to pass along the request. E.g. : resource.load(:query => {:from => (Time.now-3600).to_i, :to => Time.now.to_i})
|
70
|
+
# <tt>:headers</tt>:: a hash of HTTP headers to pass along the request. E.g. : resource.load(:headers => {'Accept' => 'application/json'})
|
71
|
+
# <tt>:body</tt>:: if you already have the unserialized response body of this resource, you may pass it so that the GET request is not triggered.
|
33
72
|
def load(options = {})
|
34
73
|
options = options.symbolize_keys
|
35
|
-
force_reload = !!options.delete(:reload)
|
36
|
-
if
|
74
|
+
force_reload = !!options.delete(:reload)
|
75
|
+
if !force_reload && (request = executed_requests['GET']) && request['options'] == options && request['body']
|
37
76
|
self
|
38
77
|
else
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
(raw['links'] || []).each{|link| define_link(Link.new(link))}
|
47
|
-
raw.each do |key, value|
|
48
|
-
case key
|
49
|
-
# when "uid", "type"
|
50
|
-
# instance_variable_set "@#{key}".to_sym, value
|
51
|
-
when 'links' then next
|
52
|
-
else
|
53
|
-
case value
|
54
|
-
when Hash
|
55
|
-
@attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
|
56
|
-
when Array
|
57
|
-
@attributes.store(key, SpecialArray.new(value))
|
58
|
-
else
|
59
|
-
@attributes.store(key, value)
|
60
|
-
end
|
61
|
-
end
|
78
|
+
reset
|
79
|
+
executed_requests['GET'] = {
|
80
|
+
'options' => options,
|
81
|
+
'body' => options[:body] || session.get(uri, options).body
|
82
|
+
}
|
83
|
+
executed_requests['GET']['body'].each do |key, value|
|
84
|
+
populate_object(key, value)
|
62
85
|
end
|
63
|
-
@state = :loaded
|
64
86
|
self
|
65
87
|
end
|
66
88
|
end
|
89
|
+
|
90
|
+
def submit(payload, options = {})
|
91
|
+
options = options.symbolize_keys
|
92
|
+
raise NotImplementedError, "The POST method is not allowed for this resource." unless http_methods.include?('POST')
|
93
|
+
raise ArgumentError, "You must pass a payload string" unless payload.kind_of?(String)
|
94
|
+
session.post(payload, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
# == Description
|
98
|
+
# Returns the list of allowed HTTP methods on the resource.
|
99
|
+
# == Usage
|
100
|
+
# resource.http_methods
|
101
|
+
# => ['GET', 'POST']
|
102
|
+
#
|
103
|
+
def http_methods
|
104
|
+
if executed_requests['HEAD'].nil?
|
105
|
+
response = session.head(uri)
|
106
|
+
executed_requests['HEAD'] = {'headers' => response.headers}
|
107
|
+
end
|
108
|
+
(executed_requests['HEAD']['headers']['Allow'] || "GET").split(/,\s*/)
|
109
|
+
end
|
67
110
|
|
68
111
|
def respond_to?(method, *args)
|
69
|
-
@
|
112
|
+
@links.has_key?(method.to_s) || super(method, *args)
|
70
113
|
end
|
71
114
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
115
|
+
def inspect(*args)
|
116
|
+
@properties.inspect(*args)
|
117
|
+
end
|
118
|
+
|
119
|
+
def pretty_print(pp)
|
120
|
+
pp.text "#<#{self.class}:0x#{self.object_id.to_s(16)}"
|
121
|
+
pp.nest 2 do
|
122
|
+
pp.breakable
|
123
|
+
pp.text "@uri="
|
124
|
+
uri.pretty_print(pp)
|
125
|
+
if @links.length > 0
|
126
|
+
pp.breakable
|
127
|
+
pp.text "LINKS"
|
128
|
+
pp.nest 2 do
|
129
|
+
@links.to_a.each_with_index do |(key, value), i|
|
130
|
+
pp.breakable
|
131
|
+
pp.text "@#{key}=#<#{value.class}:0x#{value.object_id.to_s(16)}>"
|
132
|
+
pp.text "," if i < @links.length-1
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
if @properties.length > 0
|
137
|
+
pp.breakable
|
138
|
+
pp.text "PROPERTIES"
|
139
|
+
pp.nest 2 do
|
140
|
+
@properties.to_a.each_with_index do |(key, value), i|
|
141
|
+
pp.breakable
|
142
|
+
pp.text "#{key.inspect}=>"
|
143
|
+
value.pretty_print(pp)
|
144
|
+
pp.text "," if i < @properties.length-1
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
yield pp if block_given?
|
149
|
+
end
|
150
|
+
pp.text ">"
|
151
|
+
end
|
92
152
|
|
93
153
|
protected
|
154
|
+
def populate_object(key, value)
|
155
|
+
case key
|
156
|
+
when "links"
|
157
|
+
value.each{|link| define_link(Link.new(link))}
|
158
|
+
else
|
159
|
+
case value
|
160
|
+
when Hash
|
161
|
+
@properties.store(key, SpecialHash.new.replace(value)) unless @links.has_key?(key)
|
162
|
+
when Array
|
163
|
+
@properties.store(key, SpecialArray.new(value))
|
164
|
+
else
|
165
|
+
@properties.store(key, value)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
94
169
|
def define_link(link)
|
95
170
|
if link.valid?
|
96
171
|
case link.rel
|
97
172
|
when 'parent'
|
98
|
-
@
|
173
|
+
@links['parent'] = Resource.new(uri.merge(link.href), session)
|
99
174
|
when 'collection'
|
100
|
-
|
101
|
-
@associations[link.title] = Collection.new(link.href, session,
|
102
|
-
:raw => raw_included,
|
103
|
-
:title => link.title)
|
175
|
+
@links[link.title] = Collection.new(uri.merge(link.href), session, :title => link.title)
|
104
176
|
when 'member'
|
105
|
-
|
106
|
-
@associations[link.title] = Resource.new(link.href, session,
|
107
|
-
:title => link.title,
|
108
|
-
:raw => raw_included)
|
177
|
+
@links[link.title] = Resource.new(uri.merge(link.href), session, :title => link.title)
|
109
178
|
when 'self'
|
110
179
|
# we do nothing
|
111
180
|
end
|
@@ -113,6 +182,7 @@ module Restfully
|
|
113
182
|
session.logger.warn link.errors.join("\n")
|
114
183
|
end
|
115
184
|
end
|
185
|
+
|
116
186
|
|
117
187
|
end # class Resource
|
118
188
|
end # module Restfully
|