restfully 0.7.1.rc3 → 0.7.1.rc4

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.
@@ -49,6 +49,9 @@ BANNER
49
49
  @options["logger"] = logger
50
50
  end
51
51
  opts.on("-v", "--verbose", "Run verbosely") do |v|
52
+ @options["logger"].level = Logger::INFO
53
+ end
54
+ opts.on("--debug", "Run in debug mode") do |v|
52
55
  @options["logger"].level = Logger::DEBUG
53
56
  end
54
57
  opts.on_tail("-h", "--help", "Show this message") do
@@ -14,13 +14,13 @@ module Restfully
14
14
  super(property)
15
15
  end
16
16
  end
17
-
17
+
18
18
  def find_by_uid(symbol)
19
19
  found = find{ |i| i.media_type.represents?(symbol) }
20
20
  found.expand unless found.nil?
21
21
  found
22
22
  end
23
-
23
+
24
24
  def find_by_index(index)
25
25
  index = index+length if index < 0
26
26
  each_with_index{|item, i|
@@ -69,27 +69,16 @@ module Restfully
69
69
  def empty?
70
70
  total == 0
71
71
  end
72
-
73
- # Expand the items that
72
+
73
+ # Expand the items that
74
74
  def expand
75
75
  each {|i| i.expand}
76
76
  self
77
77
  end
78
-
78
+
79
79
  def inspect
80
80
  map{|item| item}.inspect
81
81
  end
82
- # def (key)
83
- # p Addressable::URI.parse("./"+key.to_s)
84
- # p self.uri
85
- # uri_to_find = Addressable::URI.join(self.uri, "./"+key.to_s)
86
- # p uri_to_find
87
- # find{|resource|
88
- # resource.uri == uri_to_find
89
- # }
90
- # end
91
-
92
-
93
82
  end
94
83
 
95
84
  end
@@ -2,27 +2,33 @@
2
2
 
3
3
  module Restfully
4
4
  module HTTP
5
-
5
+
6
6
  class Request
7
7
  include Helper
8
-
9
- attr_reader :method, :uri, :head, :body
10
-
11
- def initialize(session, method, path, options)
8
+
9
+ attr_reader :session, :method, :uri, :head, :body, :attempts
10
+ attr_accessor :retry_on_error, :wait_before_retry
11
+
12
+ def initialize(session, method, path, options = {})
12
13
  @session = session
13
-
14
+ @attempts = 0
15
+
14
16
  request = options.symbolize_keys
17
+
18
+ @retry_on_error = request[:retry_on_error] || session.config[:retry_on_error]
19
+ @wait_before_retry = request[:wait_before_retry] || session.config[:wait_before_retry]
20
+
15
21
  request[:method] = method
16
22
 
17
23
  request[:head] = sanitize_head(@session.default_headers).merge(
18
24
  build_head(request)
19
25
  )
20
-
26
+
21
27
  request[:uri] = @session.uri_to(path)
22
28
  if request[:query]
23
29
  request[:uri].query_values = sanitize_query(request[:query])
24
30
  end
25
-
31
+
26
32
  request[:body] = if [:post, :put].include?(request[:method])
27
33
  build_body(request)
28
34
  end
@@ -30,9 +36,10 @@ module Restfully
30
36
  @method, @uri, @head, @body = request.values_at(
31
37
  :method, :uri, :head, :body
32
38
  )
39
+
33
40
  end
34
-
35
- # Updates the request header and query parameters
41
+
42
+ # Updates the request header and query parameters.
36
43
  # Returns nil if no changes were made, otherwise self.
37
44
  def update!(options = {})
38
45
  objects_that_may_be_updated = [@uri, @head]
@@ -48,33 +55,61 @@ module Restfully
48
55
  self
49
56
  end
50
57
  end
51
-
58
+
52
59
  def inspect
53
60
  "#{method.to_s.upcase} #{uri.to_s}, head=#{head.inspect}, body=#{body.inspect}"
54
61
  end
55
-
62
+
56
63
  def no_cache?
57
64
  head['Cache-Control'] && head['Cache-Control'].include?('no-cache')
58
65
  end
59
-
66
+
60
67
  def no_cache!
61
68
  @forced_cache = true
62
69
  head['Cache-Control'] = 'no-cache'
63
70
  end
64
-
71
+
65
72
  def forced_cache?
66
73
  !!@forced_cache
67
74
  end
68
-
75
+
76
+ def execute!
77
+ session.logger.debug self.inspect
78
+ resource = RestClient::Resource.new(
79
+ uri.to_s,
80
+ :headers => head
81
+ )
82
+
83
+ begin
84
+ reqcode, reqhead, reqbody = resource.send(method, body || {})
85
+ response = Response.new(session, reqcode, reqhead, reqbody)
86
+ session.logger.debug response.inspect
87
+ response
88
+ rescue Errno::ECONNREFUSED => e
89
+ retry! || raise(e)
90
+ end
91
+ end
92
+
93
+ def retry!
94
+ if @attempts < @retry_on_error
95
+ @attempts+=1
96
+ session.logger.info "Encountered connection or server error. Retrying in #{@wait_before_retry}s... [#{@attempts}/#{@retry_on_error}]"
97
+ sleep @wait_before_retry if @wait_before_retry > 0
98
+ execute!
99
+ else
100
+ false
101
+ end
102
+ end
103
+
69
104
  def remove_no_cache!
70
105
  @forced_cache = false
71
106
  if head['Cache-Control']
72
- head['Cache-Control'] = head['Cache-Control'].split(/\s+,\s+/).reject{|v|
107
+ head['Cache-Control'] = head['Cache-Control'].split(/\s+,\s+/).reject{|v|
73
108
  v =~ /no-cache/i
74
109
  }.join(",")
75
110
  end
76
111
  end
77
-
112
+
78
113
  protected
79
114
  def build_head(options = {})
80
115
  sanitize_head(
@@ -94,7 +129,7 @@ module Restfully
94
129
  nil
95
130
  end
96
131
  end
97
-
132
+
98
133
  end
99
134
  end
100
135
  end
@@ -50,7 +50,7 @@ module Restfully
50
50
  end
51
51
 
52
52
  def allow?(http_method)
53
- http_method = http_method.to_sym
53
+ http_method = http_method.downcase.to_sym
54
54
  return true if http_method == :get
55
55
  (
56
56
  media_type.respond_to?(:allow?) &&
@@ -25,7 +25,10 @@ module Restfully
25
25
  # resource["uid"]
26
26
  # => "rennes"
27
27
  def [](key)
28
- expand.media_type.property(key)
28
+ unless collection?
29
+ expand
30
+ end
31
+ media_type.property(key)
29
32
  end
30
33
 
31
34
  def uri
@@ -58,7 +61,7 @@ module Restfully
58
61
  def load(options = {})
59
62
  # Send a GET request only if given a different set of options
60
63
  if @request.update!(options) || @request.no_cache?
61
- @response = session.execute(@request)
64
+ @response = @request.execute!
62
65
  @request.remove_no_cache! if @request.forced_cache?
63
66
  if session.process(@response, @request)
64
67
  @associations.clear
@@ -121,8 +124,7 @@ module Restfully
121
124
  end
122
125
 
123
126
  def allow?(method)
124
- reload
125
- response.allow?(method)
127
+ response.allow?(method) || reload.response.allow?(method)
126
128
  end
127
129
 
128
130
  def inspect
@@ -16,6 +16,8 @@ module Restfully
16
16
  def initialize(options = {})
17
17
  @config = options.symbolize_keys
18
18
  @logger = @config.delete(:logger) || Logger.new(STDERR)
19
+ @config[:retry_on_error] ||= 5
20
+ @config[:wait_before_retry] ||= 5
19
21
 
20
22
  @uri = @config.delete(:uri)
21
23
  if @uri.nil? || @uri.empty?
@@ -100,29 +102,14 @@ module Restfully
100
102
  transmit :delete, path, options
101
103
  end
102
104
 
103
- # Build and send the corresponding HTTP request, then process the response
105
+ # Build and execute the corresponding HTTP request,
106
+ # then process the response.
104
107
  def transmit(method, path, options)
105
108
  request = HTTP::Request.new(self, method, path, options)
106
-
107
- response = execute(request)
108
-
109
+ response = request.execute!
109
110
  process(response, request)
110
111
  end
111
112
 
112
- def execute(request)
113
- resource = RestClient::Resource.new(
114
- request.uri.to_s,
115
- :headers => request.head
116
- )
117
-
118
- logger.debug request.inspect
119
- code, head, body = resource.send(request.method, request.body || {})
120
-
121
- response = Restfully::HTTP::Response.new(self, code, head, body)
122
- logger.debug response.inspect
123
- response
124
- end
125
-
126
113
  # Process a Restfully::HTTP::Response.
127
114
  def process(response, request)
128
115
  case code=response.code
@@ -135,14 +122,15 @@ module Restfully
135
122
  when 204
136
123
  true
137
124
  when 400..499
138
- msg = "Encountered error #{code} on #{request.method.upcase} #{request.uri}"
139
- msg += " --- #{response.body[0..200]}" unless response.body.empty?
140
- raise HTTP::ClientError, msg
141
- when 500..599
142
- # when 503, sleep 5, request.retry
143
- msg = "Encountered error #{code} on #{request.method.upcase} #{request.uri}"
144
- msg += " --- #{response.body[0..200]}" unless response.body.empty?
145
- raise HTTP::ServerError, msg
125
+ raise HTTP::ClientError, error_message(request, response)
126
+ when 502..504
127
+ if res = request.retry!
128
+ process(res, request)
129
+ else
130
+ raise(HTTP::ServerError, error_message(request, response))
131
+ end
132
+ when 500, 501
133
+ raise HTTP::ServerError, error_message(request, response)
146
134
  else
147
135
  raise Error, "Restfully does not handle code #{code.inspect}."
148
136
  end
@@ -150,7 +138,12 @@ module Restfully
150
138
 
151
139
  protected
152
140
  def setup_cache
153
- enable ::Rack::Cache, :verbose => (logger.level < Logger::INFO)
141
+ enable ::Rack::Cache, :verbose => (logger.level <= Logger::INFO)
142
+ end
143
+
144
+ def error_message(request, response)
145
+ msg = "Encountered error #{response.code} on #{request.method.upcase} #{request.uri}"
146
+ msg += " --- #{response.body[0..200]}" unless response.body.empty?
154
147
  end
155
148
 
156
149
  end
@@ -1,3 +1,3 @@
1
1
  module Restfully
2
- VERSION = "0.7.1.rc3"
2
+ VERSION = "0.7.1.rc4"
3
3
  end
@@ -7,6 +7,11 @@ describe Restfully::HTTP::Request do
7
7
  :default_headers => {
8
8
  'Accept' => '*/*; application/xml',
9
9
  :accept_encoding => "gzip, deflate"
10
+ },
11
+ :logger => Logger.new(STDERR),
12
+ :config => {
13
+ :retry_on_error => 5,
14
+ :wait_before_retry => 5
10
15
  }
11
16
  )
12
17
 
@@ -102,5 +107,33 @@ describe Restfully::HTTP::Request do
102
107
  request.body.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<network xmlns=\"http://api.bonfire-project.eu/doc/schemas/occi\">\n <name>whatever</name>\n</network>\n"
103
108
  end
104
109
  end
110
+
111
+ describe "execute" do
112
+ before do
113
+ @request = Restfully::HTTP::Request.new(
114
+ @session,
115
+ :get,
116
+ "/path"
117
+ )
118
+ end
119
+ it "should build the RestClient::Resource and build a response" do
120
+ RestClient::Resource.should_receive(:new).with(
121
+ @request.uri.to_s,
122
+ :headers => @request.head
123
+ ).and_return(
124
+ resource = mock(RestClient::Resource)
125
+ )
126
+ resource.should_receive(:get).and_return([
127
+ 200,
128
+ {'Content-Type' => 'text/plain'},
129
+ ["hello"]
130
+ ])
131
+ response = @request.execute!
132
+ response.should be_a(Restfully::HTTP::Response)
133
+ response.code.should == 200
134
+ response.body.should == "hello"
135
+ response.head.should == {'Content-Type' => 'text/plain'}
136
+ end
137
+ end
105
138
 
106
139
  end
@@ -36,6 +36,9 @@ describe Restfully::HTTP::Response do
36
36
 
37
37
  response.allow?(:get).should be_true
38
38
  response.allow?(:post).should be_true
39
+ response.allow?("POST").should be_true
40
+ response.allow?("GET").should be_true
41
+ response.allow?("PUT").should be_false
39
42
  end
40
43
 
41
44
  it "should raise an error if it cannot find a corresponding media-type" do
@@ -45,14 +45,23 @@ describe Restfully::Resource do
45
45
  @resource.clusters
46
46
  end
47
47
  {:update => "PUT", :submit => "POST", :delete => "DELETE"}.each do |method, http_method|
48
- it "should reload the resource first, to get the Allowed HTTP methods when calling #{method.to_sym}" do
49
- @resource.should_receive(:reload).once.and_return(@resource)
48
+ it "should get the Allowed HTTP methods when calling #{method.to_sym}" do
50
49
  @response.should_receive(:allow?).with(http_method).and_return(true)
51
50
  @session.should_receive(http_method.downcase.to_sym).
52
51
  and_return(mock(Restfully::HTTP::Response))
53
52
  @resource.send(method.to_sym)
54
53
  end
55
54
  end
55
+ {:update => "PUT", :submit => "POST", :delete => "DELETE"}.each do |method, http_method|
56
+ it "should reload itself to get the Allowed HTTP methods when calling #{method.to_sym}" do
57
+ @response.should_receive(:allow?).with(http_method).and_return(false)
58
+ @response.should_receive(:allow?).with(http_method).and_return(true)
59
+ @resource.should_receive(:reload).once.and_return(@resource)
60
+ @session.should_receive(http_method.downcase.to_sym).
61
+ and_return(mock(Restfully::HTTP::Response))
62
+ @resource.send(method.to_sym)
63
+ end
64
+ end
56
65
  it "should not allow to submit if POST not allowed on the resource" do
57
66
  @resource.should_receive(:reload).and_return(@resource)
58
67
  lambda{
@@ -125,14 +134,14 @@ describe Restfully::Resource do
125
134
  end
126
135
 
127
136
  it "should reload the resource even after having reloaded it once before" do
128
- @session.should_receive(:execute).twice.with(@request).
137
+ @request.should_receive(:execute!).twice.
129
138
  and_return(@response)
130
139
  @resource.reload
131
140
  @resource.reload
132
141
  end
133
142
 
134
143
  it "should raise an error if it cannot reload the resource" do
135
- @session.should_receive(:execute).with(@request).
144
+ @request.should_receive(:execute!).
136
145
  and_return(res=mock(Restfully::HTTP::Response))
137
146
  @session.should_receive(:process).with(res, @request).
138
147
  and_return(false)
@@ -10,20 +10,20 @@ describe Restfully::Session do
10
10
  :logger => @logger
11
11
  }
12
12
  end
13
-
13
+
14
14
  it "should initialize a session with the correct properties" do
15
15
  session = Restfully::Session.new(@config.merge("key" => "value"))
16
16
  session.logger.should == @logger
17
17
  session.uri.should == Addressable::URI.parse(@uri)
18
- session.config.should == {:key => "value"}
18
+ session.config.should == {:wait_before_retry=>5, :key=>"value", :retry_on_error=>5}
19
19
  end
20
-
20
+
21
21
  it "should raise an error if no URI given" do
22
22
  lambda{
23
23
  Restfully::Session.new(@config.merge(:uri => ""))
24
24
  }.should raise_error(ArgumentError)
25
25
  end
26
-
26
+
27
27
  it "should fetch the root path [no URI path]" do
28
28
  session = Restfully::Session.new(@config)
29
29
  session.should_receive(:get).with("").
@@ -31,7 +31,7 @@ describe Restfully::Session do
31
31
  res.should_receive(:load).and_return(res)
32
32
  session.root.should == res
33
33
  end
34
-
34
+
35
35
  it "should fetch the root path [URI path present]" do
36
36
  session = Restfully::Session.new(
37
37
  @config.merge(:uri => "https://api.grid5000.fr/resource/path")
@@ -41,21 +41,21 @@ describe Restfully::Session do
41
41
  res.should_receive(:load).and_return(res)
42
42
  session.root.should == res
43
43
  end
44
-
44
+
45
45
  it "should add or replace additional headers to the default set" do
46
46
  session = Restfully::Session.new(
47
47
  @config.merge(:default_headers => {
48
- 'Accept' => 'application/xml',
48
+ 'Accept' => 'application/xml',
49
49
  'Cache-Control' => 'no-cache'
50
50
  })
51
51
  )
52
52
  session.default_headers.should == {
53
- 'Accept' => 'application/xml',
53
+ 'Accept' => 'application/xml',
54
54
  'Cache-Control' => 'no-cache',
55
55
  'Accept-Encoding' => 'gzip, deflate'
56
56
  }
57
57
  end
58
-
58
+
59
59
  describe "middleware" do
60
60
  it "should only have Rack::Cache enabled by default" do
61
61
  session = Restfully::Session.new(@config)
@@ -64,7 +64,7 @@ describe Restfully::Session do
64
64
  Rack::Cache
65
65
  ]
66
66
  end
67
-
67
+
68
68
  it "should use Restfully::Rack::BasicAuth if basic authentication is used" do
69
69
  session = Restfully::Session.new(@config.merge(
70
70
  :username => "crohr", :password => "p4ssw0rd"
@@ -76,15 +76,15 @@ describe Restfully::Session do
76
76
  ]
77
77
  end
78
78
  end
79
-
79
+
80
80
  describe "transmitting requests" do
81
81
  before do
82
82
  @session = Restfully::Session.new(@config)
83
83
  @path = "/path"
84
84
  @default_headers = @session.default_headers
85
85
  end
86
-
87
- it "should make a get request" do
86
+
87
+ it "should make a get request" do
88
88
  stub_request(:get, @uri+@path+"?k1=v1&k2=v2").with(
89
89
  :headers => @default_headers.merge({
90
90
  'Accept' => '*/*',
@@ -106,13 +106,13 @@ describe Restfully::Session do
106
106
  @response,
107
107
  instance_of(Restfully::HTTP::Request)
108
108
  )
109
-
109
+
110
110
  @session.transmit :get, @path, {
111
111
  :query => {:k1 => "v1", :k2 => "v2"},
112
112
  :headers => {'X-Header' => 'value'}
113
113
  }
114
114
  end
115
-
115
+
116
116
  it "should make an authenticated get request" do
117
117
  stub_request(:get, "https://crohr:p4ssw0rd@api.grid5000.fr"+@path+"?k1=v1&k2=v2").with(
118
118
  :headers => @default_headers.merge({
@@ -127,57 +127,77 @@ describe Restfully::Session do
127
127
  :headers => {'X-Header' => 'value'}
128
128
  }
129
129
  end
130
+
131
+ it "should retry for at most :max_attempts_on_connection_error if connection to the server failed" do
132
+
133
+ end
130
134
  end
131
-
135
+
132
136
  describe "processing responses" do
133
137
  before do
134
138
  @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
139
+ @request = Restfully::HTTP::Request.new(
140
+ @session,
141
+ :get,
142
+ @uri
141
143
  )
144
+ @request.stub!(:head).and_return({})
145
+ @request.stub!(:update!).and_return(nil)
146
+
142
147
  @response = Restfully::HTTP::Response.new(
143
148
  @session,
144
149
  200,
145
- {'X' => 'Y'},
150
+ {'X' => 'Y', 'Content-Type' => 'text/plain'},
146
151
  'body'
147
152
  )
148
153
  end
149
-
154
+
150
155
  it "should return true if status=204" do
151
156
  @response.stub!(:code).and_return(204)
152
157
  @session.process(@response, @request).should be_true
153
158
  end
154
-
159
+
155
160
  it "should raise a Restfully::HTTP::ClientError if status in 400..499" do
156
161
  @response.stub!(:code).and_return(400)
157
162
  lambda{
158
163
  @session.process(@response, @request)
159
164
  }.should raise_error(Restfully::HTTP::ClientError)
160
165
  end
161
-
166
+
162
167
  it "should raise a Restfully::HTTP::ServerError if status in 500..599" do
163
168
  @response.stub!(:code).and_return(500)
164
169
  lambda{
165
170
  @session.process(@response, @request)
166
171
  }.should raise_error(Restfully::HTTP::ServerError)
167
172
  end
168
-
173
+ it "should retry if the server returns one of [502,503,504], and request.retry! returns a response" do
174
+ @request.should_receive(:retry!).once.
175
+ and_return(@response)
176
+ @response.should_receive(:code).ordered.and_return(503)
177
+ @response.should_receive(:code).ordered.and_return(200)
178
+ @session.process(@response, @request)
179
+ end
180
+ it "should not retry if the server returns one of [502,503,504], but request.retry! returns false" do
181
+ @request.should_receive(:retry!).once.
182
+ and_return(false)
183
+ @response.stub!(:code).and_return(503)
184
+ lambda{
185
+ @session.process(@response, @request)
186
+ }.should raise_error(Restfully::HTTP::ServerError, /503/)
187
+ end
188
+
169
189
  it "should raise an error if the status is not supported" do
170
190
  @response.stub!(:code).and_return(50)
171
191
  lambda{
172
192
  @session.process(@response, @request)
173
193
  }.should raise_error(Restfully::Error)
174
194
  end
175
-
195
+
176
196
  [201, 202].each do |status|
177
197
  it "should fetch the resource specified in the Location header if status = #{status}" do
178
198
  @response.stub!(:code).and_return(status)
179
199
  @response.head['Location'] = @uri+"/path"
180
-
200
+
181
201
  @session.should_receive(:get).
182
202
  with(@uri+"/path", :head => @request.head).
183
203
  and_return(resource=mock("resource"))
@@ -185,7 +205,7 @@ describe Restfully::Session do
185
205
  should == resource
186
206
  end
187
207
  end
188
-
208
+
189
209
  it "should return a Restfully::Resource if successful" do
190
210
  Restfully::MediaType.register Restfully::MediaType::ApplicationJson
191
211
  body = {
@@ -197,33 +217,33 @@ describe Restfully::Session do
197
217
  {'Content-Type' => 'application/json'},
198
218
  JSON.dump(body)
199
219
  )
200
-
220
+
201
221
  resource = @session.process(
202
222
  @response,
203
223
  @request
204
224
  )
205
-
225
+
206
226
  resource.should be_a(Restfully::Resource)
207
227
  resource.uri.should == @request.uri
208
228
  resource['key1'].should == body[:key1]
209
229
  resource['key2'].should == body[:key2]
210
230
  end
211
-
231
+
212
232
  it "should raise an error if the response content-type is not supported" do
213
233
  @response = Restfully::HTTP::Response.new(
214
234
  @session, 200,
215
235
  {'Content-Type' => ''},
216
236
  'body'
217
237
  )
218
-
219
- lambda{
220
- @session.process(@response,@request)
238
+
239
+ lambda{
240
+ @session.process(@response,@request)
221
241
  }.should raise_error(
222
- Restfully::Error,
242
+ Restfully::Error,
223
243
  "Cannot find a media-type for content-type=\"\""
224
244
  )
225
245
  end
226
246
  end
227
-
247
+
228
248
  end
229
249
 
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restfully
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15424107
4
+ hash: 15424101
5
5
  prerelease: 6
6
6
  segments:
7
7
  - 0
8
8
  - 7
9
9
  - 1
10
10
  - rc
11
- - 3
12
- version: 0.7.1.rc3
11
+ - 4
12
+ version: 0.7.1.rc4
13
13
  platform: ruby
14
14
  authors:
15
15
  - Cyril Rohr