rest-core 1.0.3 → 2.0.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.
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