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,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'