webmachine 0.1.0 → 0.2.0

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