rest-core 3.4.1 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85f12b68843aae98a2ceaa7d85346e0c99627f21
4
- data.tar.gz: d8ba6d00b676443be83d939f7b676ad75f96ec0a
3
+ metadata.gz: b4cd61e7344e03258e5ef9482bf9270e29115ee2
4
+ data.tar.gz: fbc1f00dcb47c15daec112ea4cf6a7397331bc74
5
5
  SHA512:
6
- metadata.gz: 56f4c5e7e9b54d700947e99598fa889d1bea1d2a9e07a954fcf795109bf5935263b26db50e881ad71458671213d60961b367738b400d1abe5046ade10c05d6b5
7
- data.tar.gz: bb5712b4ac86364fcbe05ca8ad938ef6635764e73695a02a982dfb3b6028f0752539063f80ecad5ff630f75c2b185e399e60780eeeeade4938e45066b7743aa1
6
+ metadata.gz: ffc882cb23b1e1b9017d910c4fb34c77258f4c4afd93eceb6cfb96ef1068fb8afd356c330fdb36ea5bfc8345d985cacfedb72eb374422368d8b573c85220b93c
7
+ data.tar.gz: aeb92a57d45711993ed90d561b9f4d39e780d5715c3d97acf5310d8089445a071ec0593958ecb7d024c147b3cb8e326153bbc64885e2b0fa89de10ff3c67b736
data/CHANGES.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # CHANGES
2
2
 
3
+ ## rest-core 3.5.0 -- 2014-12-09
4
+
5
+ ### Incompatible changes
6
+
7
+ * `RC::Builder` would now only deep copy arrays and hashes.
8
+ * `RC::ErrorHandler`'s only responsibility is now creating the exception.
9
+ Raising the exceptions or passing it to the callback is now handled by
10
+ `RC::Client` instead. (Thanks Andrew Clunis, #6)
11
+ * Since exceptions are raised by `RC::Client` now, `RC::Timeout` would not
12
+ raise any exception, but just hand over to `RC::Client`.
13
+ * Support for Ruby version < 1.9.2 is dropped.
14
+
15
+ ### Bugs fixed
16
+
17
+ * Reverted #10 because it caused the other encoding issue. (#12)
18
+ * `RC::Client#wait` and `RC::Client.wait` would now properly wait for
19
+ `RC::FollowRedirect`
20
+ * `RC::Event::CacheHit` is properly logged again.
21
+
22
+ ### Enhancements
23
+
24
+ * Introduced `RC::Client#error_callback` which would get called for each
25
+ exceptions raised. This is useful for monitoring and logging errors.
26
+
27
+ * Introduced `RC::Retry` which could retry the request upon certain errors.
28
+ Specify `max_retries` for maximum times for retrying, and `retry_exceptions`
29
+ for which exceptions should be trying.
30
+ Default is `[IOError, SystemCallError]`
31
+
32
+ * Eliminated a few warnings when `-w` is used.
33
+
3
34
  ## rest-core 3.4.1 -- 2014-11-29
4
35
 
5
36
  ### Bugs fixed
data/README.md CHANGED
@@ -531,6 +531,7 @@ simply ignore `:expires_in`.
531
531
  [RC::Oauth2Header]: lib/rest-core/middleware/oauth2_header.rb
532
532
  [RC::Oauth2Query]: lib/rest-core/middleware/oauth2_query.rb
533
533
  [RC::SmashResponse]: lib/rest-core/middleware/smash_response.rb
534
+ [RC::Retry]: lib/rest-core/middleware/retry.rb
534
535
  [RC::Timeout]: lib/rest-core/middleware/timeout.rb
535
536
  [moneta]: https://github.com/minad/moneta#expiration
536
537
 
@@ -68,6 +68,7 @@ module RestCore
68
68
  autoload :Oauth1Header , 'rest-core/middleware/oauth1_header'
69
69
  autoload :Oauth2Header , 'rest-core/middleware/oauth2_header'
70
70
  autoload :Oauth2Query , 'rest-core/middleware/oauth2_query'
71
+ autoload :Retry , 'rest-core/middleware/retry'
71
72
  autoload :Timeout , 'rest-core/middleware/timeout'
72
73
 
73
74
  # engines
@@ -88,7 +89,7 @@ module RestCore
88
89
  c = const.const_get(n)
89
90
  rescue LoadError, NameError => e
90
91
  warn "RestCore: WARN: #{e} for #{const}\n" \
91
- " from #{e.backtrace.grep(/top.+required/u).first}"
92
+ " from #{e.backtrace.grep(/top.+required/).first}"
92
93
  end
93
94
  eagerload(c, loaded) if c.respond_to?(:constants) && !loaded[n]
94
95
  }
@@ -76,8 +76,7 @@ class RestCore::Builder
76
76
  case obj
77
77
  when Array; obj.map{ |o| partial_deep_copy(o) }
78
78
  when Hash ; obj.inject({}){ |r, (k, v)| r[k] = partial_deep_copy(v); r }
79
- when Numeric, Symbol, TrueClass, FalseClass, NilClass; obj
80
- else begin obj.dup; rescue TypeError; obj; end
79
+ else ; obj
81
80
  end
82
81
  end
83
82
 
@@ -41,12 +41,14 @@ module RestCore::Client
41
41
  end
42
42
 
43
43
  attr_reader :app, :dry, :promises
44
+ attr_accessor :error_callback
44
45
  def initialize o={}
45
46
  @app ||= self.class.builder.to_app # lighten! would reinitialize anyway
46
47
  @dry ||= self.class.builder.to_app(Dry)
47
48
  @promises = [] # don't record any promises in lighten!
48
49
  @mutex = nil # for locking promises, lazily initialized
49
50
  # for serialization
51
+ @error_callback = nil
50
52
  o.each{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
51
53
  end
52
54
 
@@ -57,7 +59,7 @@ module RestCore::Client
57
59
  def inspect
58
60
  fields = if size > 0
59
61
  ' ' + attributes.map{ |k, v|
60
- "#{k}=#{v.inspect.sub(/(?<=.{12}).{4,}/u, '...')}"
62
+ "#{k}=#{v.inspect.sub(/(?<=.{12}).{4,}/, '...')}"
61
63
  }.join(', ')
62
64
  else
63
65
  ''
@@ -161,9 +163,20 @@ module RestCore::Client
161
163
  end
162
164
 
163
165
  def request_full env, app=app, &k
164
- response = app.call(build_env({ASYNC => !!k}.merge(env)),
165
- &(k || RC.id))
166
+ response = app.call(build_env({ASYNC => !!k}.merge(env))) do |res|
167
+ (k || RC.id).call(request_complete(res))
168
+ end
169
+
170
+ give_promise(response)
171
+
172
+ if block_given?
173
+ self
174
+ else
175
+ response
176
+ end
177
+ end
166
178
 
179
+ def give_promise response
167
180
  # under ASYNC callback, response might not be a response hash
168
181
  # in that case (maybe in a user created engine), Client#wait
169
182
  # won't work because we have no way to track the promise.
@@ -173,11 +186,7 @@ module RestCore::Client
173
186
  self.class.give_promise(weak_promise, promises, mutex)
174
187
  end
175
188
 
176
- if block_given?
177
- self
178
- else
179
- response
180
- end
189
+ response
181
190
  end
182
191
 
183
192
  def build_env env={}
@@ -200,6 +209,24 @@ module RestCore::Client
200
209
 
201
210
 
202
211
  private
212
+ def request_complete res
213
+ if err = res[FAIL].find{ |f| f.kind_of?(Exception) }
214
+ RC::Promise.set_backtrace(err) unless err.backtrace
215
+ error_callback.call(err) if error_callback
216
+ if res[ASYNC]
217
+ if res[HIJACK]
218
+ res.merge(RESPONSE_SOCKET => err)
219
+ else
220
+ res.merge(RESPONSE_BODY => err)
221
+ end
222
+ else
223
+ raise err
224
+ end
225
+ else
226
+ res
227
+ end
228
+ end
229
+
203
230
  def mutex
204
231
  @mutex ||= Mutex.new
205
232
  end
@@ -1,15 +1,14 @@
1
1
 
2
2
  module RestCore
3
3
  Universal = Builder.client do
4
- use Timeout , 0
5
-
6
4
  use DefaultSite , nil
7
5
  use DefaultHeaders, {}
8
6
  use DefaultQuery , {}
9
7
  use DefaultPayload, {}
10
8
  use JsonRequest , false
11
9
  use AuthBasic , nil, nil
12
- use CommonLogger , method(:puts)
10
+ use Retry , 0, Retry::DefaultRetryExceptions
11
+ use Timeout , 0
13
12
  use ErrorHandler , nil
14
13
  use ErrorDetectorHttp
15
14
 
@@ -17,10 +16,9 @@ module RestCore
17
16
  use ClashResponse , false
18
17
  use JsonResponse , false
19
18
  use QueryResponse , false
20
-
19
+ use FollowRedirect, 10
20
+ use CommonLogger , method(:puts)
21
21
  use Cache , {}, 600 # default :expires_in 600 but the default
22
22
  # cache {} didn't support it
23
-
24
- use FollowRedirect, 10
25
23
  end
26
24
  end
@@ -4,9 +4,14 @@ module RestCore
4
4
  RestCore.const_defined?(:EventStruct)
5
5
 
6
6
  class Event < EventStruct
7
- # self.class.name[/(?<=::)\w+$/] if RUBY_VERSION >= '1.9.2'
8
- def name; self.class.name[/::(\w+)$/u, 1] ; end
9
- def to_s; "spent #{duration} #{name} #{message}"; end
7
+ def name; self.class.name[/(?<=::)\w+$/]; end
8
+ def to_s
9
+ if duration
10
+ "spent #{duration} #{name} #{message}"
11
+ else
12
+ "#{name} #{message}"
13
+ end
14
+ end
10
15
  end
11
16
  class Event::MultiDone < Event; end
12
17
  class Event::Requested < Event; end
@@ -14,4 +19,5 @@ module RestCore
14
19
  class Event::CacheCleared < Event; end
15
20
  class Event::Failed < Event; end
16
21
  class Event::WithHeader < Event; end
22
+ class Event::Retrying < Event; end
17
23
  end
@@ -67,7 +67,7 @@ class RestCore::EventSource < Struct.new(:client, :path, :query, :opts,
67
67
  begin
68
68
  @onerror.call(error, sock) if @onerror
69
69
  onreconnect(error, sock)
70
- rescue Exception => e
70
+ rescue Exception
71
71
  mutex.synchronize do
72
72
  @closed = true
73
73
  condv.signal # so we can't be reconnecting, need to try to unblock
@@ -57,6 +57,13 @@ module RestCore::Middleware
57
57
  app
58
58
  end
59
59
  end
60
+ def error_callback res, err
61
+ res[CLIENT].error_callback.call(err) if
62
+ res[CLIENT] && res[CLIENT].error_callback
63
+ end
64
+ def give_promise res
65
+ res[CLIENT].give_promise(res) if res[CLIENT]
66
+ end
60
67
 
61
68
  module_function
62
69
  def request_uri env
@@ -64,7 +71,7 @@ module RestCore::Middleware
64
71
  if (query = (env[REQUEST_QUERY] || {}).select{ |k, v| v }).empty?
65
72
  env[REQUEST_PATH].to_s
66
73
  else
67
- q = if env[REQUEST_PATH] =~ /\?/u then '&' else '?' end
74
+ q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
68
75
  "#{env[REQUEST_PATH]}#{q}#{percent_encode(query)}"
69
76
  end
70
77
  end
@@ -81,7 +88,7 @@ module RestCore::Middleware
81
88
  end
82
89
  public :percent_encode
83
90
 
84
- UNRESERVED = /[^a-zA-Z0-9\-\.\_\~]+/u
91
+ UNRESERVED = /[^a-zA-Z0-9\-\.\_\~]+/
85
92
  def escape string
86
93
  string.gsub(UNRESERVED) do |s|
87
94
  "%#{s.unpack('H2' * s.bytesize).join('%')}".upcase
@@ -48,8 +48,9 @@ class RestCore::Cache
48
48
  uri = request_uri(env)
49
49
  start_time = Time.now
50
50
  return unless data = cache(env)[cache_key(env)]
51
- log(env, Event::CacheHit.new(Time.now - start_time, uri)).
52
- merge(data_extract(data, env.merge(REQUEST_URI => uri), k))
51
+ res = log(env.merge(REQUEST_URI => uri),
52
+ Event::CacheHit.new(Time.now - start_time, uri))
53
+ data_extract(data, res, k)
53
54
  end
54
55
 
55
56
  private
@@ -97,12 +98,12 @@ class RestCore::Cache
97
98
  "#{ res[RESPONSE_BODY]}"
98
99
  end
99
100
 
100
- def data_extract data, env, k
101
+ def data_extract data, res, k
101
102
  _, status, headers, body =
102
- data.match(/\A(\d+)\n((?:[^\n]+\n)*)\n\n(.*)\Z/um).to_a
103
+ data.match(/\A(\d+)\n((?:[^\n]+\n)*)\n\n(.*)\Z/m).to_a
103
104
 
104
- Promise.claim(env, k, body,status.to_i,
105
- Hash[(headers||'').scan(/([^:]+): ([^\n]+)\n/u)]).future_response
105
+ Promise.claim(res, k, body, status.to_i,
106
+ Hash[(headers||'').scan(/([^:]+): ([^\n]+)\n/)]).future_response
106
107
  end
107
108
 
108
109
  def cache_for? env
@@ -7,38 +7,12 @@ class RestCore::ErrorHandler
7
7
 
8
8
  def call env
9
9
  app.call(env){ |res|
10
- yield(if (res[FAIL] || []).empty? # no errors at all
10
+ h = error_handler(res)
11
+ f = res[FAIL] || []
12
+ yield(if f.empty? || f.find{ |ff| ff.kind_of?(Exception) } || !h
11
13
  res
12
14
  else
13
- # if there's an exception, hand it over
14
- if err = res[FAIL].find{ |e| e.kind_of?(Exception) }
15
- process(res, err)
16
-
17
- elsif h = error_handler(res)
18
- # if the user provides an exception, hand it over
19
- if (err = h.call(res)).kind_of?(Exception)
20
- process(res, err)
21
-
22
- else # otherwise we report all of them
23
- res.merge(FAIL => [res[FAIL], err].flatten.compact)
24
-
25
- end
26
- else # no exceptions at all, then do nothing
27
- res
28
- end
15
+ fail(res, h.call(res))
29
16
  end)}
30
17
  end
31
-
32
- def process res, err
33
- RC::Promise.set_backtrace(err)
34
- if res[ASYNC]
35
- if res[HIJACK]
36
- res.merge(RESPONSE_SOCKET => err)
37
- else
38
- res.merge(RESPONSE_BODY => err)
39
- end
40
- else
41
- raise err
42
- end
43
- end
44
18
  end
@@ -6,19 +6,15 @@ class RestCore::FollowRedirect
6
6
  include RestCore::Middleware
7
7
 
8
8
  def call env, &k
9
- e = env.merge('follow_redirect.max_redirects' =>
10
- env['follow_redirect.max_redirects'] ||
11
- max_redirects(env))
12
-
13
- if e[DRY]
14
- app.call(e, &k)
9
+ if env[DRY]
10
+ app.call(env, &k)
15
11
  else
16
- app.call(e){ |res| process(res, k) }
12
+ app.call(env){ |res| process(res, k) }
17
13
  end
18
14
  end
19
15
 
20
16
  def process res, k
21
- return k.call(res) if res['follow_redirect.max_redirects'] <= 0
17
+ return k.call(res) if max_redirects(res) <= 0
22
18
  return k.call(res) if ![301,302,303,307].include?(res[RESPONSE_STATUS])
23
19
  return k.call(res) if [301,302 ,307].include?(res[RESPONSE_STATUS]) &&
24
20
  ![:get, :head ].include?(res[REQUEST_METHOD])
@@ -30,10 +26,10 @@ class RestCore::FollowRedirect
30
26
  res[REQUEST_METHOD]
31
27
  end
32
28
 
33
- call(res.merge(REQUEST_METHOD => meth ,
34
- REQUEST_PATH => location,
35
- REQUEST_QUERY => {} ,
36
- 'follow_redirect.max_redirects' =>
37
- res['follow_redirect.max_redirects'] - 1), &k)
29
+ give_promise(call(res.merge(
30
+ REQUEST_METHOD => meth ,
31
+ REQUEST_PATH => location,
32
+ REQUEST_QUERY => {} ,
33
+ 'max_redirects' => max_redirects(res) - 1), &k))
38
34
  end
39
35
  end
@@ -30,7 +30,7 @@ class RestCore::JsonResponse
30
30
  def process response
31
31
  # StackExchange returns the problematic BOM! in UTF-8, so we need to
32
32
  # strip it or it would break JSON parsers (i.e. yajl-ruby and json)
33
- body = response[RESPONSE_BODY].to_s.sub(/\A\xEF\xBB\xBF/u, '')
33
+ body = response[RESPONSE_BODY].to_s.sub(/\A\xEF\xBB\xBF/, '')
34
34
  response.merge(RESPONSE_BODY => Json.decode("[#{body}]").first)
35
35
  # [this].first is not needed for yajl-ruby
36
36
  rescue Json.const_get(:ParseError) => error
@@ -0,0 +1,33 @@
1
+
2
+ require 'rest-core/middleware'
3
+
4
+ class RestCore::Retry
5
+ def self.members; [:max_retries, :retry_exceptions]; end
6
+ include RestCore::Middleware
7
+
8
+ DefaultRetryExceptions = [IOError, SystemCallError]
9
+
10
+ def call env, &k
11
+ if env[DRY]
12
+ app.call(env, &k)
13
+ else
14
+ app.call(env){ |res| process(res, k) }
15
+ end
16
+ end
17
+
18
+ def process res, k
19
+ times = max_retries(res)
20
+ return k.call(res) if times <= 0
21
+ errors = retry_exceptions(res) || DefaultRetryExceptions
22
+
23
+ if idx = res[FAIL].index{ |f| errors.find{ |e| f.kind_of?(e) } }
24
+ err = res[FAIL].delete_at(idx)
25
+ error_callback(res, err)
26
+ env = res.merge('max_retries' => times - 1)
27
+ give_promise(call(log(
28
+ env, Event::Retrying.new(nil, "(#{times}) for: #{err.inspect}")), &k))
29
+ else
30
+ k.call(res)
31
+ end
32
+ end
33
+ end
@@ -1,4 +1,6 @@
1
1
 
2
+ require 'timeout'
3
+
2
4
  require 'rest-core/middleware'
3
5
  require 'rest-core/timer'
4
6
 
@@ -8,21 +10,12 @@ class RestCore::Timeout
8
10
 
9
11
  def call env, &k
10
12
  return app.call(env, &k) if env[DRY] || timeout(env) == 0
11
- monitor(env){ |e|
12
- app.call(e){ |r|
13
- if r[ASYNC] ||
14
- !(exp = (r[FAIL]||[]).find{ |f| f.kind_of?(::Timeout::Error) })
15
- # we do nothing special for callback and rest-client
16
- k.call(r)
17
- else
18
- # it would go to this branch only under response future
19
- raise exp
20
- end}}
13
+ process(env, &k)
21
14
  end
22
15
 
23
- def monitor env
16
+ def process env, &k
24
17
  timer = Timer.new(timeout(env), timeout_error)
25
- yield(env.merge(TIMER => timer))
18
+ app.call(env.merge(TIMER => timer), &k)
26
19
  rescue Exception
27
20
  timer.cancel
28
21
  raise
@@ -159,15 +159,15 @@ class RestCore::Promise
159
159
  never_raise_yield do
160
160
  env[TIMER].cancel if env[TIMER]
161
161
  self.class.set_backtrace(e)
162
- # TODO: add error_log_method
163
- warn "RestCore: ERROR: #{e}\n from #{e.backtrace.inspect}"
164
162
  end
165
163
 
166
- begin
167
- rejecting(e) unless done? # not done: i/o error; done: callback error
168
- rescue Exception => e
169
- never_raise_yield do
170
- warn "RestCore: ERROR: #{e}\n from #{e.backtrace.inspect}"
164
+ if done?
165
+ callback_error(e)
166
+ else
167
+ begin
168
+ rejecting(e) # i/o error
169
+ rescue Exception => e
170
+ callback_error(e)
171
171
  end
172
172
  end
173
173
  end
@@ -195,6 +195,16 @@ class RestCore::Promise
195
195
  self.called = true
196
196
  end
197
197
 
198
+ def callback_error e
199
+ never_raise_yield do
200
+ if env[CLIENT].error_callback
201
+ env[CLIENT].error_callback.call(e)
202
+ else
203
+ warn "RestCore: ERROR: #{e}\n from #{e.backtrace.inspect}"
204
+ end
205
+ end
206
+ end
207
+
198
208
  def cancel_task
199
209
  mutex.synchronize do
200
210
  next if done?
@@ -11,7 +11,7 @@ require 'yaml'
11
11
  WebMock.disable_net_connect!(:allow_localhost => true)
12
12
  Pork::Executor.__send__(:include, Muack::API, WebMock::API)
13
13
 
14
- module Kernel
14
+ class Pork::Executor
15
15
  def with_img
16
16
  f = Tempfile.new(['img', '.jpg'])
17
17
  n = File.basename(f.path)
@@ -10,7 +10,7 @@ class RestCore::Timer
10
10
  attr_accessor :interval
11
11
 
12
12
  def group
13
- @group ||= @mutex.synchronize{ @group || group_new }
13
+ @group ||= @mutex.synchronize{ @group ||= group_new }
14
14
  end
15
15
 
16
16
  private
@@ -4,12 +4,12 @@ module RestCore::ParseLink
4
4
  module_function
5
5
  # http://tools.ietf.org/html/rfc5988
6
6
  parname = '"?([^"]+)"?'
7
- LINKPARAM = /#{parname}=#{parname}/u
7
+ LINKPARAM = /#{parname}=#{parname}/
8
8
  def parse_link link
9
9
  link.split(',').inject({}) do |r, value|
10
10
  uri, *pairs = value.split(';')
11
11
  params = Hash[pairs.map{ |p| p.strip.match(LINKPARAM)[1..2] }]
12
- r[params['rel']] = params.merge('uri' => uri[/<([^>]+)>/u, 1])
12
+ r[params['rel']] = params.merge('uri' => uri[/<([^>]+)>/, 1])
13
13
  r
14
14
  end
15
15
  end
@@ -12,7 +12,7 @@ module RestCore::ParseQuery
12
12
  def parse_query(qs, d = nil)
13
13
  params = {}
14
14
 
15
- (qs || '').split(d ? /[#{d}] */un : /[&;] */un).each do |p|
15
+ (qs || '').split(d ? /[#{d}] */n : /[&;] */n).each do |p|
16
16
  k, v = p.split('=', 2).map { |x| URI.decode_www_form_component(x) }
17
17
  if cur = params[k]
18
18
  if cur.class == Array
@@ -1,4 +1,4 @@
1
1
 
2
2
  module RestCore
3
- VERSION = '3.4.1'
3
+ VERSION = '3.5.0'
4
4
  end
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: rest-core 3.4.1 ruby lib
2
+ # stub: rest-core 3.5.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "rest-core"
6
- s.version = "3.4.1"
6
+ s.version = "3.5.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib"]
10
10
  s.authors = ["Lin Jen-Shin (godfat)"]
11
- s.date = "2014-11-29"
11
+ s.date = "2014-12-09"
12
12
  s.description = "Modular Ruby clients interface for REST APIs.\n\nThere has been an explosion in the number of REST APIs available today.\nTo address the need for a way to access these APIs easily and elegantly,\nwe have developed rest-core, which consists of composable middleware\nthat allows you to build a REST client for any REST API. Or in the case of\ncommon APIs such as Facebook, Github, and Twitter, you can simply use the\ndedicated clients provided by [rest-more][].\n\n[rest-more]: https://github.com/godfat/rest-more"
13
13
  s.email = ["godfat (XD) godfat.org"]
14
14
  s.files = [
@@ -57,6 +57,7 @@ Gem::Specification.new do |s|
57
57
  "lib/rest-core/middleware/oauth2_header.rb",
58
58
  "lib/rest-core/middleware/oauth2_query.rb",
59
59
  "lib/rest-core/middleware/query_response.rb",
60
+ "lib/rest-core/middleware/retry.rb",
60
61
  "lib/rest-core/middleware/smash_response.rb",
61
62
  "lib/rest-core/middleware/timeout.rb",
62
63
  "lib/rest-core/promise.rb",
@@ -103,6 +104,7 @@ Gem::Specification.new do |s|
103
104
  "test/test_payload.rb",
104
105
  "test/test_promise.rb",
105
106
  "test/test_query_response.rb",
107
+ "test/test_retry.rb",
106
108
  "test/test_simple.rb",
107
109
  "test/test_smash.rb",
108
110
  "test/test_smash_response.rb",
@@ -111,7 +113,7 @@ Gem::Specification.new do |s|
111
113
  "test/test_universal.rb"]
112
114
  s.homepage = "https://github.com/godfat/rest-core"
113
115
  s.licenses = ["Apache License 2.0"]
114
- s.rubygems_version = "2.4.4"
116
+ s.rubygems_version = "2.4.5"
115
117
  s.summary = "Modular Ruby clients interface for REST APIs."
116
118
  s.test_files = [
117
119
  "test/test_auth_basic.rb",
@@ -141,6 +143,7 @@ Gem::Specification.new do |s|
141
143
  "test/test_payload.rb",
142
144
  "test/test_promise.rb",
143
145
  "test/test_query_response.rb",
146
+ "test/test_retry.rb",
144
147
  "test/test_simple.rb",
145
148
  "test/test_smash.rb",
146
149
  "test/test_smash_response.rb",
@@ -7,7 +7,7 @@ describe RC::Simple do
7
7
  Muack.verify
8
8
  end
9
9
 
10
- url = 'http://localhost/'
10
+ url = 'http://example.com/'
11
11
 
12
12
  would 'do simple request' do
13
13
  c = RC::Simple.new
@@ -80,7 +80,7 @@ describe RC::Simple do
80
80
  end
81
81
 
82
82
  would 'cleanup promises' do
83
- stub_request(:get, url)
83
+ stub_request(:get, url).to_return(:body => 'nnf')
84
84
  client = RC::Builder.client
85
85
  5.times{ client.new.get(url) }
86
86
  Thread.pass
@@ -164,4 +164,14 @@ describe RC::Simple do
164
164
  client.wait
165
165
  client.should.nil? # to make sure the inner most block did run
166
166
  end
167
+
168
+ would 'call error_callback' do
169
+ error = nil
170
+ error_callback = lambda{ |e| error = e }
171
+ should.raise(Errno::ECONNREFUSED) do
172
+ RC::Simple.new(:error_callback => error_callback).
173
+ get('http://localhost/').tap{}
174
+ end
175
+ error.should.kind_of?(Errno::ECONNREFUSED)
176
+ end
167
177
  end
@@ -12,7 +12,7 @@ describe RC::ErrorHandler do
12
12
  describe 'there is an exception' do
13
13
  would 'raise an error with future' do
14
14
  lambda{
15
- client.new.get('/', {}, RC::FAIL => [exp.new('fail')])
15
+ client.new.get('/', {}, RC::FAIL => [exp.new('fail')])
16
16
  }.should.raise(exp)
17
17
  end
18
18
 
@@ -56,4 +56,14 @@ describe RC::ErrorHandler do
56
56
  end
57
57
  client.wait
58
58
  end
59
+
60
+ would 'not call error_handler if there is already an exception' do
61
+ called = false
62
+ RC::Builder.client do
63
+ use RC::ErrorHandler, lambda{ |_| called = true }
64
+ end.new.get('http://localhost') do |error|
65
+ error.should.kind_of?(SystemCallError)
66
+ end.wait
67
+ called.should.eq false
68
+ end
59
69
  end
@@ -133,21 +133,13 @@ SSE
133
133
  m.should.empty?
134
134
  end
135
135
 
136
- def mock_warning
137
- mock(any_instance_of(RC::Promise)).warn(is_a(String)) do |msg|
138
- msg.should.include?(Errno::ECONNREFUSED.new.message)
139
- end
140
- end
141
-
142
136
  would 'not deadlock without ErrorHandler' do
143
- mock_warning
144
137
  c = RC::Simple.new.event_source('http://localhost:1')
145
- c.onerror{ |e| e.should.eq nil }
138
+ c.onerror{ |e| e.should.kind_of?(Errno::ECONNREFUSED) }
146
139
  c.start.wait
147
140
  end
148
141
 
149
142
  would 'not deadlock with ErrorHandler' do
150
- mock_warning
151
143
  c = RC::Universal.new(:log_method => false).
152
144
  event_source('http://localhost:1')
153
145
  c.onerror{ |e| e.should.kind_of?(Errno::ECONNREFUSED) }
@@ -156,7 +148,7 @@ SSE
156
148
 
157
149
  would 'not deadlock if errors in onmessage' do
158
150
  err = nil
159
- es, m, t = server.call
151
+ es, _, _ = server.call
160
152
  es.onmessage do |event, data|
161
153
  raise err = "error"
162
154
  end.start.wait
@@ -7,6 +7,7 @@ describe RC::FollowRedirect do
7
7
  def call env
8
8
  yield(env.merge(RC::RESPONSE_STATUS => status,
9
9
  RC::RESPONSE_HEADERS => {'LOCATION' => 'location'}))
10
+ env
10
11
  end
11
12
  end.new
12
13
  app = RC::FollowRedirect.new(dry, 1)
@@ -39,11 +39,9 @@ describe RC::HttpClient do
39
39
  end
40
40
 
41
41
  would 'not kill the thread if error was coming from the task' do
42
- mock(any_instance_of(RC::Promise)).warn(is_a(String)) do |msg|
43
- msg.should.include?('boom')
44
- end
45
42
  mock(HTTPClient).new{ raise 'boom' }.with_any_args
46
- c.request(RC::RESPONSE_KEY => RC::FAIL).first.message.should.eq 'boom'
43
+ c.request(RC::RESPONSE_KEY => RC::FAIL,
44
+ RC::ASYNC => true).first.message.should.eq 'boom'
47
45
  Muack.verify
48
46
  end
49
47
  end
@@ -45,6 +45,27 @@ describe RC::Promise do
45
45
  @promise.send(:headers).should.eq('K' => 'V')
46
46
  end
47
47
 
48
+ would 'warn on callback error' do
49
+ mock(any_instance_of(RC::Promise)).warn(is_a(String)) do |msg|
50
+ msg.should.eq 'boom'
51
+ end
52
+
53
+ @client.new.get('http://localhost/') do |err|
54
+ err.should.kind_of?(Errno::ECONNREFUSED)
55
+ raise 'boom'
56
+ end.wait
57
+ end
58
+
59
+ would 'call error_callback on errors' do
60
+ errors = []
61
+ @client.new(:error_callback => lambda{ |e| errors << e }).
62
+ get('http://localhost/') do |err|
63
+ err.should.kind_of?(Errno::ECONNREFUSED)
64
+ raise 'boom'
65
+ end.wait
66
+ errors.map(&:class).should.eq [Errno::ECONNREFUSED, RuntimeError]
67
+ end
68
+
48
69
  would 'then then then' do
49
70
  plusone = lambda do |r|
50
71
  r.merge(RC::RESPONSE_BODY => r[RC::RESPONSE_BODY] + 1)
@@ -65,10 +86,13 @@ describe RC::Promise do
65
86
  would 'call in a new thread if pool_size == 0' do
66
87
  @client.pool_size = 0
67
88
  thread = nil
89
+ rd, wr = IO.pipe
68
90
  mock(Thread).new.with_any_args.peek_return do |t|
69
91
  thread = t
92
+ wr.puts
70
93
  end
71
94
  @promise.defer do
95
+ rd.gets
72
96
  Thread.current.should.eq thread
73
97
  @promise.reject(nil)
74
98
  end
@@ -0,0 +1,63 @@
1
+
2
+ require 'rest-core/test'
3
+
4
+ describe RC::Retry do
5
+ before do
6
+ @called = called = []
7
+ @errors = errors = []
8
+ engine = Class.new do
9
+ define_method :call do |env, &block|
10
+ called << true
11
+ env[RC::FAIL].should.eq [true]
12
+ block.call(env.merge(RC::FAIL => [true, errors.shift]))
13
+ {}
14
+ end
15
+ end.new
16
+ @app = RC::Retry.new(engine, 5)
17
+ end
18
+
19
+ after do
20
+ @errors.size.should.eq 0
21
+ end
22
+
23
+ def call env={}
24
+ @app.call({RC::FAIL => [true]}.merge(env)){}
25
+ end
26
+
27
+ def max_retries
28
+ @app.max_retries({})
29
+ end
30
+
31
+ would 'retry max_retries times' do
32
+ @errors.replace([IOError.new] * max_retries)
33
+ call
34
+ @called.size.should.eq max_retries + 1
35
+ end
36
+
37
+ would 'retry several times' do
38
+ @errors.replace([IOError.new] * 2)
39
+ call
40
+ @called.size.should.eq 3
41
+ end
42
+
43
+ would 'not retry RuntimeError by default' do
44
+ @errors.replace([RuntimeError.new])
45
+ call
46
+ @called.size.should.eq 1
47
+ end
48
+
49
+ would 'retry RuntimeError when setup' do
50
+ @errors.replace([RuntimeError.new] * max_retries)
51
+ @app.retry_exceptions = [RuntimeError]
52
+ call
53
+ @called.size.should.eq max_retries + 1
54
+ end
55
+
56
+ would 'call error_callback upon retrying' do
57
+ @errors.replace([IOError.new] * 2)
58
+ errors = []
59
+ call(RC::CLIENT => stub.error_callback{errors.method(:<<)}.object)
60
+ @called.size.should.eq 3
61
+ errors.size.should.eq 2
62
+ end
63
+ end
@@ -11,13 +11,13 @@ describe RC::Timeout do
11
11
  end
12
12
 
13
13
  would 'bypass timeout if timeout is 0' do
14
- mock(app).monitor.times(0)
14
+ mock(app).process.times(0)
15
15
  app.call({}){ |e| e.should.eq({}) }
16
16
  end
17
17
 
18
- would 'run the monitor to setup timeout' do
18
+ would 'run the process to setup timeout' do
19
19
  env = {'timeout' => 2}
20
- mock(app).monitor(env)
20
+ mock(app).process(env)
21
21
  app.call(env){|e| e[RC::TIMER].should.kind_of?(RC::Timer)}
22
22
  end
23
23
 
@@ -43,9 +43,9 @@ describe RC::Timeout do
43
43
  }
44
44
  end
45
45
  app.pool_size = 1
46
- app.new.request(RC::RESPONSE_KEY => RC::FAIL, RC::TIMER => timer).
46
+ app.new.request(RC::RESPONSE_KEY => RC::FAIL, RC::TIMER => timer,
47
+ RC::ASYNC => true).
47
48
  first.message.should.eq 'boom'
48
- Muack.verify
49
49
  end
50
50
 
51
51
  would 'interrupt the task if timing out' do
@@ -70,14 +70,10 @@ describe RC::Timeout do
70
70
  }
71
71
  end
72
72
  (-1..1).each do |size|
73
- mock(any_instance_of(RC::Promise)).warn(is_a(String)) do |msg|
74
- msg.should.include?('boom')
75
- end
76
73
  app.pool_size = size
77
74
  app.new.request(RC::RESPONSE_KEY => RC::FAIL, RC::TIMER => timer,
78
- 'pipe' => wr).
75
+ RC::ASYNC => true, 'pipe' => wr).
79
76
  first.message.should.eq 'boom'
80
- Muack.verify
81
77
  end
82
78
  end
83
79
  end
@@ -2,6 +2,12 @@
2
2
  require 'rest-core/test'
3
3
 
4
4
  describe RC::Universal do
5
+ url = 'http://localhost/'
6
+
7
+ after do
8
+ WebMock.reset!
9
+ end
10
+
5
11
  would 'send Authorization header' do
6
12
  u = RC::Universal.new(:log_method => false)
7
13
  u.username = 'Aladdin'
@@ -18,7 +24,6 @@ describe RC::Universal do
18
24
  end
19
25
 
20
26
  would 'clash' do
21
- url = 'http://localhost/'
22
27
  stub_request(:get, url).to_return(:body => '{"a":{"b":"c"}}')
23
28
  res = RC::Universal.new(:json_response => true,
24
29
  :clash_response => true,
@@ -27,12 +32,25 @@ describe RC::Universal do
27
32
  end
28
33
 
29
34
  would 'follow redirect regardless response body' do
30
- url = 'http://localhost/'
35
+ called = []
31
36
  stub_request(:get, url).to_return(:body => 'bad json!',
32
37
  :status => 302, :headers => {'Location' => "#{url}a"})
33
- stub_request(:get, "#{url}a").to_return(:body => '{"good":"json!"}')
34
- RC::Universal.new(:json_response => true,
35
- :log_method => false).
36
- get(url).should.eq 'good' => 'json!'
38
+ stub_request(:get, "#{url}a").to_return do
39
+ Thread.pass
40
+ {:body => '{"good":"json!"}'}
41
+ end
42
+ RC::Universal.new(:json_response => true, :log_method => false).
43
+ get(url, &called.method(:<<)).wait
44
+ called.should.eq([{'good' => 'json!'}])
45
+ end
46
+
47
+ would 'retry and call error_callback' do
48
+ errors = []
49
+ called = []
50
+ RC::Universal.new(:error_callback => errors.method(:<<),
51
+ :max_retries => 1, :log_method => false).
52
+ get(url, &called.method(:<<)).wait
53
+ errors.map(&:class).should.eq [Errno::ECONNREFUSED]*2
54
+ called.map(&:class).should.eq [Errno::ECONNREFUSED]
37
55
  end
38
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.1
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lin Jen-Shin (godfat)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-29 00:00:00.000000000 Z
11
+ date: 2014-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
@@ -114,6 +114,7 @@ files:
114
114
  - lib/rest-core/middleware/oauth2_header.rb
115
115
  - lib/rest-core/middleware/oauth2_query.rb
116
116
  - lib/rest-core/middleware/query_response.rb
117
+ - lib/rest-core/middleware/retry.rb
117
118
  - lib/rest-core/middleware/smash_response.rb
118
119
  - lib/rest-core/middleware/timeout.rb
119
120
  - lib/rest-core/promise.rb
@@ -160,6 +161,7 @@ files:
160
161
  - test/test_payload.rb
161
162
  - test/test_promise.rb
162
163
  - test/test_query_response.rb
164
+ - test/test_retry.rb
163
165
  - test/test_simple.rb
164
166
  - test/test_smash.rb
165
167
  - test/test_smash_response.rb
@@ -186,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
188
  version: '0'
187
189
  requirements: []
188
190
  rubyforge_project:
189
- rubygems_version: 2.4.4
191
+ rubygems_version: 2.4.5
190
192
  signing_key:
191
193
  specification_version: 4
192
194
  summary: Modular Ruby clients interface for REST APIs.
@@ -218,6 +220,7 @@ test_files:
218
220
  - test/test_payload.rb
219
221
  - test/test_promise.rb
220
222
  - test/test_query_response.rb
223
+ - test/test_retry.rb
221
224
  - test/test_simple.rb
222
225
  - test/test_smash.rb
223
226
  - test/test_smash_response.rb