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
@@ -7,7 +7,7 @@ class RestCore::Bypass
7
7
  @app = app
8
8
  end
9
9
 
10
- def call env
11
- @app.call(env)
10
+ def call env, &k
11
+ @app.call(env, &k)
12
12
  end
13
13
  end
@@ -15,86 +15,99 @@ class RestCore::Cache
15
15
  @app, @cache, @expires_in = app, cache, expires_in
16
16
  end
17
17
 
18
- def call env
19
- e = if env['cache.update'] && env[REQUEST_METHOD] == :get
20
- cache_assign(env, nil)
18
+ def call env, &k
19
+ e = if env['cache.update']
20
+ cache_clear(env)
21
21
  else
22
22
  env
23
23
  end
24
24
 
25
25
  if cached = cache_get(e)
26
- env[TIMER].cancel if env[TIMER] && !env[TIMER].canceled?
27
- wrapped.call(cached)
26
+ e[TIMER].cancel if e[TIMER]
27
+ wrapped.call(cached, &k)
28
28
  else
29
- if e[ASYNC]
30
- app.call(e.merge(ASYNC => lambda{ |response|
31
- process(e, response)
32
- }))
33
- else
34
- process(e, app.call(e))
35
- end
29
+ app.call(e){ |res|
30
+ wrapped.call(res){ |res_wrapped|
31
+ k.call(if (res_wrapped[FAIL] || []).empty?
32
+ cache_for(res).merge(res_wrapped)
33
+ else
34
+ res_wrapped
35
+ end)}}
36
36
  end
37
37
  end
38
38
 
39
- def process env, response
40
- if env[ASYNC]
41
- wrapped.call(response.merge(ASYNC => lambda{ |response_wrapped|
42
- env[ASYNC].call(process_wrapped(env, response, response_wrapped))
43
- }))
44
- else
45
- process_wrapped(env, response, wrapped.call(response))
46
- end
47
- end
48
-
49
- def process_wrapped env, response, response_wrapped
50
- if (response_wrapped[FAIL] || []).empty?
51
- cache_for(env, response).merge(response_wrapped)
52
- else
53
- response_wrapped
54
- end
55
- end
56
-
57
- protected
58
39
  def cache_key env
59
- Digest::MD5.hexdigest(env['cache.key'] || request_uri(env))
40
+ "rest-core:cache:#{Digest::MD5.hexdigest(env['cache.key'] ||
41
+ cache_key_raw(env))}"
60
42
  end
61
43
 
62
44
  def cache_get env
63
45
  return unless cache(env)
46
+ return unless cache_for?(env)
47
+
64
48
  start_time = Time.now
65
- return unless value = cache(env)[cache_key(env)]
49
+ return unless data = cache(env)[cache_key(env)]
66
50
  log(env, Event::CacheHit.new(Time.now - start_time, request_uri(env))).
67
- merge(RESPONSE_BODY => value)
51
+ merge(data_extract(data))
52
+ end
53
+
54
+ private
55
+ def cache_key_raw env
56
+ "#{env[REQUEST_METHOD]}:#{request_uri(env)}:#{header_cache_key(env)}"
57
+ end
58
+
59
+ def cache_clear env
60
+ return env unless cache(env)
61
+ return env unless cache_for?(env)
62
+
63
+ cache_store(env, :[]=, nil)
68
64
  end
69
65
 
70
- def cache_for env, response
71
- return response unless cache(env)
72
- # fake post (env['cache.post'] => true) is considered get and need cache
73
- return response if env[REQUEST_METHOD] != :get unless env['cache.post']
66
+ def cache_for res
67
+ return res unless cache(res)
68
+ return res unless cache_for?(res)
74
69
 
75
- value = response[RESPONSE_BODY]
70
+ if expires_in(res).kind_of?(Fixnum) &&
71
+ cache(res).respond_to?(:store) &&
72
+ cache(res).method(:store).arity == -3
76
73
 
77
- if expires_in(env).kind_of?(Fixnum) &&
78
- cache(env).respond_to?(:store) &&
79
- cache(env).method(:store).arity == -3
80
- cache(env).store(cache_key(env), value,
81
- :expires_in => expires_in(env))
82
- response
74
+ cache_store(res, :store, data_construct(res),
75
+ :expires_in => expires_in(res))
83
76
  else
84
- cache_assign(response, value)
77
+ cache_store(res, :[]= , data_construct(res))
85
78
  end
86
79
  end
87
80
 
88
- def cache_assign env, value
89
- return env unless cache(env)
90
-
81
+ def cache_store res, msg, value, *args
91
82
  start_time = Time.now
92
- cache(env)[cache_key(env)] = value
93
- if value.nil?
94
- log(env,
95
- Event::CacheCleared.new(Time.now - start_time, request_uri(env)))
83
+ cache(res).send(msg, cache_key(res), value, *args)
84
+
85
+ if value
86
+ res
96
87
  else
97
- env
88
+ log(res,
89
+ Event::CacheCleared.new(Time.now - start_time, request_uri(res)))
98
90
  end
99
91
  end
92
+
93
+ def data_construct res
94
+ "#{ res[RESPONSE_STATUS]}\n" \
95
+ "#{(res[RESPONSE_HEADERS]||{}).map{|k,v|"#{k}: #{v}"}.join("\n")}\n\n" \
96
+ "#{ res[RESPONSE_BODY]}"
97
+ end
98
+
99
+ def data_extract data
100
+ _, status, headers, body = data.match(/\A(\d+)\n(.*)\n\n(.*)\Z/m).to_a
101
+ {RESPONSE_BODY => body,
102
+ RESPONSE_HEADERS => Hash[(headers||'').scan(/([^:]+): ([^\n]+)\n?/)],
103
+ RESPONSE_STATUS => status.to_i}
104
+ end
105
+
106
+ def cache_for? env
107
+ [:get, :head, :otpions].include?(env[REQUEST_METHOD])
108
+ end
109
+
110
+ def header_cache_key env
111
+ (env[REQUEST_HEADERS]||{}).sort.map{|(k,v)|"#{k}=#{v}"}.join('&')
112
+ end
100
113
  end
@@ -9,13 +9,9 @@ class RestCore::CommonLogger
9
9
  def call env
10
10
  start_time = Time.now
11
11
  flushed = flush(env)
12
- if env[ASYNC]
13
- app.call(flushed.merge(ASYNC => lambda{ |response|
14
- env[ASYNC].call(process(response, start_time))
15
- }))
16
- else
17
- process(app.call(flushed), start_time)
18
- end
12
+ app.call(flushed){ |response|
13
+ yield(process(response, start_time))
14
+ }
19
15
  rescue
20
16
  process(flushed, start_time)
21
17
  raise
@@ -33,6 +29,7 @@ class RestCore::CommonLogger
33
29
  end
34
30
 
35
31
  def log_request start_time, response
36
- Event::Requested.new(Time.now - start_time, request_uri(response))
32
+ Event::Requested.new(Time.now - start_time,
33
+ "#{response[RC::REQUEST_METHOD].to_s.upcase} #{request_uri(response)}")
37
34
  end
38
35
  end
@@ -4,8 +4,8 @@ require 'rest-core/middleware'
4
4
  class RestCore::DefaultHeaders
5
5
  def self.members; [:headers]; end
6
6
  include RestCore::Middleware
7
- def call env
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
@@ -4,8 +4,32 @@ require 'rest-core/middleware'
4
4
  class RestCore::DefaultPayload
5
5
  def self.members; [:payload]; end
6
6
  include RestCore::Middleware
7
- def call env
7
+
8
+ def initialize *args
9
+ super
10
+ @payload ||= {}
11
+ end
12
+
13
+ def call env, &k
14
+ defaults = merge(@payload, payload(env))
15
+
8
16
  app.call(env.merge(REQUEST_PAYLOAD =>
9
- @payload.merge(payload(env)).merge(env[REQUEST_PAYLOAD] || {})))
17
+ merge(defaults, env[REQUEST_PAYLOAD] || {})), &k)
18
+ end
19
+
20
+ # this method is intended to merge payloads if they are non-empty hashes,
21
+ # but prefer the right most one if they are not hashes.
22
+ def merge lhs, rhs
23
+ if rhs.respond_to?(:empty?) && rhs.empty?
24
+ lhs
25
+ elsif lhs.respond_to?(:merge)
26
+ if rhs.respond_to?(:merge)
27
+ string_keys(lhs).merge(string_keys(rhs))
28
+ else
29
+ rhs
30
+ end
31
+ else
32
+ rhs
33
+ end
10
34
  end
11
35
  end
@@ -10,8 +10,10 @@ class RestCore::DefaultQuery
10
10
  @query ||= {}
11
11
  end
12
12
 
13
- def call env
13
+ def call env, &k
14
+ defaults = string_keys(@query).merge(string_keys(query(env)))
15
+
14
16
  app.call(env.merge(REQUEST_QUERY =>
15
- @query.merge(query(env)).merge(env[REQUEST_QUERY] || {})))
17
+ defaults.merge(env[REQUEST_QUERY] || {})), &k)
16
18
  end
17
19
  end
@@ -5,11 +5,13 @@ class RestCore::DefaultSite
5
5
  def self.members; [:site]; end
6
6
  include RestCore::Middleware
7
7
 
8
- def call env
9
- if env[REQUEST_PATH].to_s.start_with?('http')
10
- app.call(env)
11
- else
12
- app.call(env.merge(REQUEST_PATH => "#{site(env)}#{env[REQUEST_PATH]}"))
13
- end
8
+ def call env, &k
9
+ path = if env[REQUEST_PATH].to_s.start_with?('http')
10
+ env[REQUEST_PATH]
11
+ else
12
+ "#{site(env)}#{env[REQUEST_PATH]}"
13
+ end
14
+
15
+ app.call(env.merge(REQUEST_PATH => path), &k)
14
16
  end
15
17
  end
@@ -6,21 +6,14 @@ class RestCore::ErrorDetector
6
6
  include RestCore::Middleware
7
7
 
8
8
  def call env
9
- if env[ASYNC]
10
- app.call(env.merge(ASYNC => lambda{ |response|
11
- env[ASYNC].call(process(env, response))
12
- }))
13
- else
14
- process(env, app.call(env))
15
- end
16
- end
17
-
18
- def process env, response
19
- detector = error_detector(env)
20
- if error = (detector && detector.call(response))
21
- fail(response, error)
22
- else
23
- response
24
- end
9
+ app.call(env){ |response|
10
+ detector = error_detector(env)
11
+ yield(
12
+ if error = (detector && detector.call(response))
13
+ fail(response, error)
14
+ else
15
+ response
16
+ end)
17
+ }
25
18
  end
26
19
  end
@@ -6,20 +6,34 @@ class RestCore::ErrorHandler
6
6
  include RestCore::Middleware
7
7
 
8
8
  def call env
9
- if env[ASYNC]
10
- app.call(handle(env).merge(ASYNC => lambda{ |response|
11
- env[ASYNC].call(handle(response))
12
- }))
13
- else
14
- handle(app.call(handle(env)))
15
- end
9
+ app.call(env){ |res|
10
+ yield(if (res[FAIL] || []).empty? # no errors at all
11
+ res
12
+ else
13
+ # if there's an exception, hand it over
14
+ if err = res[FAIL].find{ |e| e.kind_of?(Exception) }
15
+ process(res, err)
16
+
17
+ elsif h = error_handler(res)
18
+ # if the user provides an exception, hand it over
19
+ if (err = h.call(res)).kind_of?(Exception)
20
+ process(res, err)
21
+
22
+ else # otherwise we report all of them
23
+ res.merge(FAIL => [res[FAIL], err].flatten.compact)
24
+
25
+ end
26
+ else # no exceptions at all, then do nothing
27
+ res
28
+ end
29
+ end)}
16
30
  end
17
31
 
18
- def handle env
19
- if error_handler(env) && !(env[FAIL] || []).empty?
20
- error_handler(env).call(env)
32
+ def process res, err
33
+ if res[ASYNC]
34
+ res.merge(RESPONSE_BODY => err)
21
35
  else
22
- env
36
+ raise err
23
37
  end
24
38
  end
25
39
  end
@@ -5,26 +5,23 @@ class RestCore::FollowRedirect
5
5
  def self.members; [:max_redirects]; end
6
6
  include RestCore::Middleware
7
7
 
8
- def call env
8
+ def call env, &k
9
9
  e = env.merge('follow_redirect.max_redirects' =>
10
10
  env['follow_redirect.max_redirects'] ||
11
11
  max_redirects(env))
12
12
 
13
- return app.call(e) if e[DRY]
14
- if e[ASYNC]
15
- app.call(e.merge(ASYNC => lambda{ |response|
16
- e[ASYNC].call(process(response))
17
- }))
13
+ if e[DRY]
14
+ app.call(e, &k)
18
15
  else
19
- process(app.call(e))
16
+ app.call(e){ |res| process(res, k) }
20
17
  end
21
18
  end
22
19
 
23
- def process res
24
- return res if res['follow_redirect.max_redirects'] <= 0
25
- return res if ![301,302,303,307].include?(res[RESPONSE_STATUS])
26
- return res if [301,302 ,307].include?(res[RESPONSE_STATUS]) &&
27
- ![:get, :head ].include?(res[REQUEST_METHOD])
20
+ def process res, k
21
+ return k.call(res) if res['follow_redirect.max_redirects'] <= 0
22
+ return k.call(res) if ![301,302,303,307].include?(res[RESPONSE_STATUS])
23
+ return k.call(res) if [301,302 ,307].include?(res[RESPONSE_STATUS]) &&
24
+ ![:get, :head ].include?(res[REQUEST_METHOD])
28
25
 
29
26
  location = [res[RESPONSE_HEADERS]['LOCATION']].flatten.first
30
27
  meth = if res[RESPONSE_STATUS] == 303
@@ -35,8 +32,8 @@ class RestCore::FollowRedirect
35
32
 
36
33
  call(res.merge(REQUEST_PATH => location,
37
34
  REQUEST_METHOD => meth ,
38
- REQUEST_PAYLOAD => nil ,
35
+ REQUEST_PAYLOAD => {} ,
39
36
  'follow_redirect.max_redirects' =>
40
- res['follow_redirect.max_redirects'] - 1))
37
+ res['follow_redirect.max_redirects'] - 1), &k)
41
38
  end
42
39
  end
@@ -0,0 +1,19 @@
1
+
2
+ require 'rest-core/middleware'
3
+ require 'rest-core/util/json'
4
+
5
+ class RestCore::JsonRequest
6
+ def self.members; [:json_request]; end
7
+ include RestCore::Middleware
8
+
9
+ JSON_REQUEST_HEADER = {'Content-Type' => 'application/json'}.freeze
10
+
11
+ def call env, &k
12
+ return app.call(env, &k) unless json_request(env)
13
+ return app.call(env, &k) unless env[REQUEST_PAYLOAD]
14
+
15
+ app.call(env.merge(
16
+ REQUEST_HEADERS => JSON_REQUEST_HEADER.merge(env[REQUEST_HEADERS]||{}),
17
+ REQUEST_PAYLOAD => Json.encode(env[REQUEST_PAYLOAD]) ), &k)
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+
2
+ require 'rest-core/middleware'
3
+ require 'rest-core/util/json'
4
+
5
+ class RestCore::JsonResponse
6
+ def self.members; [:json_response]; end
7
+ include RestCore::Middleware
8
+
9
+ JSON_RESPONSE_HEADER = {'Accept' => 'application/json'}.freeze
10
+
11
+ def call env, &k
12
+ return app.call(env, &k) if env[DRY]
13
+ return app.call(env, &k) unless json_response(env)
14
+
15
+ app.call(env.merge(REQUEST_HEADERS =>
16
+ JSON_RESPONSE_HEADER.merge(env[REQUEST_HEADERS]||{}))){ |response|
17
+ yield(process(response))
18
+ }
19
+ end
20
+
21
+ def process response
22
+ response.merge(RESPONSE_BODY =>
23
+ Json.decode("[#{response[RESPONSE_BODY]}]").first)
24
+ # [this].first is not needed for yajl-ruby
25
+ rescue Json.const_get(:ParseError) => error
26
+ fail(response, error)
27
+ end
28
+ end
@@ -15,7 +15,7 @@ class RestCore::Oauth1Header
15
15
  :oauth_token, :oauth_token_secret, :data]
16
16
  end
17
17
  include RestCore::Middleware
18
- def call env
18
+ def call env, &k
19
19
  start_time = Time.now
20
20
  headers = {'Authorization' => oauth_header(env)}.
21
21
  merge(env[REQUEST_HEADERS] || {})
@@ -23,12 +23,7 @@ class RestCore::Oauth1Header
23
23
  event = Event::WithHeader.new(Time.now - start_time,
24
24
  "Authorization: #{headers['Authorization']}")
25
25
 
26
- app.call(log(cache_key(env.merge(REQUEST_HEADERS => headers)), event))
27
- end
28
-
29
- def cache_key env
30
- env.merge('cache.key' =>
31
- "#{request_uri(env)}&#{oauth_token(env)}&#{oauth_token_secret(env)}")
26
+ app.call(log(env.merge(REQUEST_HEADERS => headers), event), &k)
32
27
  end
33
28
 
34
29
  def oauth_header env