restfully 0.6.3 → 0.7.0.pre

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.
Files changed (73) hide show
  1. data/README.md +166 -0
  2. data/Rakefile +35 -35
  3. data/bin/restfully +68 -10
  4. data/lib/restfully.rb +8 -14
  5. data/lib/restfully/collection.rb +70 -90
  6. data/lib/restfully/error.rb +2 -0
  7. data/lib/restfully/http.rb +3 -3
  8. data/lib/restfully/http/error.rb +1 -20
  9. data/lib/restfully/http/helper.rb +49 -0
  10. data/lib/restfully/http/request.rb +60 -24
  11. data/lib/restfully/http/response.rb +55 -24
  12. data/lib/restfully/link.rb +32 -24
  13. data/lib/restfully/media_type.rb +70 -0
  14. data/lib/restfully/media_type/abstract_media_type.rb +162 -0
  15. data/lib/restfully/media_type/application_json.rb +21 -0
  16. data/lib/restfully/media_type/application_vnd_bonfire_xml.rb +177 -0
  17. data/lib/restfully/media_type/application_x_www_form_urlencoded.rb +33 -0
  18. data/lib/restfully/media_type/grid5000.rb +67 -0
  19. data/lib/restfully/media_type/wildcard.rb +27 -0
  20. data/lib/restfully/rack.rb +1 -0
  21. data/lib/restfully/rack/basic_auth.rb +26 -0
  22. data/lib/restfully/resource.rb +134 -197
  23. data/lib/restfully/session.rb +127 -70
  24. data/lib/restfully/version.rb +3 -0
  25. data/spec/fixtures/bonfire-collection-with-fragments.xml +6 -0
  26. data/spec/fixtures/bonfire-compute-existing.xml +43 -0
  27. data/spec/fixtures/bonfire-empty-collection.xml +4 -0
  28. data/spec/fixtures/bonfire-experiment-collection.xml +51 -0
  29. data/spec/fixtures/bonfire-network-collection.xml +35 -0
  30. data/spec/fixtures/bonfire-network-existing.xml +6 -0
  31. data/spec/fixtures/bonfire-root.xml +5 -0
  32. data/spec/fixtures/grid5000-rennes-jobs.json +988 -146
  33. data/spec/fixtures/grid5000-rennes.json +63 -0
  34. data/spec/restfully/collection_spec.rb +87 -0
  35. data/spec/restfully/http/helper_spec.rb +18 -0
  36. data/spec/restfully/http/request_spec.rb +97 -0
  37. data/spec/restfully/http/response_spec.rb +53 -0
  38. data/spec/restfully/link_spec.rb +80 -0
  39. data/spec/restfully/media_type/application_vnd_bonfire_xml_spec.rb +153 -0
  40. data/spec/restfully/media_type_spec.rb +117 -0
  41. data/spec/restfully/resource_spec.rb +109 -0
  42. data/spec/restfully/session_spec.rb +229 -0
  43. data/spec/spec_helper.rb +10 -9
  44. metadata +162 -83
  45. data/.document +0 -5
  46. data/CHANGELOG +0 -62
  47. data/README.rdoc +0 -146
  48. data/TODO.rdoc +0 -3
  49. data/VERSION +0 -1
  50. data/examples/grid5000.rb +0 -33
  51. data/examples/scratch.rb +0 -37
  52. data/lib/restfully/extensions.rb +0 -34
  53. data/lib/restfully/http/adapters/abstract_adapter.rb +0 -29
  54. data/lib/restfully/http/adapters/patron_adapter.rb +0 -16
  55. data/lib/restfully/http/adapters/rest_client_adapter.rb +0 -75
  56. data/lib/restfully/http/headers.rb +0 -20
  57. data/lib/restfully/parsing.rb +0 -66
  58. data/lib/restfully/special_array.rb +0 -5
  59. data/lib/restfully/special_hash.rb +0 -5
  60. data/restfully.gemspec +0 -114
  61. data/spec/collection_spec.rb +0 -120
  62. data/spec/fixtures/configuration_file.yml +0 -4
  63. data/spec/fixtures/grid5000-sites.json +0 -540
  64. data/spec/http/error_spec.rb +0 -18
  65. data/spec/http/headers_spec.rb +0 -17
  66. data/spec/http/request_spec.rb +0 -49
  67. data/spec/http/response_spec.rb +0 -19
  68. data/spec/http/rest_client_adapter_spec.rb +0 -35
  69. data/spec/link_spec.rb +0 -61
  70. data/spec/parsing_spec.rb +0 -40
  71. data/spec/resource_spec.rb +0 -320
  72. data/spec/restfully_spec.rb +0 -16
  73. data/spec/session_spec.rb +0 -171
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restfully::MediaType do
4
+
5
+ class NewMediaType < Restfully::MediaType::AbstractMediaType
6
+ set :signature, "application/whatever"
7
+ set :parser, JSON
8
+ end
9
+ class BadMediaType < Restfully::MediaType::AbstractMediaType; end
10
+
11
+ describe "class methods" do
12
+ it "should refuse to add a media type without signature" do
13
+ lambda{
14
+ Restfully::MediaType.register BadMediaType
15
+ }.should raise_error(ArgumentError, "The given MediaType (BadMediaType) has no signature")
16
+ end
17
+ it "should refuse to add a media type without parser" do
18
+ BadMediaType.set :signature, "application/whatever"
19
+ lambda{
20
+ Restfully::MediaType.register BadMediaType
21
+ }.should raise_error(ArgumentError, "The given MediaType (BadMediaType) has no parser")
22
+ end
23
+ it "should add the media_type to the catalog" do
24
+ length_before = Restfully::MediaType.catalog.length
25
+ Restfully::MediaType.register NewMediaType
26
+ Restfully::MediaType.catalog.length.should == length_before+1
27
+ end
28
+
29
+ it "should find the corresponding media_type [application/json]" do
30
+ Restfully::MediaType.register Restfully::MediaType::ApplicationJson
31
+ Restfully::MediaType.find("application/json").should == Restfully::MediaType::ApplicationJson
32
+ end
33
+
34
+ it "should find the corresponding media_type [application/vnd.grid5000+json]" do
35
+ Restfully::MediaType.register Restfully::MediaType::Grid5000
36
+ Restfully::MediaType.find('application/vnd.grid5000+json; charset=utf-8').should == Restfully::MediaType::Grid5000
37
+ end
38
+
39
+ it "should find the corresponding media_type from the less generic to the most generic [application/json]" do
40
+ Restfully::MediaType.catalog.clear
41
+ Restfully::MediaType.register Restfully::MediaType::Wildcard
42
+ Restfully::MediaType.register Restfully::MediaType::ApplicationJson
43
+ Restfully::MediaType.register Restfully::MediaType::Grid5000
44
+ Restfully::MediaType.find('application/json').should == Restfully::MediaType::ApplicationJson
45
+ end
46
+
47
+ it "should fall back to wildcard" do
48
+ Restfully::MediaType.find("whatever/whatever").should == Restfully::MediaType::Wildcard
49
+ end
50
+ end
51
+
52
+ describe Restfully::MediaType::Wildcard do
53
+ ["text/plain", "text/*", "*/*"].each do |type|
54
+ it "should match everything [#{type}]" do
55
+ Restfully::MediaType::Wildcard.supports?(type).should == "*/*"
56
+ end
57
+ end
58
+ it "should not match invalid content types" do
59
+ Restfully::MediaType::Wildcard.supports?("whatever").should be_nil
60
+ end
61
+ it "should correctly initialize with a payload" do
62
+ media_type = Restfully::MediaType::Wildcard.new(
63
+ "some string",
64
+ mock(Restfully::Session)
65
+ )
66
+ media_type.property("some").should == "some"
67
+ media_type.links.should == []
68
+ end
69
+ end
70
+
71
+ describe Restfully::MediaType::Grid5000 do
72
+ before do
73
+ @session =
74
+ @media_type = Restfully::MediaType::Grid5000.new(
75
+ StringIO.new(fixture('grid5000-rennes.json')),
76
+ @session
77
+ )
78
+ end
79
+ it "should correctly extract the links" do
80
+ @media_type.links.map(&:rel).should == ["self", "parent", "versions", "clusters", "environments", "jobs", "deployments", "metrics", "status"]
81
+ @media_type.links.map(&:title).should == ["self", "parent", "versions", "clusters", "environments", "jobs", "deployments", "metrics", "status"]
82
+ end
83
+
84
+ it "should correctly find a property" do
85
+ @media_type.property("uid").should == "rennes"
86
+ end
87
+
88
+ it "should tell if it is a collection" do
89
+ @media_type.collection?.should be_false
90
+ end
91
+ end
92
+
93
+ describe Restfully::MediaType::ApplicationXWwwFormUrlencoded do
94
+ before do
95
+ @unserialized = {"k1" => 'value', "k2" => ['a','b','c']}
96
+ @serialized = "k1=value&k2[]=a&k2[]=b&k2[]=c"
97
+ end
98
+ it "should have the correct default type" do
99
+ Restfully::MediaType::ApplicationXWwwFormUrlencoded.
100
+ default_type.should == "application/x-www-form-urlencoded"
101
+ end
102
+ it "should be found" do
103
+ Restfully::MediaType.register Restfully::MediaType::ApplicationXWwwFormUrlencoded
104
+ Restfully::MediaType.find('application/x-www-form-urlencoded').
105
+ should == Restfully::MediaType::ApplicationXWwwFormUrlencoded
106
+ end
107
+ it "should correctly encode a hash" do
108
+ Restfully::MediaType::ApplicationXWwwFormUrlencoded.
109
+ serialize(@unserialized).should == @serialized
110
+ end
111
+ it "should correctly decode a string" do
112
+ Restfully::MediaType::ApplicationXWwwFormUrlencoded.
113
+ unserialize(@serialized).should == @unserialized
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restfully::Resource do
4
+ before do
5
+ @session = Restfully::Session.new(
6
+ :uri => "https://api.grid5000.fr"
7
+ )
8
+ @request = Restfully::HTTP::Request.new(
9
+ @session, :get, "/grid5000/sites/rennes",
10
+ :head => {'Accept' => 'application/vnd.grid5000+json'}
11
+ )
12
+ @response = Restfully::HTTP::Response.new(
13
+ @session, 200, {
14
+ 'Content-Type' => 'application/vnd.grid5000+json; charset=utf-8',
15
+ 'Allow' => 'GET'
16
+ }, fixture('grid5000-rennes.json')
17
+ )
18
+ Restfully::MediaType.register Restfully::MediaType::Grid5000
19
+ end
20
+
21
+ it "should correctly initialize a resource" do
22
+ resource = Restfully::Resource.new(@session, @response, @request)
23
+ resource.uri.to_s.should == "https://api.grid5000.fr/grid5000/sites/rennes"
24
+ resource.should be_a(Restfully::Resource)
25
+ resource.load.should == resource
26
+ resource.media_type.should be_a(Restfully::MediaType::Grid5000)
27
+ resource['uid'].should == 'rennes'
28
+ %w{clusters jobs deployments status self parent}.each do |rel|
29
+ resource.should respond_to(rel.to_sym)
30
+ end
31
+ end
32
+
33
+ describe "loaded" do
34
+ before do
35
+ @resource = Restfully::Resource.new(@session, @response, @request)
36
+ @resource.load
37
+ end
38
+
39
+ it "should load the requested association" do
40
+ @session.should_receive(:get).once.with(
41
+ URI.parse("/grid5000/sites/rennes/clusters"),
42
+ :head=>{"Accept"=>"application/vnd.grid5000+json"}
43
+ ).and_return(association=mock(Restfully::Resource))
44
+ association.should_receive(:load)
45
+ @resource.clusters
46
+ end
47
+
48
+ it "should not allow to submit if POST not allowed on the resource" do
49
+ lambda{
50
+ @resource.submit
51
+ }.should raise_error(Restfully::MethodNotAllowed)
52
+ end
53
+ it "should send a POST request if POST is allowed [payload argument]" do
54
+ @resource.should_receive(:allow?).with(:post).and_return(true)
55
+ @session.should_receive(:post).with(
56
+ @resource.uri,
57
+ 'some payload',
58
+ :head => {'Content-Type' => 'text/plain'},
59
+ :query => {:k1 => 'v1'}
60
+ )
61
+ @resource.submit(
62
+ 'some payload',
63
+ :headers => {'Content-Type' => 'text/plain'},
64
+ :query => {:k1 => 'v1'}
65
+ )
66
+ end
67
+ it "should send a POST request if POST is allowed [payload as hash]" do
68
+ @resource.should_receive(:allow?).with(:post).and_return(true)
69
+ @session.should_receive(:post).with(
70
+ @resource.uri,
71
+ {:key => 'value'},
72
+ :head => {'Content-Type' => 'text/plain'},
73
+ :query => {:k1 => 'v1'}
74
+ )
75
+ @resource.submit(
76
+ :key => 'value',
77
+ :headers => {'Content-Type' => 'text/plain'},
78
+ :query => {:k1 => 'v1'}
79
+ )
80
+ end
81
+
82
+ it "should reload the resource" do
83
+ @resource.should_receive(:load).
84
+ with(:head => {'Cache-Control' => 'no-cache'}).
85
+ and_return(@resource)
86
+ @resource.reload.should == @resource
87
+ end
88
+
89
+ it "should reload the resource even after having reloaded it once before" do
90
+ @session.should_receive(:execute).twice.with(@request).
91
+ and_return(@response)
92
+ @resource.reload
93
+ @resource.reload
94
+ end
95
+
96
+ it "should raise an error if it cannot reload the resource" do
97
+ @session.should_receive(:execute).with(@request).
98
+ and_return(res=mock(Restfully::HTTP::Response))
99
+ @session.should_receive(:process).with(res, @request).
100
+ and_return(false)
101
+ lambda{
102
+ @resource.reload
103
+ }.should raise_error(Restfully::Error, "Cannot reload the resource")
104
+ end
105
+ end
106
+
107
+
108
+
109
+ end
@@ -0,0 +1,229 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restfully::Session do
4
+ before do
5
+ RestClient.components.clear
6
+ @logger = Logger.new(STDERR)
7
+ @uri = "https://api.grid5000.fr"
8
+ @config = {
9
+ :uri => @uri,
10
+ :logger => @logger
11
+ }
12
+ end
13
+
14
+ it "should initialize a session with the correct properties" do
15
+ session = Restfully::Session.new(@config.merge("key" => "value"))
16
+ session.logger.should == @logger
17
+ session.uri.should == Addressable::URI.parse(@uri)
18
+ session.config.should == {:key => "value"}
19
+ end
20
+
21
+ it "should raise an error if no URI given" do
22
+ lambda{
23
+ Restfully::Session.new(@config.merge(:uri => ""))
24
+ }.should raise_error(ArgumentError)
25
+ end
26
+
27
+ it "should fetch the root path [no URI path]" do
28
+ session = Restfully::Session.new(@config)
29
+ session.should_receive(:get).with("").
30
+ and_return(res = mock(Restfully::Resource))
31
+ res.should_receive(:load).and_return(res)
32
+ session.root.should == res
33
+ end
34
+
35
+ it "should fetch the root path [URI path present]" do
36
+ session = Restfully::Session.new(
37
+ @config.merge(:uri => "https://api.grid5000.fr/resource/path")
38
+ )
39
+ session.should_receive(:get).with("/resource/path").
40
+ and_return(res = mock(Restfully::Resource))
41
+ res.should_receive(:load).and_return(res)
42
+ session.root.should == res
43
+ end
44
+
45
+ it "should add or replace additional headers to the default set" do
46
+ session = Restfully::Session.new(
47
+ @config.merge(:default_headers => {
48
+ 'Accept' => 'application/xml',
49
+ 'Cache-Control' => 'no-cache'
50
+ })
51
+ )
52
+ session.default_headers.should == {
53
+ 'Accept' => 'application/xml',
54
+ 'Cache-Control' => 'no-cache',
55
+ 'Accept-Encoding' => 'gzip, deflate'
56
+ }
57
+ end
58
+
59
+ describe "middleware" do
60
+ it "should only have Rack::Cache enabled by default" do
61
+ session = Restfully::Session.new(@config)
62
+ session.middleware.length.should == 1
63
+ session.middleware.should == [
64
+ Rack::Cache
65
+ ]
66
+ end
67
+
68
+ it "should use Restfully::Rack::BasicAuth if basic authentication is used" do
69
+ session = Restfully::Session.new(@config.merge(
70
+ :username => "crohr", :password => "p4ssw0rd"
71
+ ))
72
+ session.middleware.length.should == 2
73
+ session.middleware.should == [
74
+ Rack::Cache,
75
+ Restfully::Rack::BasicAuth
76
+ ]
77
+ end
78
+ end
79
+
80
+ describe "transmitting requests" do
81
+ before do
82
+ @session = Restfully::Session.new(@config)
83
+ @path = "/path"
84
+ @default_headers = @session.default_headers
85
+ end
86
+
87
+ it "should make a get request" do
88
+ stub_request(:get, @uri+@path+"?k1=v1&k2=v2").with(
89
+ :headers => @default_headers.merge({
90
+ 'Accept' => '*/*',
91
+ 'X-Header' => 'value'
92
+ })
93
+ ).to_return(
94
+ :status => 200,
95
+ :body => 'body',
96
+ :headers => {'X'=>'Y'}
97
+ )
98
+
99
+ Restfully::HTTP::Response.should_receive(:new).with(
100
+ @session, 200, hash_including({'X'=>'Y'}), ['body']
101
+ ).and_return(
102
+ @response = mock(Restfully::HTTP::Response)
103
+ )
104
+
105
+ @session.should_receive(:process).with(
106
+ @response,
107
+ instance_of(Restfully::HTTP::Request)
108
+ )
109
+
110
+ @session.transmit :get, @path, {
111
+ :query => {:k1 => "v1", :k2 => "v2"},
112
+ :headers => {'X-Header' => 'value'}
113
+ }
114
+ end
115
+
116
+ it "should make an authenticated get request" do
117
+ stub_request(:get, "https://crohr:p4ssw0rd@api.grid5000.fr"+@path+"?k1=v1&k2=v2").with(
118
+ :headers => @default_headers.merge({
119
+ 'Accept' => '*/*',
120
+ 'X-Header' => 'value'
121
+ })
122
+ )
123
+ @session.should_receive(:process)
124
+ @session.authenticate :username => 'crohr', :password => 'p4ssw0rd'
125
+ @session.transmit :get, @path, {
126
+ :query => {:k1 => "v1", :k2 => "v2"},
127
+ :headers => {'X-Header' => 'value'}
128
+ }
129
+ end
130
+ end
131
+
132
+ describe "processing responses" do
133
+ before do
134
+ @session = Restfully::Session.new(@config)
135
+ @request = mock(
136
+ Restfully::HTTP::Request,
137
+ :method => :get,
138
+ :uri => @uri,
139
+ :head => mock("head"),
140
+ :update! => nil
141
+ )
142
+ @response = Restfully::HTTP::Response.new(
143
+ @session,
144
+ 200,
145
+ {'X' => 'Y'},
146
+ 'body'
147
+ )
148
+ end
149
+
150
+ it "should return true if status=204" do
151
+ @response.stub!(:code).and_return(204)
152
+ @session.process(@response, @request).should be_true
153
+ end
154
+
155
+ it "should raise a Restfully::HTTP::ClientError if status in 400..499" do
156
+ @response.stub!(:code).and_return(400)
157
+ lambda{
158
+ @session.process(@response, @request)
159
+ }.should raise_error(Restfully::HTTP::ClientError)
160
+ end
161
+
162
+ it "should raise a Restfully::HTTP::ServerError if status in 500..599" do
163
+ @response.stub!(:code).and_return(500)
164
+ lambda{
165
+ @session.process(@response, @request)
166
+ }.should raise_error(Restfully::HTTP::ServerError)
167
+ end
168
+
169
+ it "should raise an error if the status is not supported" do
170
+ @response.stub!(:code).and_return(50)
171
+ lambda{
172
+ @session.process(@response, @request)
173
+ }.should raise_error(Restfully::Error)
174
+ end
175
+
176
+ [201, 202].each do |status|
177
+ it "should fetch the resource specified in the Location header if status = #{status}" do
178
+ @response.stub!(:code).and_return(status)
179
+ @response.head['Location'] = @uri+"/path"
180
+
181
+ @session.should_receive(:get).
182
+ with(@uri+"/path", :head => @request.head).
183
+ and_return(resource=mock("resource"))
184
+ @session.process(@response, @request).
185
+ should == resource
186
+ end
187
+ end
188
+
189
+ it "should return a Restfully::Resource if successful" do
190
+ Restfully::MediaType.register Restfully::MediaType::ApplicationJson
191
+ body = {
192
+ :key1 => "value1",
193
+ :key2 => ["value2", "value3"]
194
+ }
195
+ @response = Restfully::HTTP::Response.new(
196
+ @session, 200,
197
+ {'Content-Type' => 'application/json'},
198
+ JSON.dump(body)
199
+ )
200
+
201
+ resource = @session.process(
202
+ @response,
203
+ @request
204
+ )
205
+
206
+ resource.should be_a(Restfully::Resource)
207
+ resource.uri.should == @request.uri
208
+ resource['key1'].should == body[:key1]
209
+ resource['key2'].should == body[:key2]
210
+ end
211
+
212
+ it "should raise an error if the response content-type is not supported" do
213
+ @response = Restfully::HTTP::Response.new(
214
+ @session, 200,
215
+ {'Content-Type' => ''},
216
+ 'body'
217
+ )
218
+
219
+ lambda{
220
+ @session.process(@response,@request)
221
+ }.should raise_error(
222
+ Restfully::Error,
223
+ "Cannot find a media-type for content-type=\"\""
224
+ )
225
+ end
226
+ end
227
+
228
+ end
229
+