rest-core 2.0.4 → 2.1.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 +33 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/Rakefile +13 -0
- data/lib/rest-core.rb +2 -0
- data/lib/rest-core/client.rb +22 -13
- data/lib/rest-core/engine/em-http-request.rb +37 -8
- data/lib/rest-core/engine/rest-client.rb +6 -4
- data/lib/rest-core/middleware.rb +24 -3
- data/lib/rest-core/middleware/auth_basic.rb +1 -1
- data/lib/rest-core/middleware/cache.rb +2 -2
- data/lib/rest-core/middleware/default_headers.rb +1 -1
- data/lib/rest-core/middleware/default_payload.rb +1 -1
- data/lib/rest-core/middleware/default_query.rb +1 -1
- data/lib/rest-core/middleware/oauth1_header.rb +3 -13
- data/lib/rest-core/middleware/oauth2_header.rb +1 -1
- data/lib/rest-core/middleware/oauth2_query.rb +1 -1
- data/lib/rest-core/test.rb +48 -0
- data/lib/rest-core/util/payload.rb +162 -0
- data/lib/rest-core/version.rb +1 -1
- data/rest-core.gemspec +14 -7
- data/task/gemgem.rb +7 -6
- data/test/test_auth_basic.rb +7 -5
- data/test/test_cache.rb +6 -0
- data/test/test_client_oauth1.rb +22 -22
- data/test/test_default_payload.rb +38 -0
- data/test/test_default_query.rb +10 -8
- data/test/{test_em_http_request.rb → test_em-http-request.rb} +40 -0
- data/test/test_follow_redirect.rb +9 -12
- data/test/test_json_response.rb +8 -12
- data/test/test_oauth1_header.rb +53 -53
- data/test/test_payload.rb +189 -22
- data/test/test_rest-client.rb +40 -0
- data/test/test_timeout.rb +8 -10
- metadata +25 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b126115b38499438877d2c55fe68b14c3d9989bd
|
4
|
+
data.tar.gz: 6ba8f157a2025d94fe326767dc36823d11585854
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14b1c0022f58a2af1a26bb0126159937b0540ee462bb6fe96db3fa777e0ba7cd14183e6ff8e051e45acc65c02708a69f51b31ad3371d0792e4124c2811029720
|
7
|
+
data.tar.gz: 4a3e037e2b30b77c57e7b030c05322389c1a585766ba414d137ce790c8c3e0b1c141c281a1dd664d07e47150a1f9f2a963059e42ffdad91d63549afd07ce095f
|
data/CHANGES.md
CHANGED
@@ -1,5 +1,38 @@
|
|
1
1
|
# CHANGES
|
2
2
|
|
3
|
+
## rest-core 2.1.0 -- 2013-05-08
|
4
|
+
|
5
|
+
### Incompatible changes
|
6
|
+
|
7
|
+
* We no longer support Rails-like POST payload, like translating
|
8
|
+
`{:foo => [1, 2]}` to `'foo[]=1&foo[]=2'`. It would now be translated to
|
9
|
+
`'foo=1&foo=2'`. If you like `'foo[]'` as the key, simply pass it as
|
10
|
+
`{'foo[]' => [1, 2]}`.
|
11
|
+
|
12
|
+
* This also applies to nested hashes like `{:foo => {:bar => 1}`. If you
|
13
|
+
want that behaviour, just pass `{'foo[bar]' => 1}` which would then be
|
14
|
+
translated to `'foo[bar]=1'`.
|
15
|
+
|
16
|
+
### Bugs fixes
|
17
|
+
|
18
|
+
* [`Payload`] Now we could correctly support payload with "foo=1&foo=2".
|
19
|
+
* [`Client`] Fix inspect spacing.
|
20
|
+
|
21
|
+
### Enhancement
|
22
|
+
|
23
|
+
* [`Payload`] With this class introduced, replacing rest-client's own
|
24
|
+
payload implementation, we could pass StringIO or other sockets as the
|
25
|
+
payload body. This would also fix the issue that using the same key for
|
26
|
+
different values as allowed in the spec.
|
27
|
+
* [`EmHttpRequest`] Send payload as a file directly if it's a file. Buffer
|
28
|
+
the payload into a tempfile if it's from a socket or a large StringIO.
|
29
|
+
This should greatly reduce the memory usage as we don't build large
|
30
|
+
Ruby strings in the memory. Streaming is not yet supported though.
|
31
|
+
* [`Client`] Make inspect shorter.
|
32
|
+
* [`Client`] Introduce Client#default_env
|
33
|
+
* [`Middleware`] Introduce Middleware.percent_encode.
|
34
|
+
* [`Middleware`] Introduce Middleware.contain_binary?.
|
35
|
+
|
3
36
|
## rest-core 2.0.4 -- 2013-04-30
|
4
37
|
|
5
38
|
* [`EmHttpRequest`] Use `EM.schedule` to fix thread-safety issue.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -18,7 +18,7 @@ talk is in Mandarin.
|
|
18
18
|
|
19
19
|
## DESCRIPTION:
|
20
20
|
|
21
|
-
Modular Ruby clients interface for REST APIs
|
21
|
+
Modular Ruby clients interface for REST APIs.
|
22
22
|
|
23
23
|
There has been an explosion in the number of REST APIs available today.
|
24
24
|
To address the need for a way to access these APIs easily and elegantly,
|
data/Rakefile
CHANGED
@@ -22,6 +22,19 @@ task 'gem:spec' do
|
|
22
22
|
|
23
23
|
s.authors = ['Cardinal Blue', 'Lin Jen-Shin (godfat)']
|
24
24
|
s.email = ['dev (XD) cardinalblue.com']
|
25
|
+
|
26
|
+
s.post_install_message = <<-MARKDOWN
|
27
|
+
# [rest-core] Incompatible changes for POST requests:
|
28
|
+
|
29
|
+
* We no longer support Rails-like POST payload, like translating
|
30
|
+
`{:foo => [1, 2]}` to `'foo[]=1&foo[]=2'`. It would now be translated to
|
31
|
+
`'foo=1&foo=2'`. If you like `'foo[]'` as the key, simply pass it as
|
32
|
+
`{'foo[]' => [1, 2]}`.
|
33
|
+
|
34
|
+
* This also applies to nested hashes like `{:foo => {:bar => 1}`. If you
|
35
|
+
want that behaviour, just pass `{'foo[bar]' => 1}` which would then be
|
36
|
+
translated to `'foo[bar]=1'`.
|
37
|
+
MARKDOWN
|
25
38
|
end
|
26
39
|
|
27
40
|
Gemgem.write
|
data/lib/rest-core.rb
CHANGED
@@ -33,7 +33,9 @@ module RestCore
|
|
33
33
|
|
34
34
|
# misc utilities
|
35
35
|
autoload :Hmac , 'rest-core/util/hmac'
|
36
|
+
autoload :Json , 'rest-core/util/json'
|
36
37
|
autoload :ParseQuery , 'rest-core/util/parse_query'
|
38
|
+
autoload :Payload , 'rest-core/util/payload'
|
37
39
|
|
38
40
|
# middlewares
|
39
41
|
autoload :AuthBasic , 'rest-core/middleware/auth_basic'
|
data/lib/rest-core/client.rb
CHANGED
@@ -64,8 +64,14 @@ module RestCore::Client
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def inspect
|
67
|
-
fields =
|
68
|
-
|
67
|
+
fields = if size > 0
|
68
|
+
' ' + attributes.map{ |k, v|
|
69
|
+
"#{k}=#{v.inspect.sub(/(?<=.{12}).{4,}/, '...')}"
|
70
|
+
}.join(', ')
|
71
|
+
else
|
72
|
+
''
|
73
|
+
end
|
74
|
+
"#<struct #{self.class.name}#{fields}>"
|
69
75
|
end
|
70
76
|
|
71
77
|
def lighten! o={}
|
@@ -171,16 +177,8 @@ module RestCore::Client
|
|
171
177
|
end
|
172
178
|
|
173
179
|
def request_full env, app=app, &k
|
174
|
-
response = app.call(build_env(
|
175
|
-
|
176
|
-
REQUEST_PATH => '/' ,
|
177
|
-
REQUEST_QUERY => {} ,
|
178
|
-
REQUEST_PAYLOAD => {} ,
|
179
|
-
REQUEST_HEADERS => {} ,
|
180
|
-
FAIL => [] ,
|
181
|
-
LOG => [] ,
|
182
|
-
ASYNC => !!k }.merge(env)),
|
183
|
-
&(k || Middleware.id))
|
180
|
+
response = app.call(build_env({ASYNC => !!k}.merge(env)),
|
181
|
+
&(k || Middleware.id))
|
184
182
|
|
185
183
|
# under ASYNC callback, response might not be a response hash
|
186
184
|
# in that case (maybe in a user created engine), Client#wait
|
@@ -198,7 +196,18 @@ module RestCore::Client
|
|
198
196
|
end
|
199
197
|
|
200
198
|
def build_env env={}
|
201
|
-
|
199
|
+
default_env.merge(
|
200
|
+
Middleware.string_keys(attributes).merge(Middleware.string_keys(env)))
|
201
|
+
end
|
202
|
+
|
203
|
+
def default_env
|
204
|
+
{REQUEST_METHOD => :get,
|
205
|
+
REQUEST_PATH => '/' ,
|
206
|
+
REQUEST_QUERY => {} ,
|
207
|
+
REQUEST_PAYLOAD => {} ,
|
208
|
+
REQUEST_HEADERS => {} ,
|
209
|
+
FAIL => [] ,
|
210
|
+
LOG => [] }
|
202
211
|
end
|
203
212
|
# ------------------------ instance ---------------------
|
204
213
|
|
@@ -20,32 +20,61 @@ class RestCore::EmHttpRequest
|
|
20
20
|
FUTURE => future)
|
21
21
|
end
|
22
22
|
|
23
|
-
def close client
|
23
|
+
def close client, tmpfile
|
24
24
|
(client.instance_variable_get(:@callbacks)||[]).clear
|
25
25
|
(client.instance_variable_get(:@errbacks )||[]).clear
|
26
26
|
client.close
|
27
|
+
if tmpfile.respond_to?(:close!) # tempfile
|
28
|
+
tmpfile.close!
|
29
|
+
elsif tmpfile.respond_to?(:close) # regular IO
|
30
|
+
tmpfile.close
|
31
|
+
end
|
27
32
|
end
|
28
33
|
|
29
34
|
def request future, env
|
30
|
-
payload =
|
35
|
+
payload = Payload.generate(env[REQUEST_PAYLOAD])
|
36
|
+
tmpfile = payload2file(payload)
|
37
|
+
args = if tmpfile.respond_to?(:path)
|
38
|
+
{:file => tmpfile.path}
|
39
|
+
else
|
40
|
+
{:body => tmpfile}
|
41
|
+
end
|
31
42
|
client = ::EventMachine::HttpRequest.new(request_uri(env)).send(
|
32
|
-
env[REQUEST_METHOD],
|
33
|
-
|
34
|
-
:head => payload && payload.headers.
|
35
|
-
merge(env[REQUEST_HEADERS]))
|
43
|
+
env[REQUEST_METHOD], args.merge(
|
44
|
+
:head => payload.headers.merge(env[REQUEST_HEADERS])))
|
36
45
|
|
37
46
|
client.callback{
|
47
|
+
close(client, tmpfile)
|
38
48
|
future.on_load(client.response,
|
39
49
|
client.response_header.status,
|
40
50
|
client.response_header)}
|
41
51
|
|
42
52
|
client.errback{
|
43
|
-
close(client)
|
53
|
+
close(client, tmpfile)
|
44
54
|
future.on_error(client.error)}
|
45
55
|
|
46
56
|
env[TIMER].on_timeout{
|
47
|
-
close(client)
|
57
|
+
close(client, tmpfile)
|
48
58
|
future.on_error(env[TIMER].error)
|
49
59
|
} if env[TIMER]
|
50
60
|
end
|
61
|
+
|
62
|
+
def payload2file payload
|
63
|
+
if payload.io.respond_to?(:path) # already a file
|
64
|
+
payload.io
|
65
|
+
|
66
|
+
elsif payload.size == 0 || # probably a socket, buffer to disc
|
67
|
+
payload.size >= 81920 # probably too large, buffer to disc
|
68
|
+
create_tmpfile(payload.io)
|
69
|
+
|
70
|
+
else # probably not worth buffering to disc
|
71
|
+
payload.read
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_tmpfile io
|
76
|
+
tempfile = Tempfile.new("rest-core.em-http-request.#{rand(1_000_000)}")
|
77
|
+
IO.copy_stream(io, tempfile)
|
78
|
+
tempfile
|
79
|
+
end
|
51
80
|
end
|
@@ -12,10 +12,12 @@ class RestCore::RestClient
|
|
12
12
|
|
13
13
|
t = future.wrap{ # we can implement thread pool in the future
|
14
14
|
begin
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
:
|
15
|
+
payload = Payload.generate(env[REQUEST_PAYLOAD])
|
16
|
+
headers = env[REQUEST_HEADERS].merge(payload.headers)
|
17
|
+
res = ::RestClient::Request.execute(:method => env[REQUEST_METHOD],
|
18
|
+
:url => request_uri(env) ,
|
19
|
+
:payload => payload ,
|
20
|
+
:headers => headers ,
|
19
21
|
:max_redirects => 0)
|
20
22
|
future.on_load(res.body, res.code, normalize_headers(res.raw_headers))
|
21
23
|
|
data/lib/rest-core/middleware.rb
CHANGED
@@ -58,19 +58,40 @@ module RestCore::Middleware
|
|
58
58
|
env[REQUEST_PATH].to_s
|
59
59
|
else
|
60
60
|
q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
|
61
|
-
"#{env[REQUEST_PATH]}#{q}"
|
62
|
-
"#{query.sort.map{ |(k, v)|
|
63
|
-
"#{escape(k.to_s)}=#{escape(v.to_s)}" }.join('&')}"
|
61
|
+
"#{env[REQUEST_PATH]}#{q}#{percent_encode(query)}"
|
64
62
|
end
|
65
63
|
end
|
66
64
|
public :request_uri
|
67
65
|
|
66
|
+
def percent_encode query
|
67
|
+
query.sort.map{ |(k, v)|
|
68
|
+
if v.kind_of?(Array)
|
69
|
+
v.map{ |vv| "#{escape(k.to_s)}=#{escape(vv.to_s)}" }.join('&')
|
70
|
+
else
|
71
|
+
"#{escape(k.to_s)}=#{escape(v.to_s)}"
|
72
|
+
end
|
73
|
+
}.join('&')
|
74
|
+
end
|
75
|
+
public :percent_encode
|
76
|
+
|
68
77
|
UNRESERVED = /[^a-zA-Z0-9\-\.\_\~]/
|
69
78
|
def escape string
|
70
79
|
URI.escape(string, UNRESERVED)
|
71
80
|
end
|
72
81
|
public :escape
|
73
82
|
|
83
|
+
def contain_binary? payload
|
84
|
+
return false unless payload
|
85
|
+
return true if payload.respond_to?(:read)
|
86
|
+
return true if payload.find{ |k, v|
|
87
|
+
# if payload is an array, then v would be nil
|
88
|
+
(v || k).respond_to?(:read) ||
|
89
|
+
# if v is an array, it could contain binary data
|
90
|
+
(v.kind_of?(Array) && v.any?{ |vv| vv.respond_to?(:read) }) }
|
91
|
+
return false
|
92
|
+
end
|
93
|
+
public :contain_binary?
|
94
|
+
|
74
95
|
def string_keys hash
|
75
96
|
hash.inject({}){ |r, (k, v)|
|
76
97
|
if v.kind_of?(Hash)
|
@@ -9,7 +9,7 @@ class RestCore::AuthBasic
|
|
9
9
|
if username(env)
|
10
10
|
if password(env)
|
11
11
|
app.call(env.merge(REQUEST_HEADERS =>
|
12
|
-
auth_basic_header(env).merge(env[REQUEST_HEADERS]
|
12
|
+
auth_basic_header(env).merge(env[REQUEST_HEADERS])), &k)
|
13
13
|
else
|
14
14
|
app.call(log(env, "AuthBasic: username provided: #{username(env)}," \
|
15
15
|
" but password is missing."), &k)
|
@@ -106,10 +106,10 @@ class RestCore::Cache
|
|
106
106
|
end
|
107
107
|
|
108
108
|
def cache_for? env
|
109
|
-
[:get, :head, :otpions].include?(env[REQUEST_METHOD])
|
109
|
+
[:get, :head, :otpions].include?(env[REQUEST_METHOD]) && !env[DRY]
|
110
110
|
end
|
111
111
|
|
112
112
|
def header_cache_key env
|
113
|
-
|
113
|
+
env[REQUEST_HEADERS].sort.map{|(k,v)|"#{k}=#{v}"}.join('&')
|
114
114
|
end
|
115
115
|
end
|
@@ -6,6 +6,6 @@ class RestCore::DefaultHeaders
|
|
6
6
|
include RestCore::Middleware
|
7
7
|
def call env, &k
|
8
8
|
app.call(env.merge(REQUEST_HEADERS =>
|
9
|
-
@headers.merge(headers(env)).merge(env[REQUEST_HEADERS]
|
9
|
+
@headers.merge(headers(env)).merge(env[REQUEST_HEADERS])), &k)
|
10
10
|
end
|
11
11
|
end
|
@@ -14,7 +14,7 @@ class RestCore::DefaultPayload
|
|
14
14
|
defaults = merge(@payload, payload(env))
|
15
15
|
|
16
16
|
app.call(env.merge(REQUEST_PAYLOAD =>
|
17
|
-
merge(defaults, env[REQUEST_PAYLOAD]
|
17
|
+
merge(defaults, env[REQUEST_PAYLOAD])), &k)
|
18
18
|
end
|
19
19
|
|
20
20
|
# this method is intended to merge payloads if they are non-empty hashes,
|
@@ -16,7 +16,7 @@ class RestCore::Oauth1Header
|
|
16
16
|
def call env, &k
|
17
17
|
start_time = Time.now
|
18
18
|
headers = {'Authorization' => oauth_header(env)}.
|
19
|
-
merge(env[REQUEST_HEADERS]
|
19
|
+
merge(env[REQUEST_HEADERS])
|
20
20
|
|
21
21
|
event = Event::WithHeader.new(Time.now - start_time,
|
22
22
|
"Authorization: #{headers['Authorization']}")
|
@@ -52,7 +52,7 @@ class RestCore::Oauth1Header
|
|
52
52
|
method = env[REQUEST_METHOD].to_s.upcase
|
53
53
|
base_uri = env[REQUEST_PATH]
|
54
54
|
payload = payload_params(env)
|
55
|
-
query = reject_blank(env[REQUEST_QUERY]
|
55
|
+
query = reject_blank(env[REQUEST_QUERY])
|
56
56
|
params = reject_blank(oauth_params.merge(query.merge(payload))).
|
57
57
|
to_a.sort.map{ |(k, v)|
|
58
58
|
"#{escape(k.to_s)}=#{escape(v.to_s)}"}.join('&')
|
@@ -85,20 +85,10 @@ class RestCore::Oauth1Header
|
|
85
85
|
|
86
86
|
# so the Content-Type header must be application/x-www-form-urlencoded
|
87
87
|
else
|
88
|
-
reject_blank(env[REQUEST_PAYLOAD]
|
88
|
+
reject_blank(env[REQUEST_PAYLOAD])
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
def contain_binary? payload
|
93
|
-
return false unless payload
|
94
|
-
return true if payload.kind_of?(IO) ||
|
95
|
-
payload.respond_to?(:read)
|
96
|
-
return true if payload.find{ |k, v|
|
97
|
-
# if payload is an array, then v would be nil
|
98
|
-
(v || k).kind_of?(IO) || (v || k).respond_to?(:read) }
|
99
|
-
return false
|
100
|
-
end
|
101
|
-
|
102
92
|
def reject_blank params
|
103
93
|
params.reject{ |k, v| v.nil? || v == false ||
|
104
94
|
(v.respond_to?(:strip) &&
|
@@ -9,7 +9,7 @@ class RestCore::Oauth2Header
|
|
9
9
|
start_time = Time.now
|
10
10
|
headers = {'Authorization' =>
|
11
11
|
"#{access_token_type(env)} #{access_token(env)}"}.
|
12
|
-
merge(env[REQUEST_HEADERS]
|
12
|
+
merge(env[REQUEST_HEADERS]) if access_token(env)
|
13
13
|
|
14
14
|
event = Event::WithHeader.new(Time.now - start_time,
|
15
15
|
"Authorization: #{headers['Authorization']}") if headers
|
data/lib/rest-core/test.rb
CHANGED
@@ -23,4 +23,52 @@ module Kernel
|
|
23
23
|
def lt? rhs
|
24
24
|
self < rhs
|
25
25
|
end
|
26
|
+
|
27
|
+
def with_img
|
28
|
+
f = Tempfile.new(['img', '.jpg'])
|
29
|
+
n = File.basename(f.path)
|
30
|
+
f.write('a'*10)
|
31
|
+
f.rewind
|
32
|
+
yield(f, n)
|
33
|
+
ensure
|
34
|
+
f.close!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# https://github.com/bblimke/webmock/pull/280
|
39
|
+
class ::EventMachine::WebMockHttpClient
|
40
|
+
def build_request_signature
|
41
|
+
headers, body = @req.headers, @req.body
|
42
|
+
|
43
|
+
@conn.middleware.select {|m| m.respond_to?(:request) }.each do |m|
|
44
|
+
headers, body = m.request(self, headers, body)
|
45
|
+
end
|
46
|
+
|
47
|
+
method = @req.method
|
48
|
+
uri = @req.uri.clone
|
49
|
+
auth = @req.headers[:'proxy-authorization']
|
50
|
+
query = @req.query
|
51
|
+
|
52
|
+
if auth
|
53
|
+
userinfo = auth.join(':')
|
54
|
+
userinfo = WebMock::Util::URI.encode_unsafe_chars_in_userinfo(userinfo)
|
55
|
+
if @req
|
56
|
+
@req.proxy.reject! {|k,v| t.to_s == 'authorization' }
|
57
|
+
else
|
58
|
+
options.reject! {|k,v| k.to_s == 'authorization' } #we added it to url userinfo
|
59
|
+
end
|
60
|
+
uri.userinfo = userinfo
|
61
|
+
end
|
62
|
+
|
63
|
+
uri.query = encode_query(@req.uri, query).slice(/\?(.*)/, 1)
|
64
|
+
|
65
|
+
body = form_encode_body(body) if body.is_a?(Hash)
|
66
|
+
|
67
|
+
WebMock::RequestSignature.new(
|
68
|
+
method.downcase.to_sym,
|
69
|
+
uri.to_s,
|
70
|
+
:body => body || (@req.file && File.read(@req.file)),
|
71
|
+
:headers => headers
|
72
|
+
)
|
73
|
+
end
|
26
74
|
end
|