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 +4 -4
- data/CHANGES.md +31 -0
- data/README.md +1 -0
- data/lib/rest-core.rb +2 -1
- data/lib/rest-core/builder.rb +1 -2
- data/lib/rest-core/client.rb +35 -8
- data/lib/rest-core/client/universal.rb +4 -6
- data/lib/rest-core/event.rb +9 -3
- data/lib/rest-core/event_source.rb +1 -1
- data/lib/rest-core/middleware.rb +9 -2
- data/lib/rest-core/middleware/cache.rb +7 -6
- data/lib/rest-core/middleware/error_handler.rb +4 -30
- data/lib/rest-core/middleware/follow_redirect.rb +9 -13
- data/lib/rest-core/middleware/json_response.rb +1 -1
- data/lib/rest-core/middleware/retry.rb +33 -0
- data/lib/rest-core/middleware/timeout.rb +5 -12
- data/lib/rest-core/promise.rb +17 -7
- data/lib/rest-core/test.rb +1 -1
- data/lib/rest-core/timer.rb +1 -1
- data/lib/rest-core/util/parse_link.rb +2 -2
- data/lib/rest-core/util/parse_query.rb +1 -1
- data/lib/rest-core/version.rb +1 -1
- data/rest-core.gemspec +7 -4
- data/test/test_client.rb +12 -2
- data/test/test_error_handler.rb +11 -1
- data/test/test_event_source.rb +2 -10
- data/test/test_follow_redirect.rb +1 -0
- data/test/test_httpclient.rb +2 -4
- data/test/test_promise.rb +24 -0
- data/test/test_retry.rb +63 -0
- data/test/test_timeout.rb +6 -10
- data/test/test_universal.rb +24 -6
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4cd61e7344e03258e5ef9482bf9270e29115ee2
|
4
|
+
data.tar.gz: fbc1f00dcb47c15daec112ea4cf6a7397331bc74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/rest-core.rb
CHANGED
@@ -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/
|
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
|
}
|
data/lib/rest-core/builder.rb
CHANGED
@@ -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
|
-
|
80
|
-
else begin obj.dup; rescue TypeError; obj; end
|
79
|
+
else ; obj
|
81
80
|
end
|
82
81
|
end
|
83
82
|
|
data/lib/rest-core/client.rb
CHANGED
@@ -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,}
|
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
|
-
|
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
|
-
|
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
|
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
|
data/lib/rest-core/event.rb
CHANGED
@@ -4,9 +4,14 @@ module RestCore
|
|
4
4
|
RestCore.const_defined?(:EventStruct)
|
5
5
|
|
6
6
|
class Event < EventStruct
|
7
|
-
|
8
|
-
def
|
9
|
-
|
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
|
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
|
data/lib/rest-core/middleware.rb
CHANGED
@@ -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] =~ /\?/
|
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\-\.\_\~]+/
|
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
|
52
|
-
|
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,
|
101
|
+
def data_extract data, res, k
|
101
102
|
_, status, headers, body =
|
102
|
-
data.match(/\A(\d+)\n((?:[^\n]+\n)*)\n\n(.*)\Z/
|
103
|
+
data.match(/\A(\d+)\n((?:[^\n]+\n)*)\n\n(.*)\Z/m).to_a
|
103
104
|
|
104
|
-
Promise.claim(
|
105
|
-
Hash[(headers||'').scan(/([^:]+): ([^\n]+)\n/
|
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
|
-
|
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
|
-
|
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
|
-
|
10
|
-
|
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(
|
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
|
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(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
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
|
-
|
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
|
16
|
+
def process env, &k
|
24
17
|
timer = Timer.new(timeout(env), timeout_error)
|
25
|
-
|
18
|
+
app.call(env.merge(TIMER => timer), &k)
|
26
19
|
rescue Exception
|
27
20
|
timer.cancel
|
28
21
|
raise
|
data/lib/rest-core/promise.rb
CHANGED
@@ -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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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?
|
data/lib/rest-core/test.rb
CHANGED
@@ -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
|
-
|
14
|
+
class Pork::Executor
|
15
15
|
def with_img
|
16
16
|
f = Tempfile.new(['img', '.jpg'])
|
17
17
|
n = File.basename(f.path)
|
data/lib/rest-core/timer.rb
CHANGED
@@ -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}/
|
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[/<([^>]+)
|
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}] */
|
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
|
data/lib/rest-core/version.rb
CHANGED
data/rest-core.gemspec
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: rest-core 3.
|
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.
|
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
|
+
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.
|
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",
|
data/test/test_client.rb
CHANGED
@@ -7,7 +7,7 @@ describe RC::Simple do
|
|
7
7
|
Muack.verify
|
8
8
|
end
|
9
9
|
|
10
|
-
url = 'http://
|
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
|
data/test/test_error_handler.rb
CHANGED
@@ -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
|
-
|
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
|
data/test/test_event_source.rb
CHANGED
@@ -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.
|
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,
|
151
|
+
es, _, _ = server.call
|
160
152
|
es.onmessage do |event, data|
|
161
153
|
raise err = "error"
|
162
154
|
end.start.wait
|
data/test/test_httpclient.rb
CHANGED
@@ -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
|
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
|
data/test/test_promise.rb
CHANGED
@@ -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
|
data/test/test_retry.rb
ADDED
@@ -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
|
data/test/test_timeout.rb
CHANGED
@@ -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).
|
14
|
+
mock(app).process.times(0)
|
15
15
|
app.call({}){ |e| e.should.eq({}) }
|
16
16
|
end
|
17
17
|
|
18
|
-
would 'run the
|
18
|
+
would 'run the process to setup timeout' do
|
19
19
|
env = {'timeout' => 2}
|
20
|
-
mock(app).
|
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
|
data/test/test_universal.rb
CHANGED
@@ -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
|
-
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
+
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
|
+
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.
|
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
|