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