restfully 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -1
- data/README.rdoc +29 -4
- data/VERSION +1 -1
- data/examples/grid5000.rb +8 -3
- data/lib/restfully/collection.rb +11 -1
- data/lib/restfully/resource.rb +63 -13
- data/lib/restfully.rb +1 -1
- data/restfully.gemspec +2 -2
- data/spec/collection_spec.rb +56 -65
- data/spec/resource_spec.rb +64 -6
- metadata +2 -2
data/CHANGELOG
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
0.4.1
|
2
2
|
* added a require 'yaml' in restfully.rb (priteau);
|
3
|
-
* added an introductory message when
|
3
|
+
* added an introductory message when using the command-line tool;
|
4
4
|
* removed [root_path] from the list of options in command-line client 'Usage' banner (priteau);
|
5
5
|
* added test for :configuration_file option;
|
6
6
|
* tests are no longer failing on ruby1.9;
|
data/README.rdoc
CHANGED
@@ -1,8 +1,25 @@
|
|
1
1
|
= restfully
|
2
2
|
|
3
|
-
An attempt at dynamically providing "clever" wrappers on top of RESTful APIs that follow the principle of Hyperlinks As The Engine Of Application State (HATEOAS).
|
4
|
-
|
5
|
-
|
3
|
+
An attempt at dynamically providing "clever" wrappers on top of RESTful APIs that follow the principle of Hyperlinks As The Engine Of Application State (HATEOAS). It does not require to use specific (and often complex) server-side libraries, but a few constraints and conventions must be followed:
|
4
|
+
1. Return sensible HTTP status codes;
|
5
|
+
2. Make use of GET, POST, PUT, DELETE HTTP methods;
|
6
|
+
3. Return a Location HTTP header in 201 or 202 responses;
|
7
|
+
4. Return a <tt>links</tt> property in all responses to a GET request, that contains a list of link objects:
|
8
|
+
|
9
|
+
{
|
10
|
+
"property": "value",
|
11
|
+
"links": [
|
12
|
+
{"rel": "self", "href": "uri/to/resource", "type": "application/vnd.whatever+json;level=1,application/json"},
|
13
|
+
{"rel": "parent", "href": "uri/to/parent/resource", "type": "application/json"}
|
14
|
+
{"rel": "collection", "href": "uri/to/collection", "title": "my_collection", "type": "application/json"},
|
15
|
+
{"rel": "member", "href": "uri/to/member", "title": "member_title", "type": "application/json"}
|
16
|
+
]
|
17
|
+
}
|
18
|
+
|
19
|
+
* Adding a <tt>parent</tt> link automatically creates a <tt>#parent</tt> method on the current resource.
|
20
|
+
* Adding a <tt>collection</tt> link automatically creates a <tt>#my_collection</tt> method that will fetch the Collection when called.
|
21
|
+
* Adding a <tt>member</tt> link automatically creates a <tt>#member_title</tt> method that will fetch the Resource when called.
|
22
|
+
5. Advertise allowed HTTP methods in the response to GET requests by returning a <tt>Allow</tt> HTTP header containing a comma-separated list of the HTTP methods that can be used on the resource. This will allow the automatic generation of methods to interact with the resource. e.g.: advertising a <tt>POST</tt> method (<tt>Allow: GET, POST</tt>) will result in the creation of a <tt>submit</tt> method on the resource.
|
6
23
|
|
7
24
|
== Installation
|
8
25
|
$ gem install restfully --source http://gemcutter.org
|
@@ -89,8 +106,11 @@ or:
|
|
89
106
|
irb(main):006:0> root.sites.map{|s| s['uid']}.grep(/re/)
|
90
107
|
=> ["grenoble", "rennes"]
|
91
108
|
|
109
|
+
A shortcut is available to find a specific entry in a collection, by entering the searched uid as a Symbol:
|
110
|
+
irb(main):007:0> root.sites[:rennes]
|
111
|
+
# will find the item whose uid is "rennes"
|
92
112
|
|
93
|
-
|
113
|
+
For ease of use and better security, you may prefer to use a configuration file to avoid re-entering the options every time you use the client:
|
94
114
|
$ echo '
|
95
115
|
base_uri: https://api.grid5000.fr/sid/grid5000
|
96
116
|
username: MYLOGIN
|
@@ -103,6 +123,11 @@ And then:
|
|
103
123
|
=== As a library
|
104
124
|
See the +examples+ directory for examples.
|
105
125
|
|
126
|
+
== Discovering the API capabilities
|
127
|
+
A Restfully::Resource (and by extension its child Restfully::Collection) has the following methods available for introspection:
|
128
|
+
* <tt>links</tt> will return a hash whose keys are the name of the methods that can be called to navigate between resources;
|
129
|
+
* <tt>http_methods</tt> will return an array containing the list of the HTTP methods that are allowed on the resource;
|
130
|
+
|
106
131
|
== Note on Patches/Pull Requests
|
107
132
|
|
108
133
|
* Fork the project.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/examples/grid5000.rb
CHANGED
@@ -7,10 +7,15 @@ require File.dirname(__FILE__)+'/../lib/restfully'
|
|
7
7
|
logger = Logger.new(STDOUT)
|
8
8
|
logger.level = Logger::WARN
|
9
9
|
|
10
|
-
# Restfully.adapter = Restfully::HTTP::RestClientAdapter
|
11
|
-
# Restfully.adapter = Patron::Session
|
12
10
|
RestClient.log = 'stdout'
|
13
|
-
|
11
|
+
|
12
|
+
# This yaml file contains the following attributes:
|
13
|
+
# username: my_username
|
14
|
+
# password: my_password
|
15
|
+
options = YAML.load_file(File.expand_path('~/.restfully/api.grid5000.fr.yml'))
|
16
|
+
options[:base_uri] = 'https://api.grid5000.fr/sid/grid5000'
|
17
|
+
options[:logger] = logger
|
18
|
+
Restfully::Session.new(options) do |grid, session|
|
14
19
|
grid_stats = {'hardware' => {}, 'system' => {}}
|
15
20
|
grid.sites.each do |site|
|
16
21
|
site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, node_status|
|
data/lib/restfully/collection.rb
CHANGED
@@ -28,6 +28,16 @@ module Restfully
|
|
28
28
|
def each(*args, &block)
|
29
29
|
@items.each(*args, &block)
|
30
30
|
end
|
31
|
+
|
32
|
+
# if property is a Symbol, it tries to find the corresponding item whose uid.to_sym is matching the property
|
33
|
+
# else, returns the result of calling <tt>[]</tt> on its superclass.
|
34
|
+
def [](property)
|
35
|
+
if property.kind_of?(Symbol)
|
36
|
+
find{|i| i['uid'] == property.to_s}
|
37
|
+
else
|
38
|
+
super(property)
|
39
|
+
end
|
40
|
+
end
|
31
41
|
|
32
42
|
def populate_object(key, value)
|
33
43
|
case key
|
@@ -38,7 +48,7 @@ module Restfully
|
|
38
48
|
self_link = (item['links'] || []).
|
39
49
|
map{|link| Link.new(link)}.detect{|link| link.self?}
|
40
50
|
if self_link && self_link.valid?
|
41
|
-
@items.push Resource.new(
|
51
|
+
@items.push Resource.new(uri_for(self_link.href), session).load(:body => item)
|
42
52
|
else
|
43
53
|
session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
|
44
54
|
end
|
data/lib/restfully/resource.rb
CHANGED
@@ -37,6 +37,7 @@ module Restfully
|
|
37
37
|
@executed_requests = Hash.new
|
38
38
|
@links = Hash.new
|
39
39
|
@properties = Hash.new
|
40
|
+
@status = :stale
|
40
41
|
self
|
41
42
|
end
|
42
43
|
|
@@ -65,33 +66,84 @@ module Restfully
|
|
65
66
|
# properties and links
|
66
67
|
# <tt>options</tt>:: list of options to pass to the request (see below)
|
67
68
|
# == Options
|
68
|
-
# <tt>:reload</tt>:: if set to true, a GET request will be triggered
|
69
|
-
#
|
70
|
-
# <tt>:
|
71
|
-
#
|
69
|
+
# <tt>:reload</tt>:: if set to true, a GET request will be triggered
|
70
|
+
# even if the resource has already been loaded [default=false]
|
71
|
+
# <tt>:query</tt>:: a hash of query parameters to pass along the request.
|
72
|
+
# E.g. : resource.load(:query => {:from => (Time.now-3600).to_i, :to => Time.now.to_i})
|
73
|
+
# <tt>:headers</tt>:: a hash of HTTP headers to pass along the request.
|
74
|
+
# E.g. : resource.load(:headers => {'Accept' => 'application/json'})
|
75
|
+
# <tt>:body</tt>:: if you already have the unserialized response body of this resource,
|
76
|
+
# you may pass it so that the GET request is not triggered.
|
72
77
|
def load(options = {})
|
73
78
|
options = options.symbolize_keys
|
74
|
-
force_reload = !!options.delete(:reload)
|
79
|
+
force_reload = !!options.delete(:reload) || stale?
|
75
80
|
if !force_reload && (request = executed_requests['GET']) && request['options'] == options && request['body']
|
76
81
|
self
|
77
82
|
else
|
78
83
|
reset
|
84
|
+
if options[:body]
|
85
|
+
body = options[:body]
|
86
|
+
headers = nil
|
87
|
+
else
|
88
|
+
response = session.get(uri, options)
|
89
|
+
body = response.body
|
90
|
+
headers = response.headers
|
91
|
+
end
|
79
92
|
executed_requests['GET'] = {
|
80
93
|
'options' => options,
|
81
|
-
'body' =>
|
94
|
+
'body' => body,
|
95
|
+
'headers' => headers
|
82
96
|
}
|
83
97
|
executed_requests['GET']['body'].each do |key, value|
|
84
98
|
populate_object(key, value)
|
85
99
|
end
|
100
|
+
@status = :loaded
|
86
101
|
self
|
87
102
|
end
|
88
103
|
end
|
89
104
|
|
105
|
+
# == Description
|
106
|
+
# Executes a POST request on the resource, reload it and returns self if successful.
|
107
|
+
# If the response status is different than 2xx, raises a HTTP::ClientError or HTTP::ServerError.
|
108
|
+
# <tt>payload</tt>:: the input body of the request.
|
109
|
+
# It may be a serialized string, or a ruby object
|
110
|
+
# (that will be serialized according to the given or default content-type).
|
111
|
+
# <tt>options</tt>:: list of options to pass to the request (see below)
|
112
|
+
# == Options
|
113
|
+
# <tt>:query</tt>:: a hash of query parameters to pass along the request.
|
114
|
+
# E.g. : resource.submit("body", :query => {:param1 => "value1"})
|
115
|
+
# <tt>:headers</tt>:: a hash of HTTP headers to pass along the request.
|
116
|
+
# E.g. : resource.submit("body", :headers => {:accept => 'application/json', :content_type => 'application/json'})
|
90
117
|
def submit(payload, options = {})
|
91
118
|
options = options.symbolize_keys
|
92
119
|
raise NotImplementedError, "The POST method is not allowed for this resource." unless http_methods.include?('POST')
|
93
|
-
raise ArgumentError, "You must pass a payload
|
94
|
-
|
120
|
+
raise ArgumentError, "You must pass a payload" if payload.nil?
|
121
|
+
headers = {
|
122
|
+
:content_type => (executed_requests['GET']['headers']['Content-Type'] || "application/x-www-form-urlencoded").split(/,/).sort{|a,b| a.length <=> b.length}[0],
|
123
|
+
:accept => (executed_requests['GET']['headers']['Content-Type'] || "text/plain")
|
124
|
+
}.merge(options[:headers] || {})
|
125
|
+
options = {:headers => headers}
|
126
|
+
options.merge!(:query => options[:query]) unless options[:query].nil?
|
127
|
+
response = session.post(self.uri, payload, options) # raises an exception if there is an error
|
128
|
+
stale!
|
129
|
+
if [201, 202].include?(response.status)
|
130
|
+
Resource.new(uri_for(response.headers['Location']), session).load
|
131
|
+
else
|
132
|
+
reload
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
def stale!; @status = :stale; end
|
138
|
+
def stale?; @status == :stale; end
|
139
|
+
|
140
|
+
def uri_for(path)
|
141
|
+
uri.merge(URI.parse(path.to_s))
|
142
|
+
end
|
143
|
+
|
144
|
+
def reload
|
145
|
+
current_options = executed_requests['GET']['options'] rescue {}
|
146
|
+
self.load(current_options.merge(:reload => true))
|
95
147
|
end
|
96
148
|
|
97
149
|
# == Description
|
@@ -101,11 +153,8 @@ module Restfully
|
|
101
153
|
# => ['GET', 'POST']
|
102
154
|
#
|
103
155
|
def http_methods
|
104
|
-
if executed_requests['
|
105
|
-
|
106
|
-
executed_requests['HEAD'] = {'headers' => response.headers}
|
107
|
-
end
|
108
|
-
(executed_requests['HEAD']['headers']['Allow'] || "GET").split(/,\s*/)
|
156
|
+
reload if executed_requests['GET'].nil? || executed_requests['GET']['headers'].nil?
|
157
|
+
(executed_requests['GET']['headers']['Allow'] || "GET").split(/,\s*/)
|
109
158
|
end
|
110
159
|
|
111
160
|
def respond_to?(method, *args)
|
@@ -118,6 +167,7 @@ module Restfully
|
|
118
167
|
|
119
168
|
def pretty_print(pp)
|
120
169
|
pp.text "#<#{self.class}:0x#{self.object_id.to_s(16)}"
|
170
|
+
pp.text " uid=#{self['uid'].inspect}" if self.class == Resource
|
121
171
|
pp.nest 2 do
|
122
172
|
pp.breakable
|
123
173
|
pp.text "@uri="
|
data/lib/restfully.rb
CHANGED
data/restfully.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{restfully}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.5.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Cyril Rohr"]
|
12
|
-
s.date = %q{2009-
|
12
|
+
s.date = %q{2009-12-08}
|
13
13
|
s.default_executable = %q{restfully}
|
14
14
|
s.description = %q{Experimental code for auto-generation of wrappers on top of RESTful APIs that follow HATEOAS principles and provide OPTIONS methods and/or Allow headers.}
|
15
15
|
s.email = %q{cyril.rohr@gmail.com}
|
data/spec/collection_spec.rb
CHANGED
@@ -20,79 +20,70 @@ describe Collection do
|
|
20
20
|
it "should have a :offset attribute" do
|
21
21
|
@collection["offset"].should == 0
|
22
22
|
end
|
23
|
+
|
24
|
+
it "should try to find the matching item in the collection when calling [] with a symbol" do
|
25
|
+
rennes = @collection[:rennes]
|
26
|
+
rennes.class.should == Restfully::Resource
|
27
|
+
rennes['uid'].should == 'rennes'
|
28
|
+
end
|
23
29
|
end
|
24
30
|
|
25
|
-
describe "
|
26
|
-
before
|
27
|
-
@uri =
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@logger = Logger.new(STDOUT)
|
31
|
-
end
|
32
|
-
it "should not load if already loaded and no :reload" do
|
33
|
-
collection = Collection.new(@uri, mock("session"))
|
34
|
-
options = {:headers => {'key' => 'value'}}
|
35
|
-
collection.should_receive(:executed_requests).and_return({'GET' => {'options' => options, 'body' => {"key" => "value"}}})
|
36
|
-
collection.load(options.merge(:reload => false)).should == collection
|
37
|
-
end
|
38
|
-
it "should load when :reload param is true [already loaded]" do
|
39
|
-
collection = Collection.new(@uri, session=mock("session", :logger => Logger.new(STDOUT)))
|
40
|
-
session.should_receive(:get).and_return(@response_200)
|
41
|
-
collection.load(:reload => true).should == collection
|
31
|
+
describe "populating collection" do
|
32
|
+
before do
|
33
|
+
@uri = "http://server.com/path/to/collection"
|
34
|
+
@session = mock("session", :logger => Logger.new($stderr))
|
35
|
+
@collection = Collection.new(@uri, @session)
|
42
36
|
end
|
43
|
-
it "should
|
44
|
-
|
45
|
-
|
46
|
-
collection.
|
37
|
+
it "should define links" do
|
38
|
+
Link.should_receive(:new).with({"rel" => "self", "href" => "/path/to/collection"}).and_return(link1=mock("link1"))
|
39
|
+
Link.should_receive(:new).with({"rel" => "member", "href" => "/path/to/member", "title" => "member_title"}).and_return(link2=mock("link2"))
|
40
|
+
@collection.should_receive(:define_link).with(link1)
|
41
|
+
@collection.should_receive(:define_link).with(link2)
|
42
|
+
@collection.populate_object("links", [
|
43
|
+
{"rel" => "self", "href" => "/path/to/collection"},
|
44
|
+
{"rel" => "member", "href" => "/path/to/member", "title" => "member_title"}
|
45
|
+
])
|
47
46
|
end
|
48
|
-
it "should
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
it "should create a new resource for each item" do
|
48
|
+
items = [
|
49
|
+
{
|
50
|
+
'links' => [
|
51
|
+
{'rel' => 'self', 'href' => '/grid5000/sites/rennes'},
|
52
|
+
{'rel' => 'collection', 'href' => '/grid5000/sites/rennes/versions', 'title' => 'versions'}
|
53
|
+
],
|
54
|
+
'uid' => 'rennes'
|
55
|
+
}
|
56
|
+
]
|
57
|
+
@collection.populate_object("items", items)
|
58
|
+
@collection.find{|i| i['uid'] == 'rennes'}.class.should == Restfully::Resource
|
52
59
|
end
|
60
|
+
|
53
61
|
it "should not initialize resources lacking a self link" do
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
]
|
65
|
-
}), :logger => @logger))
|
66
|
-
Resource.should_not_receive(:new)
|
67
|
-
collection.load
|
68
|
-
collection.find{|i| i['uid'] == 'rennes'}.should be_nil
|
62
|
+
items = [
|
63
|
+
{
|
64
|
+
'links' => [
|
65
|
+
{'rel' => 'collection', 'href' => '/grid5000/sites/rennes/versions', 'resolvable' => false, 'title' => 'versions'}
|
66
|
+
],
|
67
|
+
'uid' => 'rennes'
|
68
|
+
}
|
69
|
+
]
|
70
|
+
@collection.populate_object("items", items)
|
71
|
+
@collection.items.should be_empty
|
69
72
|
end
|
70
|
-
it "should
|
71
|
-
collection
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
]
|
83
|
-
}), :logger => @logger))
|
84
|
-
collection.load
|
85
|
-
collection.length.should == 1
|
86
|
-
collection.find{|i| i['uid'] == 'rennes'}.class.should == Restfully::Resource
|
73
|
+
it "should store its properties in the internal hash" do
|
74
|
+
@collection.populate_object("uid", "rennes")
|
75
|
+
@collection.populate_object("list_of_values", [1,2,"whatever", {:x => :y}])
|
76
|
+
@collection.populate_object("hash", {"a" => [1,2], "b" => "c"})
|
77
|
+
@collection.properties.should == {
|
78
|
+
"hash"=>{"a"=>[1, 2], "b"=>"c"},
|
79
|
+
"list_of_values"=>[1, 2, "whatever", {:x=>:y}],
|
80
|
+
"uid"=>"rennes"
|
81
|
+
}
|
82
|
+
@collection["uid"].should == "rennes"
|
83
|
+
@collection["hash"].should == {"a"=>[1, 2], "b"=>"c"}
|
84
|
+
@collection["list_of_values"].should == [1, 2, "whatever", {:x=>:y}]
|
87
85
|
end
|
88
|
-
it "should correctly initialize its resources [integration test]" do
|
89
|
-
collection = Collection.new(@uri, session=mock("session", :logger => Logger.new(STDOUT), :get => @response_200))
|
90
|
-
collection.load
|
91
|
-
collection.uri.should == @uri
|
92
|
-
collection.find{|i| i['uid'] == 'rennes'}["uid"].should == 'rennes'
|
93
|
-
collection.find{|i| i['uid'] == 'rennes'}["type"].should == 'site'
|
94
|
-
collection.map{|s| s['uid']}.should =~ ['rennes', 'lille', 'bordeaux', 'nancy', 'sophia', 'toulouse', 'lyon', 'grenoble', 'orsay']
|
95
|
-
end
|
96
86
|
end
|
87
|
+
|
97
88
|
|
98
89
|
end
|
data/spec/resource_spec.rb
CHANGED
@@ -5,6 +5,7 @@ include Restfully
|
|
5
5
|
describe Resource do
|
6
6
|
before do
|
7
7
|
@logger = Logger.new(STDOUT)
|
8
|
+
@uri = URI.parse("http://api.local/x/y/z")
|
8
9
|
end
|
9
10
|
|
10
11
|
describe "accessors" do
|
@@ -67,7 +68,6 @@ describe Resource do
|
|
67
68
|
}
|
68
69
|
}
|
69
70
|
@response_200 = Restfully::HTTP::Response.new(200, {'Content-Type' => 'application/json;utf-8', 'Content-Length' => @raw.length}, @raw.to_json)
|
70
|
-
@uri = URI.parse("http://api.local/x/y/z")
|
71
71
|
end
|
72
72
|
it "should not be loaded in its initial state" do
|
73
73
|
resource = Resource.new(@uri, mock('session'))
|
@@ -110,7 +110,7 @@ describe Resource do
|
|
110
110
|
{'rel' => 'collection', 'href' => '/grid5000/sites/rennes/versions', 'resolvable' => false, 'title' => 'versions'}
|
111
111
|
],
|
112
112
|
'uid' => 'rennes'
|
113
|
-
}), :logger => @logger))
|
113
|
+
}, :headers => {}), :logger => @logger))
|
114
114
|
Collection.should_receive(:new).with(@uri.merge('/grid5000/sites/rennes/versions'), session, :title => 'versions').and_return(collection=mock("restfully collection"))
|
115
115
|
resource.load
|
116
116
|
resource.links['versions'].should == collection
|
@@ -121,7 +121,7 @@ describe Resource do
|
|
121
121
|
{'rel' => 'self', 'href' => '/grid5000/sites/rennes'}
|
122
122
|
],
|
123
123
|
'uid' => 'rennes'
|
124
|
-
}), :logger => @logger))
|
124
|
+
}, :headers => {}), :logger => @logger))
|
125
125
|
resource.uri.should == @uri
|
126
126
|
resource.load
|
127
127
|
resource.uri.should == @uri
|
@@ -132,7 +132,7 @@ describe Resource do
|
|
132
132
|
{'rel' => 'member', 'href' => '/grid5000/sites/rennes/versions/123', 'title' => 'version'}
|
133
133
|
],
|
134
134
|
'uid' => 'rennes'
|
135
|
-
}), :logger => @logger))
|
135
|
+
}, :headers => {}), :logger => @logger))
|
136
136
|
Resource.should_receive(:new).with(@uri.merge('/grid5000/sites/rennes/versions/123'), session, :title => 'version').and_return(member=mock("restfully resource"))
|
137
137
|
resource.load
|
138
138
|
resource.links['version'].should == member
|
@@ -144,7 +144,7 @@ describe Resource do
|
|
144
144
|
{'rel' => 'parent', 'href' => '/grid5000'}
|
145
145
|
],
|
146
146
|
'uid' => 'rennes'
|
147
|
-
}), :logger => @logger))
|
147
|
+
}, :headers => {}), :logger => @logger))
|
148
148
|
Resource.should_receive(:new).with(@uri.merge('/grid5000'), session).and_return(parent=mock("restfully resource"))
|
149
149
|
resource.load
|
150
150
|
resource.links['parent'].should == parent
|
@@ -157,7 +157,7 @@ describe Resource do
|
|
157
157
|
{'rel' => 'collection', 'href' => '/has/no/title'}
|
158
158
|
],
|
159
159
|
'uid' => 'rennes'
|
160
|
-
}), :logger => @logger))
|
160
|
+
}, :headers => {}), :logger => @logger))
|
161
161
|
resource.load
|
162
162
|
resource.links.should be_empty
|
163
163
|
end
|
@@ -170,4 +170,62 @@ describe Resource do
|
|
170
170
|
resource.links.keys.should =~ ['versions', 'clusters', 'environments', 'status', 'parent', 'version']
|
171
171
|
end
|
172
172
|
end
|
173
|
+
|
174
|
+
|
175
|
+
describe "submitting" do
|
176
|
+
before do
|
177
|
+
@resource = Resource.new(@uri, @session = mock("session", :logger => @logger))
|
178
|
+
@resource.stub!(:http_methods).and_return(['GET', 'POST'])
|
179
|
+
@resource.stub!(:executed_requests).and_return({
|
180
|
+
'GET' => {'headers' => {'Content-Type' => 'application/vnd.fr.grid5000.api.Job+json;level=1,application/json'}, 'options' => {:query => {:q1 => 'v1'}}}
|
181
|
+
})
|
182
|
+
end
|
183
|
+
describe "setting input body and options" do
|
184
|
+
before do
|
185
|
+
@resource.stub!(:reload).and_return(@resource)
|
186
|
+
@response = mock("http response", :status => 200)
|
187
|
+
end
|
188
|
+
it "should raise a NotImplementedError if the resource does not allow POST" do
|
189
|
+
@resource.should_receive(:http_methods).and_return(['GET'])
|
190
|
+
lambda{@resource.submit("whatever")}.should raise_error(NotImplementedError, /POST method is not allowed for this resource/)
|
191
|
+
end
|
192
|
+
it "should raise an error if the input body is nil" do
|
193
|
+
lambda{@resource.submit(nil)}.should raise_error(ArgumentError, /You must pass a payload/)
|
194
|
+
end
|
195
|
+
it "should pass the body as-is if the given body is a string" do
|
196
|
+
@session.should_receive(:post).with(@resource.uri, "whatever", :headers => {
|
197
|
+
:accept => 'application/vnd.fr.grid5000.api.Job+json;level=1,application/json',
|
198
|
+
:content_type => 'application/json'}
|
199
|
+
).and_return(@response)
|
200
|
+
@resource.submit("whatever")
|
201
|
+
end
|
202
|
+
it "should also pass the body as-is if the given body is an object" do
|
203
|
+
body = {:key => 'value'}
|
204
|
+
@session.should_receive(:post).with(@resource.uri, body, :headers => {
|
205
|
+
:accept => 'application/vnd.fr.grid5000.api.Job+json;level=1,application/json',
|
206
|
+
:content_type => 'application/json'}).and_return(@response)
|
207
|
+
@resource.submit(body)
|
208
|
+
end
|
209
|
+
it "should set the Content-Type header to the specified mime-type if given" do
|
210
|
+
@session.should_receive(:post).with(@resource.uri, "whatever", :headers => {
|
211
|
+
:accept => 'application/vnd.fr.grid5000.api.Job+json;level=1,application/json',
|
212
|
+
:content_type => 'application/xml'}).and_return(@response)
|
213
|
+
@resource.submit("whatever", :headers => {:content_type => 'application/xml'})
|
214
|
+
end
|
215
|
+
end
|
216
|
+
[201, 202].each do |status|
|
217
|
+
it "should return the resource referenced in the Location header after a successful submit (status=#{status})" do
|
218
|
+
@session.should_receive(:post).and_return(response = mock("http response", :status => status, :headers => {'Location' => '/path/to/new/resource'}))
|
219
|
+
@resource.should_receive(:uri_for).with('/path/to/new/resource').and_return(new_resource_uri = mock("uri"))
|
220
|
+
Resource.should_receive(:new).with(new_resource_uri, @session).and_return(new_resource = mock("resource"))
|
221
|
+
new_resource.should_receive(:load).and_return(new_resource)
|
222
|
+
@resource.submit("whatever").should == new_resource
|
223
|
+
end
|
224
|
+
end
|
225
|
+
it "should reload the resource if the response status is 2xx (and not 201 or 202)" do
|
226
|
+
@session.should_receive(:post).and_return(response = mock("http response", :status => 200, :headers => {'Location' => '/path/to/new/resource'}))
|
227
|
+
@resource.should_receive(:reload).and_return(@resource)
|
228
|
+
@resource.submit("whatever").should == @resource
|
229
|
+
end
|
230
|
+
end
|
173
231
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restfully
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Rohr
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-12-08 00:00:00 +01:00
|
13
13
|
default_executable: restfully
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|