webmachine 0.1.0 → 0.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/Gemfile +11 -3
- data/README.md +55 -27
- data/lib/webmachine/adapters/mongrel.rb +84 -0
- data/lib/webmachine/adapters/webrick.rb +12 -3
- data/lib/webmachine/adapters.rb +1 -7
- data/lib/webmachine/configuration.rb +30 -0
- data/lib/webmachine/decision/conneg.rb +7 -72
- data/lib/webmachine/decision/flow.rb +13 -11
- data/lib/webmachine/decision/fsm.rb +1 -9
- data/lib/webmachine/decision/helpers.rb +27 -7
- data/lib/webmachine/errors.rb +1 -0
- data/lib/webmachine/headers.rb +12 -3
- data/lib/webmachine/locale/en.yml +2 -2
- data/lib/webmachine/media_type.rb +117 -0
- data/lib/webmachine/resource/callbacks.rb +9 -0
- data/lib/webmachine/streaming.rb +3 -3
- data/lib/webmachine/version.rb +1 -1
- data/lib/webmachine.rb +3 -1
- data/pkg/webmachine-0.1.0/Gemfile +16 -0
- data/pkg/webmachine-0.1.0/Guardfile +11 -0
- data/pkg/webmachine-0.1.0/README.md +90 -0
- data/pkg/webmachine-0.1.0/Rakefile +31 -0
- data/pkg/webmachine-0.1.0/examples/webrick.rb +19 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/adapters/webrick.rb +74 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/adapters.rb +15 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/conneg.rb +304 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/flow.rb +502 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/fsm.rb +79 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/helpers.rb +80 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision.rb +12 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher/route.rb +85 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher.rb +40 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/errors.rb +37 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/headers.rb +16 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/locale/en.yml +28 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/request.rb +56 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource/callbacks.rb +362 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource/encodings.rb +36 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource.rb +48 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/response.rb +49 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/streaming.rb +27 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/translation.rb +11 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/version.rb +4 -0
- data/pkg/webmachine-0.1.0/lib/webmachine.rb +19 -0
- data/pkg/webmachine-0.1.0/spec/spec_helper.rb +13 -0
- data/pkg/webmachine-0.1.0/spec/tests.org +57 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/decision/conneg_spec.rb +152 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/decision/flow_spec.rb +1030 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher/route_spec.rb +109 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher_spec.rb +34 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/headers_spec.rb +19 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/request_spec.rb +24 -0
- data/pkg/webmachine-0.1.0/webmachine.gemspec +44 -0
- data/pkg/webmachine-0.1.0.gem +0 -0
- data/spec/webmachine/configuration_spec.rb +27 -0
- data/spec/webmachine/decision/conneg_spec.rb +18 -11
- data/spec/webmachine/decision/flow_spec.rb +2 -0
- data/spec/webmachine/decision/helpers_spec.rb +105 -0
- data/spec/webmachine/errors_spec.rb +13 -0
- data/spec/webmachine/headers_spec.rb +2 -1
- data/spec/webmachine/media_type_spec.rb +78 -0
- data/webmachine.gemspec +4 -1
- metadata +69 -11
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'webmachine'
|
2
|
+
|
3
|
+
class HelloResource < Webmachine::Resource
|
4
|
+
def last_modified
|
5
|
+
File.mtime(__FILE__)
|
6
|
+
end
|
7
|
+
|
8
|
+
def encodings_provided
|
9
|
+
{ "gzip" => :encode_gzip, "identity" => :encode_identity }
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_html
|
13
|
+
"<html><head><title>Hello from Webmachine</title></head><body>Hello, world!</body></html>"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Webmachine::Dispatcher.add_route([], HelloResource)
|
18
|
+
|
19
|
+
Webmachine.run
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
require 'webmachine/version'
|
3
|
+
require 'webmachine/headers'
|
4
|
+
require 'webmachine/request'
|
5
|
+
require 'webmachine/response'
|
6
|
+
require 'webmachine/dispatcher'
|
7
|
+
|
8
|
+
module Webmachine
|
9
|
+
module Adapters
|
10
|
+
# Connects Webmachine to WEBrick.
|
11
|
+
module WEBrick
|
12
|
+
# Starts the WEBrick adapter
|
13
|
+
def self.run
|
14
|
+
server = Webmachine::Adapters::WEBrick::Server.new :Port => 3000
|
15
|
+
trap("INT"){ server.shutdown }
|
16
|
+
Thread.new { server.start }.join
|
17
|
+
end
|
18
|
+
|
19
|
+
class Server < ::WEBrick::HTTPServer
|
20
|
+
def service(wreq, wres)
|
21
|
+
header = Webmachine::Headers.new
|
22
|
+
wreq.each {|k,v| header[k] = v }
|
23
|
+
request = Webmachine::Request.new(wreq.request_method,
|
24
|
+
wreq.request_uri,
|
25
|
+
header,
|
26
|
+
RequestBody.new(wreq))
|
27
|
+
response = Webmachine::Response.new
|
28
|
+
Webmachine::Dispatcher.dispatch(request, response)
|
29
|
+
wres.status = response.code.to_i
|
30
|
+
response.headers.each do |k,v|
|
31
|
+
wres[k] = v
|
32
|
+
end
|
33
|
+
wres['Server'] = [Webmachine::SERVER_STRING, wres.config[:ServerSoftware]].join(" ")
|
34
|
+
case response.body
|
35
|
+
when String
|
36
|
+
wres.body << response.body
|
37
|
+
when Enumerable
|
38
|
+
response.body.each {|part| wres.body << part }
|
39
|
+
when response.body.respond_to?(:call)
|
40
|
+
wres.body << response.body.call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Wraps the WEBrick request body so that it can be passed to
|
46
|
+
# {Request} while still lazily evaluating the body.
|
47
|
+
class RequestBody
|
48
|
+
def initialize(request)
|
49
|
+
@request = request
|
50
|
+
end
|
51
|
+
|
52
|
+
# Converts the body to a String so you can work with the entire
|
53
|
+
# thing.
|
54
|
+
def to_s
|
55
|
+
@value ? @value.join : @request.body
|
56
|
+
end
|
57
|
+
|
58
|
+
# Iterates over the body in chunks. If the body has previously
|
59
|
+
# been read, this method can be called again and get the same
|
60
|
+
# sequence of chunks.
|
61
|
+
# @yield [chunk]
|
62
|
+
# @yieldparam [String] chunk a chunk of the request body
|
63
|
+
def each
|
64
|
+
if @value
|
65
|
+
@value.each {|chunk| yield chunk }
|
66
|
+
else
|
67
|
+
@value = []
|
68
|
+
@request.body {|chunk| @value << chunk; yield chunk }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'webmachine/adapters/webrick'
|
2
|
+
|
3
|
+
module Webmachine
|
4
|
+
# Contains classes and modules that connect Webmachine to Ruby
|
5
|
+
# application servers.
|
6
|
+
module Adapters
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
# @return [Symbol] the current webserver adapter
|
11
|
+
attr_accessor :adapter
|
12
|
+
end
|
13
|
+
|
14
|
+
self.adapter = :WEBrick
|
15
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
require 'webmachine/translation'
|
2
|
+
|
3
|
+
module Webmachine
|
4
|
+
module Decision
|
5
|
+
# Contains methods concerned with Content Negotiation,
|
6
|
+
# specifically, choosing media types, encodings, character sets
|
7
|
+
# and languages.
|
8
|
+
module Conneg
|
9
|
+
HAS_ENCODING = defined?(::Encoding) # Ruby 1.9 compat
|
10
|
+
|
11
|
+
# Given the 'Accept' header and provided types, chooses an
|
12
|
+
# appropriate media type.
|
13
|
+
# @api private
|
14
|
+
def choose_media_type(provided, header)
|
15
|
+
requested = MediaTypeList.build(header.split(/\s*,\s*/))
|
16
|
+
provided = provided.map do |p| # normalize_provided
|
17
|
+
MediaType.new(*Array(p))
|
18
|
+
end
|
19
|
+
# choose_media_type1
|
20
|
+
chosen = nil
|
21
|
+
requested.each do |_, requested_type|
|
22
|
+
break if chosen = media_match(requested_type, provided)
|
23
|
+
end
|
24
|
+
chosen
|
25
|
+
end
|
26
|
+
|
27
|
+
# Given the 'Accept-Encoding' header and provided encodings, chooses an appropriate
|
28
|
+
# encoding.
|
29
|
+
# @api private
|
30
|
+
def choose_encoding(provided, header)
|
31
|
+
encodings = provided.keys
|
32
|
+
if encoding = do_choose(encodings, header, "identity")
|
33
|
+
response.headers['Content-Encoding'] = encoding unless encoding == 'identity'
|
34
|
+
metadata['Content-Encoding'] = encoding
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Given the 'Accept-Charset' header and provided charsets,
|
39
|
+
# chooses an appropriate charset.
|
40
|
+
# @api private
|
41
|
+
def choose_charset(provided, header)
|
42
|
+
if provided && !provided.empty?
|
43
|
+
charsets = provided.map {|c| c.first }
|
44
|
+
if charset = do_choose(charsets, header, HAS_ENCODING ? Encoding.default_external.name : kcode_charset)
|
45
|
+
metadata['Charset'] = charset
|
46
|
+
end
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Given the 'Accept-Language' header and provided languages,
|
53
|
+
# chooses an appropriate language.
|
54
|
+
# @api private
|
55
|
+
def choose_language(provided, header)
|
56
|
+
if provided && !provided.empty?
|
57
|
+
requested = PriorityList.build(header.split(/\s*,\s*/))
|
58
|
+
star_priority = requested.priority_of("*")
|
59
|
+
any_ok = star_priority && star_priority > 0.0
|
60
|
+
accepted = requested.find do |priority, range|
|
61
|
+
if priority == 0.0
|
62
|
+
provided.delete_if {|tag| language_match(range, tag) }
|
63
|
+
false
|
64
|
+
else
|
65
|
+
provided.any? {|tag| language_match(range, tag) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
chosen = if accepted
|
69
|
+
provided.find {|tag| language_match(accepted.last, tag) }
|
70
|
+
elsif any_ok
|
71
|
+
provided.first
|
72
|
+
end
|
73
|
+
if chosen
|
74
|
+
metadata['Language'] = chosen
|
75
|
+
response.headers['Content-Language'] = chosen
|
76
|
+
end
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# RFC2616, section 14.14:
|
83
|
+
#
|
84
|
+
# A language-range matches a language-tag if it exactly
|
85
|
+
# equals the tag, or if it exactly equals a prefix of the
|
86
|
+
# tag such that the first tag character following the prefix
|
87
|
+
# is "-".
|
88
|
+
def language_match(range, tag)
|
89
|
+
range.downcase == tag.downcase || tag =~ /^#{Regexp.escape(range)}\-/i
|
90
|
+
end
|
91
|
+
|
92
|
+
# Makes an conneg choice based what is accepted and what is
|
93
|
+
# provided.
|
94
|
+
# @api private
|
95
|
+
def do_choose(choices, header, default)
|
96
|
+
choices = choices.dup.map {|s| s.downcase }
|
97
|
+
accepted = PriorityList.build(header.split(/\s*,\s/))
|
98
|
+
default_priority = accepted.priority_of(default)
|
99
|
+
star_priority = accepted.priority_of("*")
|
100
|
+
default_ok = (default_priority.nil? && star_priority != 0.0) || default_priority
|
101
|
+
any_ok = star_priority && star_priority > 0.0
|
102
|
+
chosen = accepted.find do |priority, acceptable|
|
103
|
+
if priority == 0.0
|
104
|
+
choices.delete(acceptable.downcase)
|
105
|
+
false
|
106
|
+
else
|
107
|
+
choices.include?(acceptable.downcase)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
(chosen && chosen.last) || # Use the matching one
|
111
|
+
(any_ok && choices.first) || # Or first if "*"
|
112
|
+
(default_ok && choices.include?(default) && default) # Or default
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
# Matches acceptable items that include 'q' values
|
117
|
+
CONNEG_REGEX = /^\s*(\S+);\s*q=(\S*)\s*$/
|
118
|
+
|
119
|
+
# Matches sub-type parameters
|
120
|
+
PARAMS_REGEX = /;([^=]+)=([^;=\s]+)/
|
121
|
+
|
122
|
+
# Matches valid media types
|
123
|
+
MEDIA_TYPE_REGEX = /^\s*([^;\s]+)\s*((?:;\S+\s*)*)\s*$/
|
124
|
+
|
125
|
+
# Encapsulates a MIME media type, with logic for matching types.
|
126
|
+
class MediaType
|
127
|
+
# Creates a new MediaType by parsing its string representation.
|
128
|
+
def self.parse(str)
|
129
|
+
if str =~ MEDIA_TYPE_REGEX
|
130
|
+
type, raw_params = $1, $2
|
131
|
+
params = Hash[raw_params.scan(PARAMS_REGEX)]
|
132
|
+
new(type, params)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [String] the MIME media type
|
137
|
+
attr_accessor :type
|
138
|
+
|
139
|
+
# @return [Hash] any type parameters, e.g. charset
|
140
|
+
attr_accessor :params
|
141
|
+
|
142
|
+
def initialize(type, params={})
|
143
|
+
@type, @params = type, params
|
144
|
+
end
|
145
|
+
|
146
|
+
# Detects whether the {MediaType} represents an open wildcard
|
147
|
+
# type, that is, "*/*" without any {#params}.
|
148
|
+
def matches_all?
|
149
|
+
@type == "*/*" && @params.empty?
|
150
|
+
end
|
151
|
+
|
152
|
+
def ==(other)
|
153
|
+
other = self.class.parse(other) if String === other
|
154
|
+
other.type == type && other.params == params
|
155
|
+
end
|
156
|
+
|
157
|
+
# Detects whether this {MediaType} matches the other {MediaType},
|
158
|
+
# taking into account wildcards.
|
159
|
+
def match?(other)
|
160
|
+
type_matches?(other) && other.params == params
|
161
|
+
end
|
162
|
+
|
163
|
+
# Reconstitutes the type into a String
|
164
|
+
def to_s
|
165
|
+
[type, *params.map {|k,v| "#{k}=#{v}" }].join(";")
|
166
|
+
end
|
167
|
+
|
168
|
+
# @return [String] The major type, e.g. "application", "text", "image"
|
169
|
+
def major
|
170
|
+
type.split("/").first
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [String] the minor or sub-type, e.g. "json", "html", "jpeg"
|
174
|
+
def minor
|
175
|
+
type.split("/").last
|
176
|
+
end
|
177
|
+
|
178
|
+
def type_matches?(other)
|
179
|
+
if ["*", "*/*", type].include?(other.type)
|
180
|
+
true
|
181
|
+
else
|
182
|
+
other.major == major && other.minor == "*"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Matches the requested media type (with potential modifiers)
|
188
|
+
# against the provided types (with potential modifiers).
|
189
|
+
# @param [MediaType] requested the requested media type
|
190
|
+
# @param [Array<MediaType>] provided the provided media
|
191
|
+
# types
|
192
|
+
# @return [MediaType] the first media type that matches
|
193
|
+
def media_match(requested, provided)
|
194
|
+
return provided.first if requested.matches_all?
|
195
|
+
provided.find do |p|
|
196
|
+
p.match?(requested)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Translate a KCODE value to a charset name
|
201
|
+
def kcode_charset
|
202
|
+
case $KCODE
|
203
|
+
when /^U/i
|
204
|
+
"UTF-8"
|
205
|
+
when /^S/i
|
206
|
+
"Shift-JIS"
|
207
|
+
when /^B/i
|
208
|
+
"Big5"
|
209
|
+
else #when /^A/i, nil
|
210
|
+
"ASCII"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# @private
|
215
|
+
# Content-negotiation priority list that takes into account both
|
216
|
+
# assigned priority ("q" value) as well as order, since items
|
217
|
+
# that come earlier in an acceptance list have higher priority
|
218
|
+
# by fiat.
|
219
|
+
class PriorityList
|
220
|
+
# Given an acceptance list, create a PriorityList from them.
|
221
|
+
def self.build(list)
|
222
|
+
new.tap do |plist|
|
223
|
+
list.each {|item| plist.add_header_val(item) }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
include Enumerable
|
228
|
+
|
229
|
+
# Creates a {PriorityList}.
|
230
|
+
# @see PriorityList::build
|
231
|
+
def initialize
|
232
|
+
@hash = Hash.new {|h,k| h[k] = [] }
|
233
|
+
@index = {}
|
234
|
+
end
|
235
|
+
|
236
|
+
# Adds an acceptable item with the given priority to the list.
|
237
|
+
# @param [Float] q the priority
|
238
|
+
# @param [String] choice the acceptable item
|
239
|
+
def add(q, choice)
|
240
|
+
@index[choice] = q
|
241
|
+
@hash[q] << choice
|
242
|
+
end
|
243
|
+
|
244
|
+
# Given a raw acceptable value from an acceptance header,
|
245
|
+
# parse and add it to the list.
|
246
|
+
# @param [String] c the raw acceptable item
|
247
|
+
# @see #add
|
248
|
+
def add_header_val(c)
|
249
|
+
if c =~ CONNEG_REGEX
|
250
|
+
choice, q = $1, $2
|
251
|
+
q = "0" << q if q =~ /^\./ # handle strange FeedBurner Accept
|
252
|
+
add(q.to_f,choice)
|
253
|
+
else
|
254
|
+
add(1.0, c)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# @param [Float] q the priority to lookup
|
259
|
+
# @return [Array<String>] the list of acceptable items at
|
260
|
+
# the given priority
|
261
|
+
def [](q)
|
262
|
+
@hash[q]
|
263
|
+
end
|
264
|
+
|
265
|
+
# @param [String] choice the acceptable item
|
266
|
+
# @return [Float] the priority of that value
|
267
|
+
def priority_of(choice)
|
268
|
+
@index[choice]
|
269
|
+
end
|
270
|
+
|
271
|
+
# Iterates over the list in priority order, that is, taking
|
272
|
+
# into account the order in which items were added as well as
|
273
|
+
# their priorities.
|
274
|
+
# @yield [q,v]
|
275
|
+
# @yieldparam [Float] q the acceptable item's priority
|
276
|
+
# @yieldparam [String] v the acceptable item
|
277
|
+
def each
|
278
|
+
@hash.to_a.sort.reverse_each do |q,l|
|
279
|
+
l.each {|v| yield q, v }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Like a {PriorityList}, but for {MediaTypes}, since they have
|
285
|
+
# parameters in addition to q.
|
286
|
+
# @private
|
287
|
+
class MediaTypeList < PriorityList
|
288
|
+
include Translation
|
289
|
+
|
290
|
+
# Overrides {PriorityList#add_header_val} to insert
|
291
|
+
# {MediaType} items instead of Strings.
|
292
|
+
# @see PriorityList#add_header_val
|
293
|
+
def add_header_val(c)
|
294
|
+
if mt = MediaType.parse(c)
|
295
|
+
q = mt.params.delete('q') || 1.0
|
296
|
+
add(q.to_f, mt)
|
297
|
+
else
|
298
|
+
raise MalformedRequest, t('invalid_media_type', :type => c)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
@@ -0,0 +1,502 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'webmachine/decision/conneg'
|
4
|
+
require 'webmachine/translation'
|
5
|
+
|
6
|
+
module Webmachine
|
7
|
+
module Decision
|
8
|
+
# This module encapsulates all of the decisions in Webmachine's
|
9
|
+
# flow-chart. These invoke {Resource} {Callbacks} to determine the
|
10
|
+
# appropriate response code, headers, and body for the response.
|
11
|
+
#
|
12
|
+
# This module is included into {FSM}, which drives the processing
|
13
|
+
# of the chart.
|
14
|
+
# @see http://webmachine.basho.com/images/http-headers-status-v3.png
|
15
|
+
module Flow
|
16
|
+
# Version of the flow diagram
|
17
|
+
VERSION = 3
|
18
|
+
|
19
|
+
# The first state in flow diagram
|
20
|
+
START = :b13
|
21
|
+
|
22
|
+
# Separate content-negotiation logic from flow diagram.
|
23
|
+
include Conneg
|
24
|
+
|
25
|
+
# Extract error strings into locale files
|
26
|
+
include Translation
|
27
|
+
|
28
|
+
# Handles standard decisions where halting is allowed
|
29
|
+
def decision_test(test, value, iftrue, iffalse)
|
30
|
+
case test
|
31
|
+
when value
|
32
|
+
iftrue
|
33
|
+
when Fixnum # Allows callbacks to "halt" with a given response code
|
34
|
+
test
|
35
|
+
else
|
36
|
+
iffalse
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Service available?
|
41
|
+
def b13
|
42
|
+
decision_test(resource.service_available?, true, :b12, 503)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Known method?
|
46
|
+
def b12
|
47
|
+
decision_test(resource.known_methods.include?(request.method), true, :b11, 501)
|
48
|
+
end
|
49
|
+
|
50
|
+
# URI too long?
|
51
|
+
def b11
|
52
|
+
decision_test(resource.uri_too_long?(request.uri), true, 414, :b10)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Method allowed?
|
56
|
+
def b10
|
57
|
+
if resource.allowed_methods.include?(request.method)
|
58
|
+
:b9
|
59
|
+
else
|
60
|
+
response.headers["Allow"] = resource.allowed_methods.join(", ")
|
61
|
+
405
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Content-MD5 present?
|
66
|
+
def b9
|
67
|
+
request.content_md5 ? :b9a : :b9b
|
68
|
+
end
|
69
|
+
|
70
|
+
# Content-MD5 valid?
|
71
|
+
def b9a
|
72
|
+
case valid = resource.validate_content_checksum
|
73
|
+
when Fixnum
|
74
|
+
valid
|
75
|
+
when true
|
76
|
+
:b9b
|
77
|
+
when false
|
78
|
+
response.body = "Content-MD5 header does not match request body."
|
79
|
+
400
|
80
|
+
else # not_validated
|
81
|
+
if request.content_md5 == Digest::MD5.hexdigest(request.body)
|
82
|
+
:b9b
|
83
|
+
else
|
84
|
+
response.body = "Content-MD5 header does not match request body."
|
85
|
+
400
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Malformed?
|
91
|
+
def b9b
|
92
|
+
decision_test(resource.malformed_request?, true, 400, :b8)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Authorized?
|
96
|
+
def b8
|
97
|
+
result = resource.is_authorized?(request.authorization)
|
98
|
+
case result
|
99
|
+
when true
|
100
|
+
:b7
|
101
|
+
when Fixnum
|
102
|
+
result
|
103
|
+
when String
|
104
|
+
response.headers['WWW-Authenticate'] = result
|
105
|
+
401
|
106
|
+
else
|
107
|
+
401
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Forbidden?
|
112
|
+
def b7
|
113
|
+
decision_test(resource.forbidden?, true, 403, :b6)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Okay Content-* Headers?
|
117
|
+
def b6
|
118
|
+
decision_test(resource.valid_content_headers?(request.headers.grep(/content-/)), true, :b5, 501)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Known Content-Type?
|
122
|
+
def b5
|
123
|
+
decision_test(resource.known_content_type?(request.content_type), true, :b4, 415)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Req Entity Too Large?
|
127
|
+
def b4
|
128
|
+
decision_test(resource.valid_entity_length?(request.content_length), true, :b3, 413)
|
129
|
+
end
|
130
|
+
|
131
|
+
# OPTIONS?
|
132
|
+
def b3
|
133
|
+
if request.method == "OPTIONS"
|
134
|
+
response.headers.merge!(resource.options)
|
135
|
+
200
|
136
|
+
else
|
137
|
+
:c3
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Accept exists?
|
142
|
+
def c3
|
143
|
+
if !request.accept
|
144
|
+
metadata['Content-Type'] = MediaType.parse(resource.content_types_provided.first.first)
|
145
|
+
:d4
|
146
|
+
else
|
147
|
+
:c4
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Acceptable media type available?
|
152
|
+
def c4
|
153
|
+
types = resource.content_types_provided.map {|pair| pair.first }
|
154
|
+
chosen_type = choose_media_type(types, request.accept)
|
155
|
+
if !chosen_type
|
156
|
+
406
|
157
|
+
else
|
158
|
+
metadata['Content-Type'] = chosen_type
|
159
|
+
:d4
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Accept-Language exists?
|
164
|
+
def d4
|
165
|
+
if !request.accept_language
|
166
|
+
choose_language(resource.languages_provided, "*") ? :e5 : 406
|
167
|
+
else
|
168
|
+
:d5
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Acceptable language available?
|
173
|
+
def d5
|
174
|
+
choose_language(resource.languages_provided, request.accept_language) ? :e5 : 406
|
175
|
+
end
|
176
|
+
|
177
|
+
# Accept-Charset exists?
|
178
|
+
def e5
|
179
|
+
if !request.accept_charset
|
180
|
+
choose_charset(resource.charsets_provided, "*") ? :f6 : 406
|
181
|
+
else
|
182
|
+
:e6
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Acceptable Charset available?
|
187
|
+
def e6
|
188
|
+
choose_charset(resource.charsets_provided, request.accept_charset) ? :f6 : 406
|
189
|
+
end
|
190
|
+
|
191
|
+
# Accept-Encoding exists?
|
192
|
+
# (also, set content-type header here, now that charset is chosen)
|
193
|
+
def f6
|
194
|
+
chosen_type = metadata['Content-Type']
|
195
|
+
if chosen_charset = metadata['Charset']
|
196
|
+
chosen_type.params['charset'] = chosen_charset
|
197
|
+
end
|
198
|
+
response.headers['Content-Type'] = chosen_type.to_s
|
199
|
+
if !request.accept_encoding
|
200
|
+
choose_encoding(resource.encodings_provided, "identity;q=1.0,*;q=0.5") ? :g7 : 406
|
201
|
+
else
|
202
|
+
:f7
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Acceptable encoding available?
|
207
|
+
def f7
|
208
|
+
choose_encoding(resource.encodings_provided, request.accept_encoding) ? :g7 : 406
|
209
|
+
end
|
210
|
+
|
211
|
+
# Resource exists?
|
212
|
+
def g7
|
213
|
+
# This is the first place after all conneg, so set Vary here
|
214
|
+
response.headers['Vary'] = variances.join(", ") if variances.any?
|
215
|
+
decision_test(resource.resource_exists?, true, :g8, :h7)
|
216
|
+
end
|
217
|
+
|
218
|
+
# If-Match exists?
|
219
|
+
def g8
|
220
|
+
request.if_match ? :g9 : :h10
|
221
|
+
end
|
222
|
+
|
223
|
+
# If-Match: * exists?
|
224
|
+
def g9
|
225
|
+
request.if_match == "*" ? :h10 : :g11
|
226
|
+
end
|
227
|
+
|
228
|
+
# ETag in If-Match
|
229
|
+
def g11
|
230
|
+
request_etags = request.if_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
|
231
|
+
request_etags.include?(resource.generate_etag) ? :h10 : 412
|
232
|
+
end
|
233
|
+
|
234
|
+
# If-Match exists?
|
235
|
+
def h7
|
236
|
+
(request.if_match && unquote_header(request.if_match) == '*') ? 412 : :i7
|
237
|
+
end
|
238
|
+
|
239
|
+
# If-Unmodified-Since exists?
|
240
|
+
def h10
|
241
|
+
request.if_unmodified_since ? :h11 : :i12
|
242
|
+
end
|
243
|
+
|
244
|
+
# If-Unmodified-Since is valid date?
|
245
|
+
def h11
|
246
|
+
begin
|
247
|
+
date = Time.httpdate(request.if_unmodified_since)
|
248
|
+
metadata['If-Unmodified-Since'] = date
|
249
|
+
rescue ArgumentError
|
250
|
+
:i12
|
251
|
+
else
|
252
|
+
:h12
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Last-Modified > I-UM-S?
|
257
|
+
def h12
|
258
|
+
resource.last_modified > metadata['If-Unmodified-Since'] ? 412 : :i12
|
259
|
+
end
|
260
|
+
|
261
|
+
# Moved permanently? (apply PUT to different URI)
|
262
|
+
def i4
|
263
|
+
case uri = resource.moved_permanently?
|
264
|
+
when String, URI
|
265
|
+
response.headers["Location"] = uri.to_s
|
266
|
+
301
|
267
|
+
when Fixnum
|
268
|
+
uri
|
269
|
+
else
|
270
|
+
:p3
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# PUT?
|
275
|
+
def i7
|
276
|
+
request.method == "PUT" ? :i4 : :k7
|
277
|
+
end
|
278
|
+
|
279
|
+
# If-none-match exists?
|
280
|
+
def i12
|
281
|
+
request.if_none_match ? :i13 : :l13
|
282
|
+
end
|
283
|
+
|
284
|
+
# If-none-match: * exists?
|
285
|
+
def i13
|
286
|
+
request.if_none_match == "*" ? :j18 : :k13
|
287
|
+
end
|
288
|
+
|
289
|
+
# GET or HEAD?
|
290
|
+
def j18
|
291
|
+
%w{GET HEAD}.include?(request.method) ? 304 : 412
|
292
|
+
end
|
293
|
+
|
294
|
+
# Moved permanently?
|
295
|
+
def k5
|
296
|
+
case uri = resource.moved_permanently?
|
297
|
+
when String, URI
|
298
|
+
response.headers["Location"] = uri.to_s
|
299
|
+
301
|
300
|
+
when Fixnum
|
301
|
+
uri
|
302
|
+
else
|
303
|
+
:l5
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Previously existed?
|
308
|
+
def k7
|
309
|
+
decision_test(resource.previously_existed?, true, :k5, :l7)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Etag in if-none-match?
|
313
|
+
def k13
|
314
|
+
request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
|
315
|
+
request_etags.include?(resource.generate_etag) ? :j18 : :l13
|
316
|
+
end
|
317
|
+
|
318
|
+
# Moved temporarily?
|
319
|
+
def l5
|
320
|
+
case uri = resource.moved_temporarily?
|
321
|
+
when String, URI
|
322
|
+
response.headers["Location"] = uri.to_s
|
323
|
+
307
|
324
|
+
when Fixnum
|
325
|
+
uri
|
326
|
+
else
|
327
|
+
:m5
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# POST?
|
332
|
+
def l7
|
333
|
+
request.method == "POST" ? :m7 : 404
|
334
|
+
end
|
335
|
+
|
336
|
+
# If-Modified-Since exists?
|
337
|
+
def l13
|
338
|
+
request.if_modified_since ? :l14 : :m16
|
339
|
+
end
|
340
|
+
|
341
|
+
# IMS is valid date?
|
342
|
+
def l14
|
343
|
+
begin
|
344
|
+
date = Time.httpdate(request.if_modified_since)
|
345
|
+
metadata['If-Modified-Since'] = date
|
346
|
+
rescue ArgumentError
|
347
|
+
:m16
|
348
|
+
else
|
349
|
+
:l15
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# IMS > Now?
|
354
|
+
def l15
|
355
|
+
metadata['If-Modified-Since'] > Time.now ? :m16 : :l17
|
356
|
+
end
|
357
|
+
|
358
|
+
# Last-Modified > IMS?
|
359
|
+
def l17
|
360
|
+
resource.last_modified.nil? || resource.last_modified > metadata['If-Modified-Since'] ? :m16 : 304
|
361
|
+
end
|
362
|
+
|
363
|
+
# POST?
|
364
|
+
def m5
|
365
|
+
request.method == "POST" ? :n5 : 410
|
366
|
+
end
|
367
|
+
|
368
|
+
# Server allows POST to missing resource?
|
369
|
+
def m7
|
370
|
+
decision_test(resource.allow_missing_post?, true, :n11, 404)
|
371
|
+
end
|
372
|
+
|
373
|
+
# DELETE?
|
374
|
+
def m16
|
375
|
+
request.method == "DELETE" ? :m20 : :n16
|
376
|
+
end
|
377
|
+
|
378
|
+
# DELETE enacted immediately? (Also where DELETE is forced.)
|
379
|
+
def m20
|
380
|
+
decision_test(resource.delete_resource, true, :m20b, 500)
|
381
|
+
end
|
382
|
+
|
383
|
+
def m20b
|
384
|
+
decision_test(resource.delete_completed?, true, :o20, 202)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Server allows POST to missing resource?
|
388
|
+
def n5
|
389
|
+
decision_test(resource.allow_missing_post?, true, :n11, 410)
|
390
|
+
end
|
391
|
+
|
392
|
+
# Redirect?
|
393
|
+
def n11
|
394
|
+
# Stage1
|
395
|
+
if resource.post_is_create?
|
396
|
+
case uri = resource.create_path
|
397
|
+
when nil
|
398
|
+
raise InvalidResource, t('create_path_nil', :class => resource.class)
|
399
|
+
when URI, String
|
400
|
+
base_uri = resource.base_uri || request.base_uri
|
401
|
+
new_uri = URI.join(base_uri.to_s, uri)
|
402
|
+
request.disp_path = new_uri.path
|
403
|
+
response.headers['Location'] = new_uri.to_s
|
404
|
+
result = accept_helper
|
405
|
+
return result if Fixnum === result
|
406
|
+
end
|
407
|
+
else
|
408
|
+
case result = resource.process_post
|
409
|
+
when true
|
410
|
+
encode_body_if_set
|
411
|
+
when Fixnum
|
412
|
+
return result
|
413
|
+
else
|
414
|
+
raise InvalidResource, t('process_post_invalid', :result => result)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
if response.is_redirect?
|
418
|
+
if response.headers['Location']
|
419
|
+
303
|
420
|
+
else
|
421
|
+
raise InvalidResource, t('do_redirect')
|
422
|
+
end
|
423
|
+
else
|
424
|
+
:p11
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# POST?
|
429
|
+
def n16
|
430
|
+
request.method == "POST" ? :n11 : :o16
|
431
|
+
end
|
432
|
+
|
433
|
+
# Conflict?
|
434
|
+
def o14
|
435
|
+
if resource.is_conflict?
|
436
|
+
409
|
437
|
+
else
|
438
|
+
res = accept_helper
|
439
|
+
(Fixnum === res) ? res : :p11
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# PUT?
|
444
|
+
def o16
|
445
|
+
request.method == "PUT" ? :o14 : :o18
|
446
|
+
end
|
447
|
+
|
448
|
+
# Multiple representations?
|
449
|
+
# Also where body generation for GET and HEAD is done.
|
450
|
+
def o18
|
451
|
+
if request.method =~ /^(GET|HEAD)$/
|
452
|
+
if etag = resource.generate_etag
|
453
|
+
response.headers['ETag'] = ensure_quoted_header(etag)
|
454
|
+
end
|
455
|
+
if last_modified = resource.last_modified
|
456
|
+
response.headers['Last-Modified'] = last_modified.httpdate
|
457
|
+
end
|
458
|
+
if expires = resource.expires
|
459
|
+
response.headers['Expires'] = expires.httpdate
|
460
|
+
end
|
461
|
+
content_type = metadata['Content-Type']
|
462
|
+
handler = resource.content_types_provided.find {|ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last
|
463
|
+
result = resource.send(handler)
|
464
|
+
if Fixnum === result
|
465
|
+
result
|
466
|
+
else
|
467
|
+
response.body = result
|
468
|
+
encode_body
|
469
|
+
:o18b
|
470
|
+
end
|
471
|
+
else
|
472
|
+
:o18b
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Multiple choices?
|
477
|
+
def o18b
|
478
|
+
decision_test(resource.multiple_choices?, true, 300, 200)
|
479
|
+
end
|
480
|
+
|
481
|
+
# Response includes an entity?
|
482
|
+
def o20
|
483
|
+
has_response_body? ? :o18 : 204
|
484
|
+
end
|
485
|
+
|
486
|
+
# Conflict?
|
487
|
+
def p3
|
488
|
+
if resource.is_conflict?
|
489
|
+
409
|
490
|
+
else
|
491
|
+
res = accept_helper
|
492
|
+
(Fixnum === res) ? res : :p11
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# New resource?
|
497
|
+
def p11
|
498
|
+
!response.headers["Location"] ? :o20 : 201
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|