rest-core 3.4.1 → 3.5.0

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.
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