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,133 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class MetaStore
5
+
6
+ def lookup(request, entity_store)
7
+ key = cache_key(request)
8
+ entries = read(key)
9
+
10
+ # bail out if we have nothing cached
11
+ return nil if entries.empty?
12
+
13
+ # find a cached entry that matches the request.
14
+ env = request.env
15
+ match = entries.detect{|req,res| requests_match?(res['Vary'], env, req)}
16
+ return nil if match.nil?
17
+
18
+ req, res = match
19
+ if body = entity_store.open(res['X-Content-Digest'])
20
+ restore_response(res, body)
21
+ else
22
+ # TODO the metastore referenced an entity that doesn't exist in
23
+ # the entitystore. we definitely want to return nil but we should
24
+ # also purge the entry from the meta-store when this is detected.
25
+ end
26
+ end
27
+
28
+ # Write a cache entry to the store under the given key. Existing
29
+ # entries are read and any that match the response are removed.
30
+ # This method calls #write with the new list of cache entries.
31
+ def store(request, response, entity_store)
32
+ key = cache_key(request)
33
+ stored_env = persist_request(request)
34
+
35
+ # write the response body to the entity store if this is the
36
+ # original response.
37
+ if response.headers['X-Content-Digest'].nil?
38
+ digest, size = entity_store.write(response.body)
39
+ response.headers['X-Content-Digest'] = digest
40
+ response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
41
+ response.body = entity_store.open(digest)
42
+ end
43
+
44
+ # read existing cache entries, remove non-varying, and add this one to
45
+ # the list
46
+ vary = response.vary
47
+ entries =
48
+ read(key).reject do |env,res|
49
+ (vary == res['Vary']) &&
50
+ requests_match?(vary, env, stored_env)
51
+ end
52
+
53
+ headers = persist_response(response)
54
+ headers.delete 'Age'
55
+
56
+ entries.unshift [stored_env, headers]
57
+ write key, entries
58
+ key
59
+ end
60
+
61
+ def cache_key(request)
62
+ keygen = request.env['rack-client-cache.cache_key'] || Key
63
+ keygen.call(request)
64
+ end
65
+
66
+ # Extract the environment Hash from +request+ while making any
67
+ # necessary modifications in preparation for persistence. The Hash
68
+ # returned must be marshalable.
69
+ def persist_request(request)
70
+ env = request.env.dup
71
+ env.reject! { |key,val| key =~ /[^0-9A-Z_]/ }
72
+ env
73
+ end
74
+
75
+ def persist_response(response)
76
+ hash = response.headers.to_hash
77
+ hash['X-Status'] = response.status.to_s
78
+ hash
79
+ end
80
+
81
+ # Converts a stored response hash into a Response object. The caller
82
+ # is responsible for loading and passing the body if needed.
83
+ def restore_response(hash, body=nil)
84
+ status = hash.delete('X-Status').to_i
85
+ Rack::Client::Cache::Response.new(status, hash, body)
86
+ end
87
+
88
+ # Determine whether the two environment hashes are non-varying based on
89
+ # the vary response header value provided.
90
+ def requests_match?(vary, env1, env2)
91
+ return true if vary.nil? || vary == ''
92
+ vary.split(/[\s,]+/).all? do |header|
93
+ key = "HTTP_#{header.upcase.tr('-', '_')}"
94
+ env1[key] == env2[key]
95
+ end
96
+ end
97
+
98
+ class Heap < MetaStore
99
+ def initialize(hash={})
100
+ @hash = hash
101
+ end
102
+
103
+ def read(key)
104
+ @hash.fetch(key, []).collect do |req,res|
105
+ [req.dup, res.dup]
106
+ end
107
+ end
108
+
109
+ def write(key, entries)
110
+ @hash[key] = entries
111
+ end
112
+
113
+ def purge(key)
114
+ @hash.delete(key)
115
+ nil
116
+ end
117
+
118
+ def to_hash
119
+ @hash
120
+ end
121
+
122
+ def self.resolve(uri)
123
+ new
124
+ end
125
+ end
126
+
127
+ HEAP = Heap
128
+ MEM = HEAP
129
+
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,147 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ # vendored, originally from rack-cache.
5
+ module Options
6
+ def self.option_accessor(key)
7
+ name = option_name(key)
8
+ define_method(key) { || options[name] }
9
+ define_method("#{key}=") { |value| options[name] = value }
10
+ define_method("#{key}?") { || !! options[name] }
11
+ end
12
+
13
+ def option_name(key)
14
+ case key
15
+ when Symbol ; "rack-client-cache.#{key}"
16
+ when String ; key
17
+ else raise ArgumentError
18
+ end
19
+ end
20
+ module_function :option_name
21
+
22
+ # Enable verbose trace logging. This option is currently enabled by
23
+ # default but is likely to be disabled in a future release.
24
+ option_accessor :verbose
25
+
26
+ # The storage resolver. Defaults to the Rack::Cache.storage singleton instance
27
+ # of Rack::Cache::Storage. This object is responsible for resolving metastore
28
+ # and entitystore URIs to an implementation instances.
29
+ option_accessor :storage
30
+
31
+ # A URI specifying the meta-store implementation that should be used to store
32
+ # request/response meta information. The following URIs schemes are
33
+ # supported:
34
+ #
35
+ # * heap:/
36
+ # * file:/absolute/path or file:relative/path
37
+ # * memcached://localhost:11211[/namespace]
38
+ #
39
+ # If no meta store is specified the 'heap:/' store is assumed. This
40
+ # implementation has significant draw-backs so explicit configuration is
41
+ # recommended.
42
+ option_accessor :metastore
43
+
44
+ # A custom cache key generator, which can be anything that responds to :call.
45
+ # By default, this is the Rack::Cache::Key class, but you can implement your
46
+ # own generator. A cache key generator gets passed a request and generates the
47
+ # appropriate cache key.
48
+ #
49
+ # In addition to setting the generator to an object, you can just pass a block
50
+ # instead, which will act as the cache key generator:
51
+ #
52
+ # set :cache_key do |request|
53
+ # request.fullpath.replace(/\//, '-')
54
+ # end
55
+ option_accessor :cache_key
56
+
57
+ # A URI specifying the entity-store implementation that should be used to
58
+ # store response bodies. See the metastore option for information on
59
+ # supported URI schemes.
60
+ #
61
+ # If no entity store is specified the 'heap:/' store is assumed. This
62
+ # implementation has significant draw-backs so explicit configuration is
63
+ # recommended.
64
+ option_accessor :entitystore
65
+
66
+ # The number of seconds that a cache entry should be considered
67
+ # "fresh" when no explicit freshness information is provided in
68
+ # a response. Explicit Cache-Control or Expires headers
69
+ # override this value.
70
+ #
71
+ # Default: 0
72
+ option_accessor :default_ttl
73
+
74
+ # Set of request headers that trigger "private" cache-control behavior
75
+ # on responses that don't explicitly state whether the response is
76
+ # public or private via a Cache-Control directive. Applications that use
77
+ # cookies for authorization may need to add the 'Cookie' header to this
78
+ # list.
79
+ #
80
+ # Default: ['Authorization', 'Cookie']
81
+ option_accessor :private_headers
82
+
83
+ # Specifies whether the client can force a cache reload by including a
84
+ # Cache-Control "no-cache" directive in the request. This is enabled by
85
+ # default for compliance with RFC 2616.
86
+ option_accessor :allow_reload
87
+
88
+ # Specifies whether the client can force a cache revalidate by including
89
+ # a Cache-Control "max-age=0" directive in the request. This is enabled by
90
+ # default for compliance with RFC 2616.
91
+ option_accessor :allow_revalidate
92
+
93
+ # The underlying options Hash. During initialization (or outside of a
94
+ # request), this is a default values Hash. During a request, this is the
95
+ # Rack environment Hash. The default values Hash is merged in underneath
96
+ # the Rack environment before each request is processed.
97
+ def options
98
+ @env || @default_options
99
+ end
100
+
101
+ # Set multiple options.
102
+ def options=(hash={})
103
+ hash.each { |key,value| write_option(key, value) }
104
+ end
105
+
106
+ # Set an option. When +option+ is a Symbol, it is set in the Rack
107
+ # Environment as "rack-cache.option". When +option+ is a String, it
108
+ # exactly as specified. The +option+ argument may also be a Hash in
109
+ # which case each key/value pair is merged into the environment as if
110
+ # the #set method were called on each.
111
+ def set(option, value=self, &block)
112
+ if block_given?
113
+ write_option option, block
114
+ elsif value == self
115
+ self.options = option.to_hash
116
+ else
117
+ write_option option, value
118
+ end
119
+ end
120
+
121
+ private
122
+ def initialize_options(options={})
123
+ @default_options = {
124
+ 'rack-client-cache.cache_key' => Key,
125
+ 'rack-client-cache.verbose' => true,
126
+ 'rack-client-cache.storage' => Storage.instance,
127
+ 'rack-client-cache.metastore' => 'heap:/',
128
+ 'rack-client-cache.entitystore' => 'heap:/',
129
+ 'rack-client-cache.default_ttl' => 0,
130
+ 'rack-client-cache.private_headers' => ['Authorization', 'Cookie'],
131
+ 'rack-client-cache.allow_reload' => false,
132
+ 'rack-client-cache.allow_revalidate' => false
133
+ }
134
+ self.options = options
135
+ end
136
+
137
+ def read_option(key)
138
+ options[option_name(key)]
139
+ end
140
+
141
+ def write_option(key, value)
142
+ options[option_name(key)] = value
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,46 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class Request < Rack::Request
5
+ include Options
6
+
7
+ def cacheable?
8
+ request_method == 'GET'
9
+ end
10
+
11
+ def env
12
+ return super if @calculating_headers
13
+ cache_control_headers.merge(super)
14
+ end
15
+
16
+ def cache_control_headers
17
+ @calculating_headers = true
18
+ return {} unless cacheable?
19
+ entry = metastore.lookup(self, entitystore)
20
+
21
+ if entry
22
+ headers_for(entry)
23
+ else
24
+ {}
25
+ end
26
+ ensure
27
+ @calculating_headers = nil
28
+ end
29
+
30
+ def headers_for(response)
31
+ return 'HTTP_If-None-Match' => response.etag
32
+ end
33
+
34
+ def metastore
35
+ uri = options['rack-client-cache.metastore']
36
+ storage.resolve_metastore_uri(uri)
37
+ end
38
+
39
+ def entitystore
40
+ uri = options['rack-client-cache.entitystore']
41
+ storage.resolve_entitystore_uri(uri)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class Response < Rack::Client::Response
5
+ include Rack::Response::Helpers
6
+
7
+ def not_modified?
8
+ status == 304
9
+ end
10
+
11
+ alias_method :finish, :to_a
12
+
13
+ # Status codes of responses that MAY be stored by a cache or used in reply
14
+ # to a subsequent request.
15
+ #
16
+ # http://tools.ietf.org/html/rfc2616#section-13.4
17
+ CACHEABLE_RESPONSE_CODES = [
18
+ 200, # OK
19
+ 203, # Non-Authoritative Information
20
+ 300, # Multiple Choices
21
+ 301, # Moved Permanently
22
+ 302, # Found
23
+ 404, # Not Found
24
+ 410 # Gone
25
+ ].to_set
26
+
27
+ # A Hash of name=value pairs that correspond to the Cache-Control header.
28
+ # Valueless parameters (e.g., must-revalidate, no-store) have a Hash value
29
+ # of true. This method always returns a Hash, empty if no Cache-Control
30
+ # header is present.
31
+ def cache_control
32
+ @cache_control ||= CacheControl.new(headers['Cache-Control'])
33
+ end
34
+
35
+ def cacheable?
36
+ return false unless CACHEABLE_RESPONSE_CODES.include?(status)
37
+ return false if cache_control.no_store? || cache_control.private?
38
+ validateable? || fresh?
39
+ end
40
+
41
+ # The literal value of ETag HTTP header or nil if no ETag is specified.
42
+ def etag
43
+ headers['ETag']
44
+ end
45
+
46
+ def validateable?
47
+ headers.key?('Last-Modified') || headers.key?('ETag')
48
+ end
49
+
50
+ # The literal value of the Vary header, or nil when no header is present.
51
+ def vary
52
+ headers['Vary']
53
+ end
54
+
55
+ # Does the response include a Vary header?
56
+ def vary?
57
+ ! vary.nil?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ module Rack
2
+ module Client
3
+ module Cache
4
+ class Storage
5
+ def initialize
6
+ @metastores = {}
7
+ @entitystores = {}
8
+ end
9
+
10
+ def resolve_metastore_uri(uri)
11
+ @metastores[uri.to_s] ||= create_store(MetaStore, uri)
12
+ end
13
+
14
+ def resolve_entitystore_uri(uri)
15
+ @entitystores[uri.to_s] ||= create_store(EntityStore, uri)
16
+ end
17
+
18
+ def create_store(type, uri)
19
+ if uri.respond_to?(:scheme) || uri.respond_to?(:to_str)
20
+ uri = URI.parse(uri) unless uri.respond_to?(:scheme)
21
+ if type.const_defined?(uri.scheme.upcase)
22
+ klass = type.const_get(uri.scheme.upcase)
23
+ klass.resolve(uri)
24
+ else
25
+ fail "Unknown storage provider: #{uri.to_s}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def clear
31
+ @metastores.clear
32
+ @entitystores.clear
33
+ nil
34
+ end
35
+
36
+ @@singleton_instance = new
37
+ def self.instance
38
+ @@singleton_instance
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ module Rack
2
+ module Client
3
+ module CookieJar
4
+ def self.new(app, &b)
5
+ Context.new(app, &b)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'rack/client/cookie_jar/options'
12
+ require 'rack/client/cookie_jar/cookie'
13
+ require 'rack/client/cookie_jar/cookiestore'
14
+ require 'rack/client/cookie_jar/context'
15
+ require 'rack/client/cookie_jar/request'
16
+ require 'rack/client/cookie_jar/response'
17
+ require 'rack/client/cookie_jar/storage'