rack-client 0.1.1 → 0.3.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 (76) hide show
  1. data/History.txt +2 -2
  2. data/README.textile +2 -2
  3. data/Rakefile +11 -5
  4. data/demo/demo_spec.rb +3 -3
  5. data/lib/rack/client.rb +29 -25
  6. data/lib/rack/client/adapter.rb +6 -0
  7. data/lib/rack/client/adapter/base.rb +57 -0
  8. data/lib/rack/client/adapter/simple.rb +49 -0
  9. data/lib/rack/client/auth/abstract/challenge.rb +53 -0
  10. data/lib/rack/client/auth/basic.rb +57 -0
  11. data/lib/rack/client/auth/digest/challenge.rb +38 -0
  12. data/lib/rack/client/auth/digest/md5.rb +78 -0
  13. data/lib/rack/client/auth/digest/params.rb +10 -0
  14. data/lib/rack/client/body.rb +12 -0
  15. data/lib/rack/client/cache.rb +19 -0
  16. data/lib/rack/client/cache/cachecontrol.rb +195 -0
  17. data/lib/rack/client/cache/context.rb +95 -0
  18. data/lib/rack/client/cache/entitystore.rb +77 -0
  19. data/lib/rack/client/cache/key.rb +51 -0
  20. data/lib/rack/client/cache/metastore.rb +133 -0
  21. data/lib/rack/client/cache/options.rb +147 -0
  22. data/lib/rack/client/cache/request.rb +46 -0
  23. data/lib/rack/client/cache/response.rb +62 -0
  24. data/lib/rack/client/cache/storage.rb +43 -0
  25. data/lib/rack/client/cookie_jar.rb +17 -0
  26. data/lib/rack/client/cookie_jar/context.rb +59 -0
  27. data/lib/rack/client/cookie_jar/cookie.rb +59 -0
  28. data/lib/rack/client/cookie_jar/cookiestore.rb +36 -0
  29. data/lib/rack/client/cookie_jar/options.rb +43 -0
  30. data/lib/rack/client/cookie_jar/request.rb +15 -0
  31. data/lib/rack/client/cookie_jar/response.rb +16 -0
  32. data/lib/rack/client/cookie_jar/storage.rb +34 -0
  33. data/lib/rack/client/dual_band.rb +13 -0
  34. data/lib/rack/client/follow_redirects.rb +47 -20
  35. data/lib/rack/client/handler.rb +10 -0
  36. data/lib/rack/client/handler/em-http.rb +66 -0
  37. data/lib/rack/client/handler/excon.rb +50 -0
  38. data/lib/rack/client/handler/net_http.rb +85 -0
  39. data/lib/rack/client/handler/typhoeus.rb +62 -0
  40. data/lib/rack/client/headers.rb +49 -0
  41. data/lib/rack/client/parser.rb +18 -0
  42. data/lib/rack/client/parser/base.rb +25 -0
  43. data/lib/rack/client/parser/body_collection.rb +50 -0
  44. data/lib/rack/client/parser/context.rb +15 -0
  45. data/lib/rack/client/parser/json.rb +54 -0
  46. data/lib/rack/client/parser/middleware.rb +8 -0
  47. data/lib/rack/client/parser/request.rb +21 -0
  48. data/lib/rack/client/parser/response.rb +19 -0
  49. data/lib/rack/client/parser/yaml.rb +52 -0
  50. data/lib/rack/client/response.rb +9 -0
  51. data/lib/rack/client/version.rb +5 -0
  52. data/spec/apps/example.org.ru +47 -3
  53. data/spec/auth/basic_spec.rb +69 -0
  54. data/spec/auth/digest/md5_spec.rb +69 -0
  55. data/spec/cache_spec.rb +40 -0
  56. data/spec/cookie_jar_spec.rb +37 -0
  57. data/spec/endpoint_spec.rb +4 -13
  58. data/spec/follow_redirect_spec.rb +27 -0
  59. data/spec/handler/async_api_spec.rb +69 -0
  60. data/spec/handler/em_http_spec.rb +22 -0
  61. data/spec/handler/excon_spec.rb +7 -0
  62. data/spec/handler/net_http_spec.rb +8 -0
  63. data/spec/handler/sync_api_spec.rb +55 -0
  64. data/spec/handler/typhoeus_spec.rb +22 -0
  65. data/spec/middleware_helper.rb +37 -0
  66. data/spec/middleware_spec.rb +48 -5
  67. data/spec/parser/json_spec.rb +22 -0
  68. data/spec/parser/yaml_spec.rb +22 -0
  69. data/spec/server_helper.rb +72 -0
  70. data/spec/spec_helper.rb +17 -3
  71. metadata +86 -31
  72. data/lib/rack/client/auth.rb +0 -13
  73. data/lib/rack/client/http.rb +0 -77
  74. data/spec/auth_spec.rb +0 -22
  75. data/spec/core_spec.rb +0 -123
  76. data/spec/redirect_spec.rb +0 -12
@@ -0,0 +1,59 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class Context
5
+ include Options
6
+ include DualBand
7
+
8
+ def initialize(app, options = {})
9
+ @app = app
10
+
11
+ initialize_options options
12
+ end
13
+
14
+ def sync_call(env)
15
+ request = Request.new(env)
16
+ cookies = lookup(request)
17
+ request.inject(cookies)
18
+
19
+ response = Response.new(*@app.call(request.env))
20
+ cookies = Cookie.merge(cookies, response.cookies)
21
+ store cookies
22
+
23
+ response['rack-client-cookiejar.cookies'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
24
+ response.finish
25
+ end
26
+
27
+ def async_call(env)
28
+ request = Request.new(env)
29
+ cookies = lookup(request)
30
+ request.inject(cookies)
31
+
32
+ @app.call(request.env) do |request_parts|
33
+ response = Response.new(*request_parts)
34
+ cookies = Cookie.merge(cookies, response.cookies)
35
+ store cookies
36
+
37
+ response['rack-client-cookiejar.cookies'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
38
+ yield response.finish
39
+ end
40
+ end
41
+
42
+ def lookup(request)
43
+ cookiestore.match(request.host, request.path)
44
+ end
45
+
46
+ def store(cookies)
47
+ cookies.each do |cookie|
48
+ cookiestore.store(cookie)
49
+ end
50
+ end
51
+
52
+ def cookiestore
53
+ uri = options['rack-client-cookiejar.cookiestore']
54
+ storage.resolve_cookiestore_uri(uri)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class Cookie < Struct.new(:key, :value, :domain, :path)
5
+
6
+ def self.merge(bottom, top)
7
+ bottom.reject {|a| top.any? {|b| a == b } } | top
8
+ end
9
+
10
+ def self.parse(raw)
11
+ raw.split(', ').map {|header| from(header) }
12
+ end
13
+
14
+ def self.from(header)
15
+ data = header.split('; ')
16
+ tuple = data.shift.split('=')
17
+ parts = data.map {|s| s.split('=') }
18
+
19
+ new parts.inject('key'=> tuple.first, 'value'=> tuple.last) {|h,(k,v)| h.update(k => v)}
20
+ end
21
+
22
+ def initialize(parts = {})
23
+ parts.each do |k,v|
24
+ send(:"#{k}=", v)
25
+ end
26
+ end
27
+
28
+ def to_key
29
+ [ key, domain, path ] * ';'
30
+ end
31
+
32
+ def to_header
33
+ hash = members.zip(values).inject({}) {|h,(k,v)| h.update(k => v) }.reject {|k,v| v.nil?}
34
+ "#{hash.delete('key')}=#{hash.delete('value')}" << ('; ' + hash.map {|(k,v)| "#{k}=#{v}" } * '; ' unless hash.empty?)
35
+ end
36
+
37
+ def eql?(other)
38
+ to_key == other.to_key
39
+ end
40
+
41
+ def match?(domain, path)
42
+ fuzzy_domain_equal(domain) && fuzzy_path_equal(path)
43
+ end
44
+
45
+ def fuzzy_domain_equal(other_domain)
46
+ if domain =~ /^\./
47
+ other_domain =~ /#{Regexp.escape(domain)}$/
48
+ else
49
+ domain == other_domain
50
+ end
51
+ end
52
+
53
+ def fuzzy_path_equal(other_path)
54
+ path == '/' || path == other_path
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class CookieStore
5
+ def store(cookie)
6
+ write cookie.to_key, cookie.to_header
7
+ end
8
+
9
+ def match(domain, path)
10
+ cookies = map {|header| Cookie.from(header) }
11
+ cookies.select {|cookie| cookie.match?(domain, path) }
12
+ end
13
+
14
+ class Heap < CookieStore
15
+ def initialize
16
+ @heap = Hash.new {|h,k| h[k] = [] }
17
+ end
18
+
19
+ def write(key, value)
20
+ @heap[key] << value
21
+ end
22
+
23
+ def map
24
+ @heap.values.flatten.map {|*a| yield *a }
25
+ end
26
+
27
+ def self.resolve(uri)
28
+ new
29
+ end
30
+ end
31
+
32
+ HEAP = Heap
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ module Options
5
+ def self.option_accessor(key)
6
+ name = option_name(key)
7
+ define_method(key) { || options[name] }
8
+ define_method("#{key}=") { |value| options[name] = value }
9
+ define_method("#{key}?") { || !! options[name] }
10
+ end
11
+
12
+ def options
13
+ @default_options.merge(@options)
14
+ end
15
+
16
+ def options=(hash = {})
17
+ @options = hash.each { |key,value| write_option(key, value) }
18
+ end
19
+
20
+ def option_name(key)
21
+ case key
22
+ when Symbol ; "rack-client-cookiejar.#{key}"
23
+ when String ; key
24
+ else raise ArgumentError
25
+ end
26
+ end
27
+ module_function :option_name
28
+
29
+ option_accessor :storage
30
+ option_accessor :cookiestore
31
+ option_accessor :cookies
32
+
33
+ def initialize_options(options={})
34
+ @default_options = {
35
+ 'rack-client-cookiejar.storage' => Storage.new,
36
+ 'rack-client-cookiejar.cookiestore' => 'heap:/',
37
+ }
38
+ self.options = options
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class Request < Rack::Request
5
+ def inject(cookies)
6
+ if raw_cookies = env['HTTP_COOKIE']
7
+ cookies = Cookie.merge(cookies, raw_cookies)
8
+ end
9
+
10
+ env['HTTP_COOKIE'] = cookies.map {|c| c.to_header } * ', ' unless cookies.empty?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class Response < Client::Response
5
+ def cookies
6
+ return [] unless set_cookie
7
+ Cookie.parse(set_cookie.last)
8
+ end
9
+
10
+ def set_cookie
11
+ @set_cookie ||= headers.detect {|(k,v)| k =~ /Set-Cookie/i }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ class Storage
5
+ def initialize
6
+ @cookiestores = {}
7
+ end
8
+
9
+ def resolve_cookiestore_uri(uri)
10
+ @cookiestores[uri.to_s] ||= create_store(uri)
11
+ end
12
+
13
+ def create_store(uri)
14
+ if uri.respond_to?(:scheme) || uri.respond_to?(:to_str)
15
+ uri = URI.parse(uri) unless uri.respond_to?(:scheme)
16
+ if CookieStore.const_defined?(uri.scheme.upcase)
17
+ klass = CookieStore.const_get(uri.scheme.upcase)
18
+ klass.resolve(uri)
19
+ else
20
+ fail "Unknown storage provider: #{uri.to_s}"
21
+ end
22
+ else
23
+ fail "Unknown storage provider: #{uri.to_s}"
24
+ end
25
+ end
26
+
27
+ @@singleton_instance = new
28
+ def self.instance
29
+ @@singleton_instance
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ module Rack
2
+ module Client
3
+ module DualBand
4
+ def call(env, &block)
5
+ if block_given?
6
+ async_call(env, &block)
7
+ else
8
+ sync_call(env)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,27 +1,54 @@
1
- class Rack::Client::FollowRedirects
2
- include Rack::Test::Methods
1
+ module Rack
2
+ module Client
3
+ class FollowRedirects
4
+ include DualBand
3
5
 
4
- def initialize(app)
5
- @app = app
6
- end
6
+ def initialize(app)
7
+ @app = app
8
+ end
7
9
 
8
- def call(env)
9
- status, headers, body = @app.call(env)
10
- response = Rack::Response.new(body, status, headers)
11
- if response.redirect?
12
- uri = URI.parse(response["Location"])
13
- new_env = {}
14
- env.each do |k,v|
15
- if %w| HTTP_HOST SERVER_NAME SERVER_PORT |.include?(k)
16
- new_env[k] = v
10
+ def async_call(env, &block)
11
+ @app.call(env) do |tuple|
12
+ response = Response.new(*tuple)
13
+
14
+ if response.redirect?
15
+ follow_redirect(response, env, &block)
16
+ else
17
+ yield response.finish
17
18
  end
18
19
  end
19
- new_env["REQUEST_METHOD"] = "GET"
20
- session = Rack::Test::Session.new(@app)
21
- env = session.send(:env_for, uri.to_s, new_env)
22
- call(env)
23
- else
24
- [status, headers, body]
20
+ end
21
+
22
+ def sync_call(env, &block)
23
+ response = Response.new(*@app.call(env))
24
+ response.redirect? ? follow_redirect(response, env, &block) : response
25
+ end
26
+
27
+ def follow_redirect(response, env, &block)
28
+ call(next_env(response, env), &block)
29
+ end
30
+
31
+ def next_env(response, env)
32
+ env = env.dup
33
+
34
+ original = URI.parse(env['REQUEST_URI'])
35
+ redirection = URI.parse(response['Location'])
36
+
37
+ uri = original.merge(redirection)
38
+
39
+ env.update 'REQUEST_METHOD' => 'GET'
40
+ env.update 'PATH_INFO' => uri.path.empty? ? '/' : uri.path
41
+ env.update 'REQUEST_URI' => uri.to_s
42
+ env.update 'SERVER_NAME' => uri.host
43
+ env.update 'SERVER_PORT' => uri.port
44
+ env.update 'SCRIPT_NAME' => ''
45
+
46
+ env.update 'rack.url_scheme' => uri.scheme
47
+
48
+ env.update 'HTTPS' => env["rack.url_scheme"] == "https" ? "on" : "off"
49
+
50
+ env
25
51
  end
26
52
  end
27
53
  end
54
+ end
@@ -0,0 +1,10 @@
1
+ module Rack
2
+ module Client
3
+ module Handler
4
+ autoload :NetHTTP, 'rack/client/handler/net_http'
5
+ autoload :Excon, 'rack/client/handler/excon'
6
+ autoload :EmHttp, 'rack/client/handler/em-http'
7
+ autoload :Typhoeus, 'rack/client/handler/typhoeus'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,66 @@
1
+ require 'em-http'
2
+
3
+ module Rack
4
+ module Client
5
+ module Handler
6
+ class EmHttp
7
+ include Rack::Client::DualBand
8
+
9
+ def initialize(url)
10
+ @uri = URI.parse(url)
11
+ end
12
+
13
+ def sync_call(env)
14
+ raise("Synchronous API is not supported for EmHttp Handler") unless block_given?
15
+ end
16
+
17
+ def async_call(env)
18
+ request = Rack::Request.new(env)
19
+
20
+ EM.schedule do
21
+ em_http = connection(request.path).send(request.request_method.downcase, request_options(request))
22
+ em_http.callback do
23
+ yield parse(em_http).finish
24
+ end
25
+
26
+ em_http.errback do
27
+ yield parse(em_http).finish
28
+ end
29
+ end
30
+ end
31
+
32
+ def connection(path)
33
+ @connection ||= EventMachine::HttpRequest.new((@uri + path).to_s)
34
+ end
35
+
36
+ def request_options(request)
37
+ options = {}
38
+
39
+ if request.body
40
+ options[:body] = case request.body
41
+ when Array then request.body.to_s
42
+ when StringIO then request.body.string
43
+ when IO then request.body.read
44
+ when String then request.body
45
+ end
46
+ end
47
+
48
+ options
49
+ end
50
+
51
+ def parse(em_http)
52
+ body = em_http.response.empty? ? [] : StringIO.new(em_http.response)
53
+ Response.new(em_http.response_header.status, Headers.new(em_http.response_header).to_http, body)
54
+ end
55
+
56
+ def normalize_headers(em_http)
57
+ headers = em_http.response_header
58
+
59
+ headers['LOCATION'] = URI.parse(headers['LOCATION']).path if headers.include?('LOCATION')
60
+
61
+ headers
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ require 'excon'
2
+
3
+ module Rack
4
+ module Client
5
+ module Handler
6
+ class Excon
7
+ include DualBand
8
+
9
+ def initialize(url)
10
+ @uri = URI.parse(url)
11
+ end
12
+
13
+ def async_call(env)
14
+ raise("Asynchronous API is not supported for EmHttp Handler") unless block_given?
15
+ end
16
+
17
+ def sync_call(env)
18
+ request = Rack::Request.new(env)
19
+
20
+ body = case request.body
21
+ when StringIO then request.body.string
22
+ when IO then request.body.read
23
+ when Array then request.body.to_s
24
+ when String then request.body
25
+ end
26
+
27
+ response = parse connection.request(:method => request.request_method,
28
+ :path => request.path,
29
+ :headers => Headers.from(env).to_http,
30
+ :body => body)
31
+
32
+ response.finish
33
+ end
34
+
35
+ def parse(excon_response)
36
+ body = excon_response.body.empty? ? [] : StringIO.new(excon_response.body)
37
+ Response.new(excon_response.status, Headers.new(excon_response.headers).to_http, body)
38
+ end
39
+
40
+ def connection
41
+ connection_table[self] ||= ::Excon.new(@uri.to_s)
42
+ end
43
+
44
+ def connection_table
45
+ Thread.current[:_rack_client_excon_connections] ||= {}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end