rest-core 1.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/.travis.yml +6 -7
  2. data/CHANGES.md +137 -0
  3. data/Gemfile +1 -1
  4. data/README.md +183 -191
  5. data/TODO.md +5 -8
  6. data/example/multi.rb +31 -24
  7. data/example/simple.rb +28 -0
  8. data/example/use-cases.rb +194 -0
  9. data/lib/rest-core.rb +26 -19
  10. data/lib/rest-core/builder.rb +2 -2
  11. data/lib/rest-core/client.rb +40 -27
  12. data/lib/rest-core/client/universal.rb +16 -13
  13. data/lib/rest-core/client_oauth1.rb +5 -5
  14. data/lib/rest-core/engine/auto.rb +25 -0
  15. data/lib/rest-core/{app → engine}/dry.rb +1 -2
  16. data/lib/rest-core/engine/em-http-request.rb +39 -0
  17. data/lib/rest-core/engine/future/future.rb +106 -0
  18. data/lib/rest-core/engine/future/future_fiber.rb +39 -0
  19. data/lib/rest-core/engine/future/future_thread.rb +29 -0
  20. data/lib/rest-core/engine/rest-client.rb +56 -0
  21. data/lib/rest-core/middleware.rb +27 -5
  22. data/lib/rest-core/middleware/auth_basic.rb +5 -5
  23. data/lib/rest-core/middleware/bypass.rb +2 -2
  24. data/lib/rest-core/middleware/cache.rb +67 -54
  25. data/lib/rest-core/middleware/common_logger.rb +5 -8
  26. data/lib/rest-core/middleware/default_headers.rb +2 -2
  27. data/lib/rest-core/middleware/default_payload.rb +26 -2
  28. data/lib/rest-core/middleware/default_query.rb +4 -2
  29. data/lib/rest-core/middleware/default_site.rb +8 -6
  30. data/lib/rest-core/middleware/error_detector.rb +9 -16
  31. data/lib/rest-core/middleware/error_handler.rb +25 -11
  32. data/lib/rest-core/middleware/follow_redirect.rb +11 -14
  33. data/lib/rest-core/middleware/json_request.rb +19 -0
  34. data/lib/rest-core/middleware/json_response.rb +28 -0
  35. data/lib/rest-core/middleware/oauth1_header.rb +2 -7
  36. data/lib/rest-core/middleware/oauth2_header.rb +4 -7
  37. data/lib/rest-core/middleware/oauth2_query.rb +2 -2
  38. data/lib/rest-core/middleware/timeout.rb +21 -65
  39. data/lib/rest-core/middleware/timeout/{eventmachine_timer.rb → timer_em.rb} +3 -1
  40. data/lib/rest-core/middleware/timeout/timer_thread.rb +36 -0
  41. data/lib/rest-core/patch/multi_json.rb +8 -0
  42. data/lib/rest-core/test.rb +3 -12
  43. data/lib/rest-core/util/json.rb +65 -0
  44. data/lib/rest-core/util/parse_query.rb +2 -2
  45. data/lib/rest-core/version.rb +1 -1
  46. data/lib/rest-core/wrapper.rb +16 -16
  47. data/rest-core.gemspec +28 -27
  48. data/test/test_auth_basic.rb +14 -10
  49. data/test/test_builder.rb +7 -7
  50. data/test/test_cache.rb +126 -37
  51. data/test/test_client.rb +3 -1
  52. data/test/test_client_oauth1.rb +2 -3
  53. data/test/test_default_query.rb +17 -23
  54. data/test/test_em_http_request.rb +146 -0
  55. data/test/test_error_detector.rb +0 -1
  56. data/test/test_error_handler.rb +44 -0
  57. data/test/test_follow_redirect.rb +17 -19
  58. data/test/test_json_request.rb +28 -0
  59. data/test/test_json_response.rb +51 -0
  60. data/test/test_oauth1_header.rb +4 -4
  61. data/test/test_payload.rb +20 -12
  62. data/test/test_simple.rb +14 -0
  63. data/test/test_timeout.rb +11 -19
  64. data/test/test_universal.rb +5 -5
  65. data/test/test_wrapper.rb +19 -13
  66. metadata +28 -29
  67. data/doc/ToC.md +0 -7
  68. data/doc/dependency.md +0 -4
  69. data/doc/design.md +0 -4
  70. data/example/auto.rb +0 -51
  71. data/example/coolio.rb +0 -21
  72. data/example/eventmachine.rb +0 -30
  73. data/example/rest-client.rb +0 -16
  74. data/lib/rest-core/app/abstract/async_fiber.rb +0 -13
  75. data/lib/rest-core/app/auto.rb +0 -23
  76. data/lib/rest-core/app/coolio-async.rb +0 -32
  77. data/lib/rest-core/app/coolio-fiber.rb +0 -30
  78. data/lib/rest-core/app/coolio.rb +0 -9
  79. data/lib/rest-core/app/em-http-request-async.rb +0 -37
  80. data/lib/rest-core/app/em-http-request-fiber.rb +0 -45
  81. data/lib/rest-core/app/em-http-request.rb +0 -9
  82. data/lib/rest-core/app/rest-client.rb +0 -41
  83. data/lib/rest-core/middleware/json_decode.rb +0 -93
  84. data/lib/rest-core/middleware/timeout/coolio_timer.rb +0 -10
  85. data/pending/test_multi.rb +0 -123
  86. data/pending/test_test_util.rb +0 -86
  87. data/test/test_json_decode.rb +0 -24
@@ -1,18 +1,21 @@
1
1
 
2
- RestCore::Universal = RestCore::Builder.client do
3
- s = RestCore
4
- use s::Timeout , 0
2
+ module RestCore
3
+ Universal = Builder.client do
4
+ use Timeout , 0
5
5
 
6
- use s::DefaultSite , nil
7
- use s::DefaultHeaders, {}
8
- use s::DefaultQuery , {}
9
- use s::AuthBasic , nil, nil
6
+ use DefaultSite , nil
7
+ use DefaultHeaders, {}
8
+ use DefaultQuery , {}
9
+ use DefaultPayload, {}
10
+ use JsonRequest , false
11
+ use AuthBasic , nil, nil
10
12
 
11
- use s::FollowRedirect, 10
12
- use s::CommonLogger , method(:puts)
13
- use s::Cache , {}, 600 do
14
- use s::ErrorHandler, nil
15
- use s::ErrorDetectorHttp
16
- use s::JsonDecode , false
13
+ use FollowRedirect, 10
14
+ use CommonLogger , method(:puts)
15
+ use Cache , {}, 600 do
16
+ use ErrorHandler, nil
17
+ use ErrorDetectorHttp
18
+ use JsonResponse, false
19
+ end
17
20
  end
18
21
  end
@@ -6,7 +6,7 @@ module RestCore::ClientOauth1
6
6
 
7
7
  def authorize_url! opts={}
8
8
  self.data = ParseQuery.parse_query(
9
- post(request_token_path, {}, {}, {:json_decode => false}.merge(opts)))
9
+ post(request_token_path, {}, {}, {:json_response => false}.merge(opts)))
10
10
 
11
11
  authorize_url
12
12
  end
@@ -17,7 +17,7 @@ module RestCore::ClientOauth1
17
17
 
18
18
  def authorize! opts={}
19
19
  self.data = ParseQuery.parse_query(
20
- post(access_token_path, {}, {}, {:json_decode => false}.merge(opts)))
20
+ post(access_token_path, {}, {}, {:json_response => false}.merge(opts)))
21
21
 
22
22
  data['authorized'] = 'true'
23
23
  data
@@ -28,12 +28,12 @@ module RestCore::ClientOauth1
28
28
  end
29
29
 
30
30
  def data_json
31
- JsonDecode.json_encode(data.merge('sig' => calculate_sig))
31
+ Json.encode(data.merge('sig' => calculate_sig))
32
32
  end
33
33
 
34
34
  def data_json= json
35
- self.data = check_sig_and_return_data(JsonDecode.json_decode(json))
36
- rescue JsonDecode.const_get(:ParseError)
35
+ self.data = check_sig_and_return_data(Json.decode(json))
36
+ rescue Json.const_get(:ParseError)
37
37
  self.data = nil
38
38
  end
39
39
 
@@ -0,0 +1,25 @@
1
+
2
+ require 'rest-core/middleware'
3
+
4
+ class RestCore::Auto
5
+ include RestCore::Middleware
6
+ def call env, &k
7
+ client = http_client
8
+ client.call(log(env, "Auto picked: #{client.class}"), &k)
9
+ end
10
+
11
+ def http_client
12
+ if Object.const_defined?(:EventMachine) &&
13
+ ::EventMachine.const_defined?(:HttpRequest) &&
14
+ ::EventMachine.reactor_running? &&
15
+ # it should be either wrapped around a thread or a fiber
16
+ ((Thread.main != Thread.current) ||
17
+ (Fiber.respond_to?(:current) && RootFiber != Fiber.current))
18
+
19
+ @emhttprequest ||= RestCore::EmHttpRequest.new
20
+
21
+ else
22
+ @restclient ||= RestCore::RestClient.new
23
+ end
24
+ end
25
+ end
@@ -4,7 +4,6 @@ require 'rest-core/middleware'
4
4
  class RestCore::Dry
5
5
  include RestCore::Middleware
6
6
  def call env
7
- env[ASYNC].call(env) if env[ASYNC]
8
- env
7
+ yield(env)
9
8
  end
10
9
  end
@@ -0,0 +1,39 @@
1
+
2
+ require 'em-http-request'
3
+ require 'restclient/payload'
4
+
5
+ require 'rest-core/engine/future/future'
6
+ require 'rest-core/middleware'
7
+
8
+ class RestCore::EmHttpRequest
9
+ include RestCore::Middleware
10
+ def call env, &k
11
+ future = Future.create(env, k, env[ASYNC])
12
+ payload = ::RestClient::Payload.generate(env[REQUEST_PAYLOAD])
13
+ client = ::EventMachine::HttpRequest.new(request_uri(env)).send(
14
+ env[REQUEST_METHOD],
15
+ :body => payload && payload.read,
16
+ :head => payload && payload.headers.
17
+ merge(env[REQUEST_HEADERS]))
18
+
19
+ client.callback{
20
+ future.wrap{ # callbacks are run in main thread, so we need to wrap it
21
+ future.on_load(client.response,
22
+ client.response_header.status,
23
+ client.response_header)}}
24
+
25
+ client.errback{future.wrap{ future.on_error(client.error) }}
26
+
27
+ env[TIMER].on_timeout{
28
+ (client.instance_variable_get(:@callbacks)||[]).clear
29
+ (client.instance_variable_get(:@errbacks )||[]).clear
30
+ client.close
31
+ future.wrap{ future.on_error(env[TIMER].error) }
32
+ } if env[TIMER]
33
+
34
+ env.merge(RESPONSE_BODY => future.proxy_body,
35
+ RESPONSE_STATUS => future.proxy_status,
36
+ RESPONSE_HEADERS => future.proxy_headers,
37
+ FUTURE => future)
38
+ end
39
+ end
@@ -0,0 +1,106 @@
1
+
2
+ require 'rest-core'
3
+
4
+ class RestCore::Future
5
+ include RestCore
6
+
7
+ class Proxy < BasicObject
8
+ def initialize future, target
9
+ @future, @target = future, target
10
+ end
11
+
12
+ def method_missing msg, *args, &block
13
+ @future.yield[@target].__send__(msg, *args, &block)
14
+ end
15
+ end
16
+
17
+ def self.create *args, &block
18
+ if Fiber.respond_to?(:current) && RootFiber != Fiber.current &&
19
+ # because under a thread, Fiber.current won't return the root fiber
20
+ Thread.main == Thread.current
21
+ FutureFiber .new(*args, &block)
22
+ else
23
+ FutureThread.new(*args, &block)
24
+ end
25
+ end
26
+
27
+ def initialize env, k, immediate
28
+ self.env = env
29
+ self.k = k
30
+ self.immediate = immediate
31
+ self.response, self.body, self.status, self.headers, self.error = nil
32
+ end
33
+
34
+ def proxy_body ; Proxy.new(self, RESPONSE_BODY ); end
35
+ def proxy_status ; Proxy.new(self, RESPONSE_STATUS ); end
36
+ def proxy_headers; Proxy.new(self, RESPONSE_HEADERS); end
37
+
38
+ def wrap ; raise NotImplementedError; end
39
+ def wait ; raise NotImplementedError; end
40
+ def resume; raise NotImplementedError; end
41
+
42
+ def loaded?
43
+ !!status
44
+ end
45
+
46
+ def yield
47
+ wait
48
+ callback
49
+ end
50
+
51
+ def callback
52
+ self.response ||= k.call(
53
+ env.merge(RESPONSE_BODY => body ,
54
+ RESPONSE_STATUS => status,
55
+ RESPONSE_HEADERS => headers,
56
+ FAIL => ((env[FAIL]||[]) + [error]).compact,
57
+ LOG => (env[LOG] ||[]) +
58
+ ["Future picked: #{self.class}"]))
59
+ end
60
+
61
+ def on_load body, status, headers
62
+ env[TIMER].cancel if env[TIMER]
63
+ synchronize{
64
+ self.body, self.status, self.headers = body, status, headers
65
+ begin
66
+ # under ASYNC callback, should call immediate
67
+ next_tick{ callback } if immediate
68
+ rescue Exception => e
69
+ # nothing we can do here for an asynchronous exception,
70
+ # so we just log the error
71
+ logger = method(:warn) # TODO: add error_log_method
72
+ logger.call "RestCore: ERROR: #{e}\n" \
73
+ " from #{e.backtrace.inspect}"
74
+ end
75
+ }
76
+ resume # client or response might be waiting
77
+ end
78
+
79
+ def on_error error
80
+ self.error = if error.kind_of?(Exception)
81
+ error
82
+ else
83
+ Error.new(error || 'unknown')
84
+ end
85
+ on_load('', 0, {})
86
+ end
87
+
88
+ protected
89
+ attr_accessor :env, :k, :immediate,
90
+ :response, :body, :status, :headers, :error
91
+
92
+ private
93
+ def synchronize; yield; end
94
+ # next_tick is used for telling the reactor that there's something else
95
+ # should be done, don't sleep and don't stop at the moment
96
+ def next_tick
97
+ if Object.const_defined?(:EventMachine) && EventMachine.reactor_running?
98
+ EventMachine.next_tick{ yield }
99
+ else
100
+ yield
101
+ end
102
+ end
103
+
104
+ autoload :FutureFiber , 'rest-core/engine/future/future_fiber'
105
+ autoload :FutureThread, 'rest-core/engine/future/future_thread'
106
+ end
@@ -0,0 +1,39 @@
1
+
2
+ require 'fiber'
3
+
4
+ class RestCore::Future::FutureFiber < RestCore::Future
5
+ def initialize *args
6
+ super
7
+ self.fibers = []
8
+ end
9
+
10
+ def wrap
11
+ Fiber.new{ yield }.resume
12
+ end
13
+
14
+ def wait
15
+ fibers << Fiber.current
16
+ Fiber.yield until loaded? # it might be resumed by some other futures!
17
+ end
18
+
19
+ def resume
20
+ return if fibers.empty?
21
+ current_fibers = fibers.dup
22
+ fibers.clear
23
+ current_fibers.each{ |f|
24
+ next unless f.alive?
25
+ next_tick{
26
+ begin
27
+ f.resume
28
+ rescue FiberError
29
+ # whenever timeout, it would be already resumed,
30
+ # and we have no way to tell if it's already resumed or not!
31
+ end
32
+ }
33
+ }
34
+ resume
35
+ end
36
+
37
+ protected
38
+ attr_accessor :fibers
39
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require 'thread'
3
+
4
+ class RestCore::Future::FutureThread < RestCore::Future
5
+ def initialize *args
6
+ super
7
+ self.condv = ConditionVariable.new
8
+ self.mutex = Mutex.new
9
+ end
10
+
11
+ def wrap
12
+ Thread.new{ yield }
13
+ end
14
+
15
+ def wait
16
+ # it might be awaken by some other futures!
17
+ synchronize{ condv.wait(mutex) until loaded? } unless loaded?
18
+ end
19
+
20
+ def resume
21
+ condv.broadcast
22
+ end
23
+
24
+ protected
25
+ attr_accessor :condv, :mutex
26
+
27
+ private
28
+ def synchronize; mutex.synchronize{ yield }; end
29
+ end
@@ -0,0 +1,56 @@
1
+
2
+ require 'restclient'
3
+ require 'rest-core/patch/rest-client'
4
+
5
+ require 'rest-core/engine/future/future'
6
+ require 'rest-core/middleware'
7
+
8
+ class RestCore::RestClient
9
+ include RestCore::Middleware
10
+ def call env, &k
11
+ future = Future::FutureThread.new(env, k, env[ASYNC])
12
+
13
+ t = future.wrap{ # we can implement thread pool in the future
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],
19
+ :max_redirects => 0)
20
+ future.on_load(res.body, res.code, normalize_headers(res.raw_headers))
21
+
22
+ rescue ::RestClient::Exception => e
23
+ if res = e.response
24
+ # we don't want to raise an exception for 404 requests
25
+ future.on_load(res.body, res.code,
26
+ normalize_headers(res.raw_headers))
27
+ else
28
+ future.on_error(e)
29
+ end
30
+ rescue Exception => e
31
+ future.on_error(e)
32
+ end
33
+ }
34
+
35
+ env[TIMER].on_timeout{
36
+ t.kill
37
+ future.on_error(env[TIMER].error)
38
+ } if env[TIMER]
39
+
40
+ env.merge(RESPONSE_BODY => future.proxy_body,
41
+ RESPONSE_STATUS => future.proxy_status,
42
+ RESPONSE_HEADERS => future.proxy_headers,
43
+ FUTURE => future)
44
+ end
45
+
46
+ def normalize_headers raw_headers
47
+ raw_headers.inject({}){ |r, (k, v)|
48
+ r[k.to_s.upcase.tr('-', '_')] = if v.kind_of?(Array) && v.size == 1
49
+ v.first
50
+ else
51
+ v
52
+ end
53
+ r
54
+ }
55
+ end
56
+ end
@@ -6,11 +6,16 @@ require 'cgi'
6
6
  module RestCore::Middleware
7
7
  include RestCore
8
8
 
9
+ # identity function
10
+ def self.id
11
+ @id ||= lambda{ |a| a }
12
+ end
13
+
9
14
  def self.included mod
10
15
  mod.send(:include, RestCore)
11
16
  mod.send(:attr_reader, :app)
12
- return unless mod.respond_to?(:members)
13
- src = mod.members.map{ |member| <<-RUBY }
17
+ mem = if mod.respond_to?(:members) then mod.members else [] end
18
+ src = mem.map{ |member| <<-RUBY }
14
19
  def #{member} env
15
20
  if env.key?('#{member}')
16
21
  env['#{member}']
@@ -19,7 +24,7 @@ module RestCore::Middleware
19
24
  end
20
25
  end
21
26
  RUBY
22
- args = [:app] + mod.members
27
+ args = [:app] + mem
23
28
  para_list = args.map{ |a| "#{a}=nil"}.join(', ')
24
29
  args_list = args .join(', ')
25
30
  ivar_list = args.map{ |a| "@#{a}" }.join(', ')
@@ -34,9 +39,10 @@ module RestCore::Middleware
34
39
  mod.send(:include, accessor)
35
40
  end
36
41
 
37
- def call env ; app.call(env) ; end
42
+ def call env, &k; app.call(env, &(k || id)) ; end
38
43
  def fail env, obj; env.merge(FAIL => (env[FAIL] || []) + [obj]); end
39
44
  def log env, obj; env.merge(LOG => (env[LOG] || []) + [obj]); end
45
+ def id ; Middleware.id ; end
40
46
  def run app=app
41
47
  if app.respond_to?(:app) && app.app
42
48
  run(app.app)
@@ -53,9 +59,25 @@ module RestCore::Middleware
53
59
  else
54
60
  q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
55
61
  "#{env[REQUEST_PATH]}#{q}" \
56
- "#{query.map{ |(k, v)|
62
+ "#{query.sort.map{ |(k, v)|
57
63
  "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')}"
58
64
  end
59
65
  end
60
66
  public :request_uri
67
+
68
+ def string_keys hash
69
+ hash.inject({}){ |r, (k, v)|
70
+ if v.kind_of?(Hash)
71
+ r[k.to_s] = case k.to_s
72
+ when REQUEST_QUERY, REQUEST_PAYLOAD, REQUEST_HEADERS
73
+ string_keys(v)
74
+ else; v
75
+ end
76
+ else
77
+ r[k.to_s] = v
78
+ end
79
+ r
80
+ }
81
+ end
82
+ public :string_keys
61
83
  end
@@ -5,20 +5,20 @@ class RestCore::AuthBasic
5
5
  def self.members; [:username, :password]; end
6
6
  include RestCore::Middleware
7
7
 
8
- def call env
8
+ def call env, &k
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
- " but password is missing."))
15
+ " but password is missing."), &k)
16
16
  end
17
17
  elsif password(env)
18
18
  app.call(log(env, "AuthBasic: password provided: #{password(env)}," \
19
- " but username is missing."))
19
+ " but username is missing."), &k)
20
20
  else
21
- app.call(env)
21
+ app.call(env, &k)
22
22
  end
23
23
  end
24
24