rest-core 2.0.4 → 2.1.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 +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
|