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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 13edab18f56e62ce573dc6cff597226d8d5ade6c
4
- data.tar.gz: e41513b602d15644f9f4000cfc85a14bd93b71f2
3
+ metadata.gz: b126115b38499438877d2c55fe68b14c3d9989bd
4
+ data.tar.gz: 6ba8f157a2025d94fe326767dc36823d11585854
5
5
  SHA512:
6
- metadata.gz: 70583190f10ad4f2a90075eb894a9f12da622c585353b20a77152de34ea573f5cc1d5c9512ca9d6b19722c13baba87bc829a10a9eb77162bf95c94bdc88b1cc7
7
- data.tar.gz: c583d03d2448e7d202a506661aea8a67be6f127f1b2ec3bf97176939585437d78fc28c5bb708f4f9fdcf2501d9e2806f2eb6a75f102949f32f5d238f34665826
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
@@ -1,5 +1,5 @@
1
1
 
2
- source 'http://rubygems.org'
2
+ source 'https://rubygems.org/'
3
3
 
4
4
  gemspec
5
5
 
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'
@@ -64,8 +64,14 @@ module RestCore::Client
64
64
  end
65
65
 
66
66
  def inspect
67
- fields = attributes.map{ |k, v| "#{k}=#{v.inspect}" }.join(', ')
68
- "#<struct #{self.class.name}#{if fields.empty? then '' else fields end}>"
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
- {REQUEST_METHOD => :get,
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
- Middleware.string_keys(attributes).merge(Middleware.string_keys(env))
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 = ::RestClient::Payload.generate(env[REQUEST_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
- :body => payload && payload.read,
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
- res = ::RestClient::Request.execute(:method => env[REQUEST_METHOD ],
16
- :url => request_uri(env) ,
17
- :payload => env[REQUEST_PAYLOAD],
18
- :headers => env[REQUEST_HEADERS],
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
 
@@ -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] || {})), &k)
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
- (env[REQUEST_HEADERS]||{}).sort.map{|(k,v)|"#{k}=#{v}"}.join('&')
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] || {})), &k)
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] || {})), &k)
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,
@@ -14,6 +14,6 @@ class RestCore::DefaultQuery
14
14
  defaults = string_keys(@query).merge(string_keys(query(env)))
15
15
 
16
16
  app.call(env.merge(REQUEST_QUERY =>
17
- defaults.merge(env[REQUEST_QUERY] || {})), &k)
17
+ defaults.merge(env[REQUEST_QUERY])), &k)
18
18
  end
19
19
  end
@@ -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] || {}) if access_token(env)
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
@@ -9,7 +9,7 @@ class RestCore::Oauth2Query
9
9
  local = if access_token(env)
10
10
  env.merge(REQUEST_QUERY =>
11
11
  {'access_token' => access_token(env)}.
12
- merge(env[REQUEST_QUERY] || {}))
12
+ merge(env[REQUEST_QUERY]))
13
13
  else
14
14
  env
15
15
  end
@@ -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