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