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 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 suing the command-line tool;
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). For it to work, the API must follow certain conventions (to be explained later).
4
-
5
- Alpha work.
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
- To enter the command-line options, you may prefer to use a configuration file to avoid re-entering the options every time you use the client:
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.4.1
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
- Restfully::Session.new(:base_uri => 'http://api.local/sid/grid5000', :logger => logger) do |grid, session|
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|
@@ -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(uri.merge(self_link.href), session).load(:body => item)
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
@@ -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 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.
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' => options[:body] || session.get(uri, options).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 string" unless payload.kind_of?(String)
94
- session.post(payload, options)
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['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*/)
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
@@ -14,7 +14,7 @@ require 'restfully/collection'
14
14
 
15
15
 
16
16
  module Restfully
17
- VERSION = "0.4.1"
17
+ VERSION = "0.5.0"
18
18
  class << self
19
19
  attr_accessor :adapter
20
20
  end
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.4.1"
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-11-16}
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}
@@ -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 "loading" do
26
- before(:all) do
27
- @uri = URI.parse('http://api.local/x/y/z')
28
- @raw = fixture("grid5000-sites.json")
29
- @response_200 = Restfully::HTTP::Response.new(200, {'Content-Type' => 'application/json;charset=utf-8', 'Content-Length' => @raw.length}, @raw)
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 load when force_reload is true [not loaded]" do
44
- collection = Collection.new(@uri, session=mock("session", :logger => Logger.new(STDOUT)))
45
- session.should_receive(:get).and_return(@response_200)
46
- collection.load(:reload => true).should == 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 force reload when query parameters are given" do
49
- collection = Collection.new(@uri, session=mock("session", :logger => Logger.new(STDOUT)))
50
- session.should_receive(:get).and_return(@response_200)
51
- collection.load(:query => {:q1 => 'v1'}).should == collection
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
- collection = Collection.new(@uri, session = mock("session", :get => mock("restfully response", :body => {
55
- "total" => 1,
56
- "offset" => 0,
57
- "items" => [
58
- {
59
- 'links' => [
60
- {'rel' => 'collection', 'href' => '/grid5000/sites/rennes/versions', 'resolvable' => false, 'title' => 'versions'}
61
- ],
62
- 'uid' => 'rennes'
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 initialize resources having a self link" do
71
- collection = Collection.new(@uri, session = mock("session", :get => mock("restfully response", :body => {
72
- "total" => 1,
73
- "offset" => 0,
74
- "items" => [
75
- {
76
- 'links' => [
77
- {'rel' => 'self', 'href' => '/grid5000/sites/rennes'},
78
- {'rel' => 'collection', 'href' => '/grid5000/sites/rennes/versions', 'title' => 'versions'}
79
- ],
80
- 'uid' => 'rennes'
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
@@ -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.1
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-11-16 00:00:00 +01:00
12
+ date: 2009-12-08 00:00:00 +01:00
13
13
  default_executable: restfully
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency