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