openlogic-resourceful 1.2.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 +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
|
+
|