openlogic-resourceful 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +45 -0
- data/MIT-LICENSE +21 -0
- data/Manifest +46 -0
- data/README.markdown +92 -0
- data/Rakefile +91 -0
- data/lib/resourceful.rb +27 -0
- data/lib/resourceful/abstract_form_data.rb +30 -0
- data/lib/resourceful/authentication_manager.rb +107 -0
- data/lib/resourceful/cache_manager.rb +242 -0
- data/lib/resourceful/exceptions.rb +34 -0
- data/lib/resourceful/header.rb +355 -0
- data/lib/resourceful/http_accessor.rb +103 -0
- data/lib/resourceful/memcache_cache_manager.rb +75 -0
- data/lib/resourceful/multipart_form_data.rb +46 -0
- data/lib/resourceful/net_http_adapter.rb +84 -0
- data/lib/resourceful/promiscuous_basic_authenticator.rb +18 -0
- data/lib/resourceful/request.rb +235 -0
- data/lib/resourceful/resource.rb +179 -0
- data/lib/resourceful/response.rb +221 -0
- data/lib/resourceful/simple.rb +36 -0
- data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
- data/lib/resourceful/urlencoded_form_data.rb +19 -0
- data/lib/resourceful/util.rb +6 -0
- data/openlogic-resourceful.gemspec +51 -0
- data/resourceful.gemspec +51 -0
- data/spec/acceptance/authorization_spec.rb +16 -0
- data/spec/acceptance/caching_spec.rb +190 -0
- data/spec/acceptance/header_spec.rb +24 -0
- data/spec/acceptance/redirecting_spec.rb +12 -0
- data/spec/acceptance/resource_spec.rb +84 -0
- data/spec/acceptance/resourceful_spec.rb +56 -0
- data/spec/acceptance_shared_specs.rb +44 -0
- data/spec/caching_spec.rb +89 -0
- data/spec/old_acceptance_specs.rb +378 -0
- data/spec/resourceful/header_spec.rb +153 -0
- data/spec/resourceful/http_accessor_spec.rb +56 -0
- data/spec/resourceful/multipart_form_data_spec.rb +84 -0
- data/spec/resourceful/promiscuous_basic_authenticator_spec.rb +30 -0
- data/spec/resourceful/resource_spec.rb +20 -0
- data/spec/resourceful/response_spec.rb +51 -0
- data/spec/resourceful/urlencoded_form_data_spec.rb +64 -0
- data/spec/resourceful_spec.rb +79 -0
- data/spec/simple_sinatra_server.rb +74 -0
- data/spec/simple_sinatra_server_spec.rb +98 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +31 -0
- metadata +192 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'resourceful/header'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
module Resourceful
|
5
|
+
|
6
|
+
class AbstractCacheManager
|
7
|
+
def initialize
|
8
|
+
raise NotImplementedError,
|
9
|
+
"Use one of CacheManager's child classes instead. Try NullCacheManager if you don't want any caching at all."
|
10
|
+
end
|
11
|
+
|
12
|
+
# Finds a previously cached response to the provided request. The
|
13
|
+
# response returned may be stale.
|
14
|
+
#
|
15
|
+
# @param [Resourceful::Request] request
|
16
|
+
# The request for which we are looking for a response.
|
17
|
+
#
|
18
|
+
# @return [Resourceful::Response]
|
19
|
+
# A (possibly stale) response for the request provided.
|
20
|
+
def lookup(request); end
|
21
|
+
|
22
|
+
# Store a response in the cache.
|
23
|
+
#
|
24
|
+
# This method is smart enough to not store responses that cannot be
|
25
|
+
# cached (Vary: * or Cache-Control: no-cache, private, ...)
|
26
|
+
#
|
27
|
+
# @param request<Resourceful::Request>
|
28
|
+
# The request used to obtain the response. This is needed so the
|
29
|
+
# values from the response's Vary header can be stored.
|
30
|
+
# @param response<Resourceful::Response>
|
31
|
+
# The response to be stored.
|
32
|
+
def store(request, response); end
|
33
|
+
|
34
|
+
# Invalidates a all cached entries for a uri.
|
35
|
+
#
|
36
|
+
# This is used, for example, to invalidate the cache for a resource
|
37
|
+
# that gets POSTed to.
|
38
|
+
#
|
39
|
+
# @param uri<String>
|
40
|
+
# The uri of the resource to be invalidated
|
41
|
+
def invalidate(uri); end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# Returns an alphanumeric hash of a URI
|
46
|
+
def uri_hash(uri)
|
47
|
+
Digest::MD5.hexdigest(uri)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This is the default cache, and does not do any caching. All lookups
|
52
|
+
# result in nil, and all attempts to store a response are a no-op.
|
53
|
+
class NullCacheManager < AbstractCacheManager
|
54
|
+
def initialize; end
|
55
|
+
|
56
|
+
def lookup(request)
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def store(request, response); end
|
61
|
+
end
|
62
|
+
|
63
|
+
# This is a nieve implementation of caching. Unused entries are never
|
64
|
+
# removed, and this may eventually eat up all your memory and cause your
|
65
|
+
# machine to explode.
|
66
|
+
class InMemoryCacheManager < AbstractCacheManager
|
67
|
+
|
68
|
+
def initialize
|
69
|
+
@collection = Hash.new{ |h,k| h[k] = CacheEntryCollection.new}
|
70
|
+
end
|
71
|
+
|
72
|
+
def lookup(request)
|
73
|
+
response = @collection[request.uri.to_s][request]
|
74
|
+
response.authoritative = false if response
|
75
|
+
response
|
76
|
+
end
|
77
|
+
|
78
|
+
def store(request, response)
|
79
|
+
return unless response.cacheable?
|
80
|
+
|
81
|
+
@collection[request.uri.to_s][request] = response
|
82
|
+
end
|
83
|
+
|
84
|
+
def invalidate(uri)
|
85
|
+
@collection.delete(uri)
|
86
|
+
end
|
87
|
+
end # class InMemoryCacheManager
|
88
|
+
|
89
|
+
# Stores cache entries in a directory on the filesystem. Similarly to the
|
90
|
+
# InMemoryCacheManager there are no limits on storage, so this will eventually
|
91
|
+
# eat up all your disk!
|
92
|
+
class FileCacheManager < AbstractCacheManager
|
93
|
+
# Create a new FileCacheManager
|
94
|
+
#
|
95
|
+
# @param [String] location
|
96
|
+
# A directory on the filesystem to store cache entries. This directory
|
97
|
+
# will be created if it doesn't exist
|
98
|
+
def initialize(location="/tmp/resourceful")
|
99
|
+
require 'fileutils'
|
100
|
+
require 'yaml'
|
101
|
+
@dir = FileUtils.mkdir_p(location)
|
102
|
+
end
|
103
|
+
|
104
|
+
def lookup(request)
|
105
|
+
response = cache_entries_for(request)[request]
|
106
|
+
response.authoritative = false if response
|
107
|
+
response
|
108
|
+
end
|
109
|
+
|
110
|
+
def store(request, response)
|
111
|
+
return unless response.cacheable?
|
112
|
+
|
113
|
+
entries = cache_entries_for(request)
|
114
|
+
entries[request] = response
|
115
|
+
File.open(cache_file(request.uri), "w") {|fh| fh.write( YAML.dump(entries) ) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def invalidate(uri);
|
119
|
+
File.unlink(cache_file(uri));
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def cache_entries_for(request)
|
125
|
+
if File.readable?( cache_file(request.uri) )
|
126
|
+
YAML.load_file( cache_file(request.uri) )
|
127
|
+
else
|
128
|
+
Resourceful::CacheEntryCollection.new
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def cache_file(uri)
|
133
|
+
"#{@dir}/#{uri_hash(uri)}"
|
134
|
+
end
|
135
|
+
end # class FileCacheManager
|
136
|
+
|
137
|
+
|
138
|
+
# The collection of cached entries. Nominally all the entry in a
|
139
|
+
# collection of this sort will be for the same resource but that is
|
140
|
+
# not required to be true.
|
141
|
+
class CacheEntryCollection
|
142
|
+
include Enumerable
|
143
|
+
|
144
|
+
def initialize
|
145
|
+
@entries = []
|
146
|
+
end
|
147
|
+
|
148
|
+
# Iterates over the entries. Needed for Enumerable
|
149
|
+
def each(&block)
|
150
|
+
@entries.each(&block)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Looks for an Entry that could fullfil the request. Returns nil if none
|
154
|
+
# was found.
|
155
|
+
#
|
156
|
+
# @param [Resourceful::Request] request
|
157
|
+
# The request to use for the lookup.
|
158
|
+
#
|
159
|
+
# @return [Resourceful::Response]
|
160
|
+
# The cached response for the specified request if one is available.
|
161
|
+
def [](request)
|
162
|
+
entry = find { |entry| entry.valid_for?(request) }
|
163
|
+
entry.response if entry
|
164
|
+
end
|
165
|
+
|
166
|
+
# Saves an entry into the collection. Replaces any existing ones that could
|
167
|
+
# be used with the updated response.
|
168
|
+
#
|
169
|
+
# @param [Resourceful::Request] request
|
170
|
+
# The request that was used to obtain the response
|
171
|
+
# @param [Resourceful::Response] response
|
172
|
+
# The cache_entry generated from response that was obtained.
|
173
|
+
def []=(request, response)
|
174
|
+
@entries.delete_if { |e| e.valid_for?(request) }
|
175
|
+
@entries << CacheEntry.new(request, response)
|
176
|
+
|
177
|
+
response
|
178
|
+
end
|
179
|
+
end # class CacheEntryCollection
|
180
|
+
|
181
|
+
# Represents a previous request and cached response with enough
|
182
|
+
# detail to determine construct a cached response to a matching
|
183
|
+
# request in the future. It also understands what a matching
|
184
|
+
# request means.
|
185
|
+
class CacheEntry
|
186
|
+
# request_vary_headers is a HttpHeader with keys from the Vary
|
187
|
+
# header of the response, plus the values from the matching fields
|
188
|
+
# in the request
|
189
|
+
attr_reader :request_vary_headers
|
190
|
+
|
191
|
+
# The time at which the client believes the request was made.
|
192
|
+
attr_reader :request_time
|
193
|
+
|
194
|
+
# The URI of the request
|
195
|
+
attr_reader :request_uri
|
196
|
+
|
197
|
+
# The response to that we are caching
|
198
|
+
attr_reader :response
|
199
|
+
|
200
|
+
# @param [Resourceful::Request] request
|
201
|
+
# The request whose response we are storing in the cache.
|
202
|
+
# @param response<Resourceful::Response>
|
203
|
+
# The Response obhect to be stored.
|
204
|
+
def initialize(request, response)
|
205
|
+
@request_uri = request.uri
|
206
|
+
@request_time = request.request_time
|
207
|
+
@request_vary_headers = select_request_headers(request, response)
|
208
|
+
@response = response
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns true if this entry may be used to fullfil the given request,
|
212
|
+
# according to the vary headers.
|
213
|
+
#
|
214
|
+
# @param request<Resourceful::Request>
|
215
|
+
# The request to do the lookup on.
|
216
|
+
def valid_for?(request)
|
217
|
+
request.uri == @request_uri and
|
218
|
+
@request_vary_headers.all? {|key, value|
|
219
|
+
request.header[key] == value
|
220
|
+
}
|
221
|
+
end
|
222
|
+
|
223
|
+
# Selects the headers from the request named by the response's Vary header
|
224
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.6
|
225
|
+
#
|
226
|
+
# @param [Resourceful::Request] request
|
227
|
+
# The request used to obtain the response.
|
228
|
+
# @param [Resourceful::Response] response
|
229
|
+
# The response obtained from the request.
|
230
|
+
def select_request_headers(request, response)
|
231
|
+
header = Resourceful::Header.new
|
232
|
+
|
233
|
+
response.header['Vary'].each do |name|
|
234
|
+
header[name] = request.header[name] if request.header[name]
|
235
|
+
end if response.header['Vary']
|
236
|
+
|
237
|
+
header
|
238
|
+
end
|
239
|
+
|
240
|
+
end # class CacheEntry
|
241
|
+
|
242
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
module Resourceful
|
3
|
+
|
4
|
+
# This exception used to indicate that the request did not succeed.
|
5
|
+
# The HTTP response is included so that the appropriate actions can
|
6
|
+
# be taken based on the details of that response
|
7
|
+
class UnsuccessfulHttpRequestError < Exception
|
8
|
+
attr_reader :http_response, :http_request
|
9
|
+
|
10
|
+
# Initialize new error from the HTTP request and response attributes.
|
11
|
+
def initialize(http_request, http_response)
|
12
|
+
super("#{http_request.method} request to <#{http_request.uri}> failed with code #{http_response.code}")
|
13
|
+
@http_request = http_request
|
14
|
+
@http_response = http_response
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MalformedServerResponse < UnsuccessfulHttpRequestError
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Exception indicating that the server used a content coding scheme
|
23
|
+
# that Resourceful is unable to handle.
|
24
|
+
class UnsupportedContentCoding < Exception
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when a body is supplied, but not a content-type header
|
28
|
+
class MissingContentType < ArgumentError
|
29
|
+
def initialize
|
30
|
+
super("A Content-Type must be specified when an entity-body is supplied.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'options'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
# Represents the header fields of an HTTP message. To access a field
|
5
|
+
# you can use `#[]` and `#[]=`. For example, to get the content type
|
6
|
+
# of a response you can do
|
7
|
+
#
|
8
|
+
# response.header['Content-Type'] # => "application/xml"
|
9
|
+
#
|
10
|
+
# Lookups and modifications done in this way are case insensitive, so
|
11
|
+
# 'Content-Type', 'content-type' and :content_type are all equivalent.
|
12
|
+
#
|
13
|
+
# Multi-valued fields
|
14
|
+
# -------------------
|
15
|
+
#
|
16
|
+
# Multi-value fields (e.g. Accept) are always returned as an Array
|
17
|
+
# regardless of the number of values, if the field is present.
|
18
|
+
# Single-value fields (e.g. Content-Type) are always returned as
|
19
|
+
# strings. The multi/single valueness of a header field is determined
|
20
|
+
# by the way it is defined in the HTTP spec. Unknown fields are
|
21
|
+
# treated as multi-valued.
|
22
|
+
#
|
23
|
+
# (This behavior is new in 0.6 and may be slightly incompatible with
|
24
|
+
# the way previous versions worked in some situations.)
|
25
|
+
#
|
26
|
+
# For example
|
27
|
+
#
|
28
|
+
# h = Resourceful::Header.new
|
29
|
+
# h['Accept'] = "application/xml"
|
30
|
+
# h['Accept'] # => ["application/xml"]
|
31
|
+
#
|
32
|
+
module Resourceful
|
33
|
+
class Header
|
34
|
+
include Enumerable
|
35
|
+
|
36
|
+
def initialize(hash={})
|
37
|
+
@raw_fields = {}
|
38
|
+
hash.each { |k, v| self[k] = v }
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_hash
|
42
|
+
@raw_fields.dup
|
43
|
+
end
|
44
|
+
|
45
|
+
def [](k)
|
46
|
+
field_def(k).get_from(@raw_fields)
|
47
|
+
end
|
48
|
+
|
49
|
+
def []=(k, v)
|
50
|
+
field_def(k).set_to(v, @raw_fields)
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(k)
|
54
|
+
field_def(k).delete(@raw_fields)
|
55
|
+
end
|
56
|
+
|
57
|
+
def has_key?(k)
|
58
|
+
field_def(k).exists_in?(@raw_fields)
|
59
|
+
end
|
60
|
+
alias has_field? has_key?
|
61
|
+
|
62
|
+
def each(&blk)
|
63
|
+
@raw_fields.each(&blk)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Iterates through the fields with values provided as message
|
67
|
+
# ready strings.
|
68
|
+
def each_field(&blk)
|
69
|
+
each do |k,v|
|
70
|
+
str_v = if field_def(k).multivalued?
|
71
|
+
v.join(', ')
|
72
|
+
else
|
73
|
+
v.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
yield k, str_v
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def merge!(another)
|
81
|
+
another.each do |k,v|
|
82
|
+
self[k] = v
|
83
|
+
end
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete(k)
|
88
|
+
@raw_fields.delete(field_def(k).name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def merge(another)
|
92
|
+
self.class.new(self).merge!(another)
|
93
|
+
end
|
94
|
+
|
95
|
+
def reverse_merge(another)
|
96
|
+
self.class.new(another).merge!(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
def dup
|
100
|
+
self.class.new(@raw_fields.dup)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Class to handle the details of each type of field.
|
104
|
+
class FieldDesc
|
105
|
+
include Comparable
|
106
|
+
|
107
|
+
##
|
108
|
+
attr_reader :name
|
109
|
+
|
110
|
+
# Create a new header field descriptor.
|
111
|
+
#
|
112
|
+
# @param [String] name The canonical name of this field.
|
113
|
+
#
|
114
|
+
# @param [Hash] options hash containing extra information about
|
115
|
+
# this header fields. Valid keys are:
|
116
|
+
#
|
117
|
+
# `:multivalued`
|
118
|
+
# `:multivalue`
|
119
|
+
# `:repeatable`
|
120
|
+
# : Values of this field are comma separated list of values.
|
121
|
+
# (n#VALUE per HTTP spec.) Default: false
|
122
|
+
#
|
123
|
+
# `:hop_by_hop`
|
124
|
+
# : True if the header is a hop-by-hop header. Default: false
|
125
|
+
#
|
126
|
+
# `:modifiable`
|
127
|
+
# : False if the header should not be modified by intermediates or caches. Default: true
|
128
|
+
#
|
129
|
+
def initialize(name, options = {})
|
130
|
+
@name = name
|
131
|
+
options = Options.for(options).validate(:repeatable, :hop_by_hop, :modifiable)
|
132
|
+
|
133
|
+
@repeatable = options.getopt([:repeatable, :multivalue, :multivalued]) || false
|
134
|
+
@hop_by_hop = options.getopt(:hop_by_hop) || false
|
135
|
+
@modifiable = options.getopt(:modifiable, true)
|
136
|
+
end
|
137
|
+
|
138
|
+
def repeatable?
|
139
|
+
@repeatable
|
140
|
+
end
|
141
|
+
alias multivalued? repeatable?
|
142
|
+
|
143
|
+
def hop_by_hop?
|
144
|
+
@hop_by_hop
|
145
|
+
end
|
146
|
+
|
147
|
+
def modifiable?
|
148
|
+
@modifiable
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_from(raw_fields_hash)
|
152
|
+
raw_fields_hash[name]
|
153
|
+
end
|
154
|
+
|
155
|
+
def set_to(value, raw_fields_hash)
|
156
|
+
raw_fields_hash[name] = if multivalued?
|
157
|
+
Array(value).map{|v| v.split(/,\s*/)}.flatten
|
158
|
+
elsif value.kind_of?(Array)
|
159
|
+
raise ArgumentError, "#{name} field may only have one value" if value.size > 1
|
160
|
+
value.first
|
161
|
+
else
|
162
|
+
value
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def delete(raw_fields_hash)
|
167
|
+
raw_fields_hash.delete(name)
|
168
|
+
end
|
169
|
+
|
170
|
+
def exists_in?(raw_fields_hash)
|
171
|
+
raw_fields_hash.has_key?(name)
|
172
|
+
end
|
173
|
+
|
174
|
+
def <=>(another)
|
175
|
+
name <=> another.name
|
176
|
+
end
|
177
|
+
|
178
|
+
def ==(another)
|
179
|
+
name_pattern === another.to_s
|
180
|
+
end
|
181
|
+
alias eql? ==
|
182
|
+
|
183
|
+
def ===(another)
|
184
|
+
if another.kind_of?(FieldDesc)
|
185
|
+
self == another
|
186
|
+
else
|
187
|
+
name_pattern === another
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def name_pattern
|
192
|
+
@name_pattern ||= Regexp.new('^' + name.gsub('-', '[_-]') + '$', Regexp::IGNORECASE)
|
193
|
+
end
|
194
|
+
|
195
|
+
def methodized_name
|
196
|
+
@methodized_name ||= name.downcase.gsub('-', '_')
|
197
|
+
end
|
198
|
+
|
199
|
+
def constantized_name
|
200
|
+
@constantized_name ||= name.upcase.gsub('-', '_')
|
201
|
+
end
|
202
|
+
|
203
|
+
alias to_s name
|
204
|
+
|
205
|
+
def accessor_module
|
206
|
+
@accessor_module ||= begin
|
207
|
+
Module.new.tap{|m| m.module_eval(<<-RUBY)}
|
208
|
+
#{constantized_name} = '#{name}'
|
209
|
+
|
210
|
+
def #{methodized_name} # def accept
|
211
|
+
self[#{constantized_name}] # self[ACCEPT]
|
212
|
+
end # end
|
213
|
+
|
214
|
+
def #{methodized_name}=(val) # def accept=(val)
|
215
|
+
self[#{constantized_name}] = val # self[ACCEPT] = val
|
216
|
+
end # end
|
217
|
+
RUBY
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def hash
|
222
|
+
@name.hash
|
223
|
+
end
|
224
|
+
|
225
|
+
# Yields each commonly used lookup key for this header field.
|
226
|
+
def lookup_keys(&blk)
|
227
|
+
yield name
|
228
|
+
yield name.upcase
|
229
|
+
yield name.downcase
|
230
|
+
yield methodized_name
|
231
|
+
yield methodized_name.to_sym
|
232
|
+
yield constantized_name
|
233
|
+
yield constantized_name.to_sym
|
234
|
+
end
|
235
|
+
end # FieldDesc
|
236
|
+
|
237
|
+
@@known_fields = Set.new
|
238
|
+
@@known_fields_lookup = Hash.new
|
239
|
+
|
240
|
+
# Declares a common header field. Header fields do not have to be
|
241
|
+
# defined this way but accessing them is easier, safer and faster
|
242
|
+
# if you do. Declaring a field does the following things:
|
243
|
+
#
|
244
|
+
# * defines accessor methods (e.g. `#content_type` and
|
245
|
+
# `#content_type=`) on `Header`
|
246
|
+
#
|
247
|
+
# * defines constant that can be used to reference there field
|
248
|
+
# name (e.g. `some_header[Header::CONTENT_TYPE]`)
|
249
|
+
#
|
250
|
+
# * includes the field in the appropriate *_fields groups (e.g. `Header.non_modifiable_fields`)
|
251
|
+
#
|
252
|
+
# * provides improved multiple value parsing
|
253
|
+
#
|
254
|
+
# Create a new header field descriptor.
|
255
|
+
#
|
256
|
+
# @param [String] name The canonical name of this field.
|
257
|
+
#
|
258
|
+
# @param [Hash] options hash containing extra information about
|
259
|
+
# this header fields. Valid keys are:
|
260
|
+
#
|
261
|
+
# `:multivalued`
|
262
|
+
# `:multivalue`
|
263
|
+
# `:repeatable`
|
264
|
+
# : Values of this field are comma separated list of values.
|
265
|
+
# (n#VALUE per HTTP spec.) Default: false
|
266
|
+
#
|
267
|
+
# `:hop_by_hop`
|
268
|
+
# : True if the header is a hop-by-hop header. Default: false
|
269
|
+
#
|
270
|
+
# `:modifiable`
|
271
|
+
# : False if the header should not be modified by intermediates or caches. Default: true
|
272
|
+
#
|
273
|
+
def self.header_field(name, options = {})
|
274
|
+
hfd = FieldDesc.new(name, options)
|
275
|
+
|
276
|
+
@@known_fields << hfd
|
277
|
+
hfd.lookup_keys do |a_key|
|
278
|
+
@@known_fields_lookup[a_key] = hfd
|
279
|
+
end
|
280
|
+
|
281
|
+
include(hfd.accessor_module)
|
282
|
+
end
|
283
|
+
|
284
|
+
def self.hop_by_hop_fields
|
285
|
+
@@known_fields.select{|hfd| hfd.hop_by_hop?}
|
286
|
+
end
|
287
|
+
|
288
|
+
def self.non_modifiable_fields
|
289
|
+
@@known_fields.reject{|hfd| hfd.modifiable?}
|
290
|
+
end
|
291
|
+
|
292
|
+
protected
|
293
|
+
|
294
|
+
# ---
|
295
|
+
#
|
296
|
+
# We have to fall back on a slow iteration to find the header
|
297
|
+
# field some times because field names are
|
298
|
+
def field_def(name)
|
299
|
+
@@known_fields_lookup[name] || # the fast way
|
300
|
+
@@known_fields.find{|hfd| hfd === name} || # the slow way
|
301
|
+
FieldDesc.new(name.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-'), :repeatable => true) # make up as we go
|
302
|
+
end
|
303
|
+
|
304
|
+
header_field('Accept', :repeatable => true)
|
305
|
+
header_field('Accept-Charset', :repeatable => true)
|
306
|
+
header_field('Accept-Encoding', :repeatable => true)
|
307
|
+
header_field('Accept-Language', :repeatable => true)
|
308
|
+
header_field('Accept-Ranges', :repeatable => true)
|
309
|
+
header_field('Age')
|
310
|
+
header_field('Allow', :repeatable => true)
|
311
|
+
header_field('Authorization', :repeatable => true)
|
312
|
+
header_field('Cache-Control', :repeatable => true)
|
313
|
+
header_field('Connection', :hop_by_hop => true)
|
314
|
+
header_field('Content-Encoding', :repeatable => true)
|
315
|
+
header_field('Content-Language', :repeatable => true)
|
316
|
+
header_field('Content-Length')
|
317
|
+
header_field('Content-Location', :modifiable => false)
|
318
|
+
header_field('Content-MD5', :modifiable => false)
|
319
|
+
header_field('Content-Range')
|
320
|
+
header_field('Content-Type')
|
321
|
+
header_field('Date')
|
322
|
+
header_field('ETag', :modifiable => false)
|
323
|
+
header_field('Expect', :repeatable => true)
|
324
|
+
header_field('Expires', :modifiable => false)
|
325
|
+
header_field('From')
|
326
|
+
header_field('Host')
|
327
|
+
header_field('If-Match', :repeatable => true)
|
328
|
+
header_field('If-Modified-Since')
|
329
|
+
header_field('If-None-Match', :repeatable => true)
|
330
|
+
header_field('If-Range')
|
331
|
+
header_field('If-Unmodified-Since')
|
332
|
+
header_field('Keep-Alive', :hop_by_hop => true)
|
333
|
+
header_field('Last-Modified', :modifiable => false)
|
334
|
+
header_field('Location')
|
335
|
+
header_field('Max-Forwards')
|
336
|
+
header_field('Pragma', :repeatable => true)
|
337
|
+
header_field('Proxy-Authenticate', :hop_by_hop => true)
|
338
|
+
header_field('Proxy-Authorization', :hop_by_hop => true)
|
339
|
+
header_field('Range')
|
340
|
+
header_field('Referer')
|
341
|
+
header_field('Retry-After')
|
342
|
+
header_field('Server')
|
343
|
+
header_field('TE', :repeatable => true, :hop_by_hop => true)
|
344
|
+
header_field('Trailer', :repeatable => true, :hop_by_hop => true)
|
345
|
+
header_field('Transfer-Encoding', :repeatable => true, :hop_by_hop => true)
|
346
|
+
header_field('Upgrade', :repeatable => true, :hop_by_hop => true)
|
347
|
+
header_field('User-Agent')
|
348
|
+
header_field('Vary', :repeatable => true)
|
349
|
+
header_field('Via', :repeatable => true)
|
350
|
+
header_field('Warning', :repeatable => true)
|
351
|
+
header_field('WWW-Authenticate', :repeatable => true)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
|