restfully 0.7.1.rc3 → 0.7.1.rc4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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