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.
Files changed (63) hide show
  1. data/Gemfile +11 -3
  2. data/README.md +55 -27
  3. data/lib/webmachine/adapters/mongrel.rb +84 -0
  4. data/lib/webmachine/adapters/webrick.rb +12 -3
  5. data/lib/webmachine/adapters.rb +1 -7
  6. data/lib/webmachine/configuration.rb +30 -0
  7. data/lib/webmachine/decision/conneg.rb +7 -72
  8. data/lib/webmachine/decision/flow.rb +13 -11
  9. data/lib/webmachine/decision/fsm.rb +1 -9
  10. data/lib/webmachine/decision/helpers.rb +27 -7
  11. data/lib/webmachine/errors.rb +1 -0
  12. data/lib/webmachine/headers.rb +12 -3
  13. data/lib/webmachine/locale/en.yml +2 -2
  14. data/lib/webmachine/media_type.rb +117 -0
  15. data/lib/webmachine/resource/callbacks.rb +9 -0
  16. data/lib/webmachine/streaming.rb +3 -3
  17. data/lib/webmachine/version.rb +1 -1
  18. data/lib/webmachine.rb +3 -1
  19. data/pkg/webmachine-0.1.0/Gemfile +16 -0
  20. data/pkg/webmachine-0.1.0/Guardfile +11 -0
  21. data/pkg/webmachine-0.1.0/README.md +90 -0
  22. data/pkg/webmachine-0.1.0/Rakefile +31 -0
  23. data/pkg/webmachine-0.1.0/examples/webrick.rb +19 -0
  24. data/pkg/webmachine-0.1.0/lib/webmachine/adapters/webrick.rb +74 -0
  25. data/pkg/webmachine-0.1.0/lib/webmachine/adapters.rb +15 -0
  26. data/pkg/webmachine-0.1.0/lib/webmachine/decision/conneg.rb +304 -0
  27. data/pkg/webmachine-0.1.0/lib/webmachine/decision/flow.rb +502 -0
  28. data/pkg/webmachine-0.1.0/lib/webmachine/decision/fsm.rb +79 -0
  29. data/pkg/webmachine-0.1.0/lib/webmachine/decision/helpers.rb +80 -0
  30. data/pkg/webmachine-0.1.0/lib/webmachine/decision.rb +12 -0
  31. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher/route.rb +85 -0
  32. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher.rb +40 -0
  33. data/pkg/webmachine-0.1.0/lib/webmachine/errors.rb +37 -0
  34. data/pkg/webmachine-0.1.0/lib/webmachine/headers.rb +16 -0
  35. data/pkg/webmachine-0.1.0/lib/webmachine/locale/en.yml +28 -0
  36. data/pkg/webmachine-0.1.0/lib/webmachine/request.rb +56 -0
  37. data/pkg/webmachine-0.1.0/lib/webmachine/resource/callbacks.rb +362 -0
  38. data/pkg/webmachine-0.1.0/lib/webmachine/resource/encodings.rb +36 -0
  39. data/pkg/webmachine-0.1.0/lib/webmachine/resource.rb +48 -0
  40. data/pkg/webmachine-0.1.0/lib/webmachine/response.rb +49 -0
  41. data/pkg/webmachine-0.1.0/lib/webmachine/streaming.rb +27 -0
  42. data/pkg/webmachine-0.1.0/lib/webmachine/translation.rb +11 -0
  43. data/pkg/webmachine-0.1.0/lib/webmachine/version.rb +4 -0
  44. data/pkg/webmachine-0.1.0/lib/webmachine.rb +19 -0
  45. data/pkg/webmachine-0.1.0/spec/spec_helper.rb +13 -0
  46. data/pkg/webmachine-0.1.0/spec/tests.org +57 -0
  47. data/pkg/webmachine-0.1.0/spec/webmachine/decision/conneg_spec.rb +152 -0
  48. data/pkg/webmachine-0.1.0/spec/webmachine/decision/flow_spec.rb +1030 -0
  49. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher/route_spec.rb +109 -0
  50. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher_spec.rb +34 -0
  51. data/pkg/webmachine-0.1.0/spec/webmachine/headers_spec.rb +19 -0
  52. data/pkg/webmachine-0.1.0/spec/webmachine/request_spec.rb +24 -0
  53. data/pkg/webmachine-0.1.0/webmachine.gemspec +44 -0
  54. data/pkg/webmachine-0.1.0.gem +0 -0
  55. data/spec/webmachine/configuration_spec.rb +27 -0
  56. data/spec/webmachine/decision/conneg_spec.rb +18 -11
  57. data/spec/webmachine/decision/flow_spec.rb +2 -0
  58. data/spec/webmachine/decision/helpers_spec.rb +105 -0
  59. data/spec/webmachine/errors_spec.rb +13 -0
  60. data/spec/webmachine/headers_spec.rb +2 -1
  61. data/spec/webmachine/media_type_spec.rb +78 -0
  62. data/webmachine.gemspec +4 -1
  63. 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