rack-client 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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