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
@@ -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