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.
- data/History.txt +2 -2
- data/README.textile +2 -2
- data/Rakefile +11 -5
- data/demo/demo_spec.rb +3 -3
- data/lib/rack/client.rb +29 -25
- data/lib/rack/client/adapter.rb +6 -0
- data/lib/rack/client/adapter/base.rb +57 -0
- data/lib/rack/client/adapter/simple.rb +49 -0
- data/lib/rack/client/auth/abstract/challenge.rb +53 -0
- data/lib/rack/client/auth/basic.rb +57 -0
- data/lib/rack/client/auth/digest/challenge.rb +38 -0
- data/lib/rack/client/auth/digest/md5.rb +78 -0
- data/lib/rack/client/auth/digest/params.rb +10 -0
- data/lib/rack/client/body.rb +12 -0
- data/lib/rack/client/cache.rb +19 -0
- data/lib/rack/client/cache/cachecontrol.rb +195 -0
- data/lib/rack/client/cache/context.rb +95 -0
- data/lib/rack/client/cache/entitystore.rb +77 -0
- data/lib/rack/client/cache/key.rb +51 -0
- data/lib/rack/client/cache/metastore.rb +133 -0
- data/lib/rack/client/cache/options.rb +147 -0
- data/lib/rack/client/cache/request.rb +46 -0
- data/lib/rack/client/cache/response.rb +62 -0
- data/lib/rack/client/cache/storage.rb +43 -0
- data/lib/rack/client/cookie_jar.rb +17 -0
- data/lib/rack/client/cookie_jar/context.rb +59 -0
- data/lib/rack/client/cookie_jar/cookie.rb +59 -0
- data/lib/rack/client/cookie_jar/cookiestore.rb +36 -0
- data/lib/rack/client/cookie_jar/options.rb +43 -0
- data/lib/rack/client/cookie_jar/request.rb +15 -0
- data/lib/rack/client/cookie_jar/response.rb +16 -0
- data/lib/rack/client/cookie_jar/storage.rb +34 -0
- data/lib/rack/client/dual_band.rb +13 -0
- data/lib/rack/client/follow_redirects.rb +47 -20
- data/lib/rack/client/handler.rb +10 -0
- data/lib/rack/client/handler/em-http.rb +66 -0
- data/lib/rack/client/handler/excon.rb +50 -0
- data/lib/rack/client/handler/net_http.rb +85 -0
- data/lib/rack/client/handler/typhoeus.rb +62 -0
- data/lib/rack/client/headers.rb +49 -0
- data/lib/rack/client/parser.rb +18 -0
- data/lib/rack/client/parser/base.rb +25 -0
- data/lib/rack/client/parser/body_collection.rb +50 -0
- data/lib/rack/client/parser/context.rb +15 -0
- data/lib/rack/client/parser/json.rb +54 -0
- data/lib/rack/client/parser/middleware.rb +8 -0
- data/lib/rack/client/parser/request.rb +21 -0
- data/lib/rack/client/parser/response.rb +19 -0
- data/lib/rack/client/parser/yaml.rb +52 -0
- data/lib/rack/client/response.rb +9 -0
- data/lib/rack/client/version.rb +5 -0
- data/spec/apps/example.org.ru +47 -3
- data/spec/auth/basic_spec.rb +69 -0
- data/spec/auth/digest/md5_spec.rb +69 -0
- data/spec/cache_spec.rb +40 -0
- data/spec/cookie_jar_spec.rb +37 -0
- data/spec/endpoint_spec.rb +4 -13
- data/spec/follow_redirect_spec.rb +27 -0
- data/spec/handler/async_api_spec.rb +69 -0
- data/spec/handler/em_http_spec.rb +22 -0
- data/spec/handler/excon_spec.rb +7 -0
- data/spec/handler/net_http_spec.rb +8 -0
- data/spec/handler/sync_api_spec.rb +55 -0
- data/spec/handler/typhoeus_spec.rb +22 -0
- data/spec/middleware_helper.rb +37 -0
- data/spec/middleware_spec.rb +48 -5
- data/spec/parser/json_spec.rb +22 -0
- data/spec/parser/yaml_spec.rb +22 -0
- data/spec/server_helper.rb +72 -0
- data/spec/spec_helper.rb +17 -3
- metadata +86 -31
- data/lib/rack/client/auth.rb +0 -13
- data/lib/rack/client/http.rb +0 -77
- data/spec/auth_spec.rb +0 -22
- data/spec/core_spec.rb +0 -123
- 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'
|