webmachine 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|