rackful 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,76 +1,99 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # Required for parsing:
4
+ require 'rackful/global.rb'
5
+
6
+ # Required for running:
3
7
 
4
8
  module Rackful
5
9
 
6
10
  # Subclass of {Rack::Request}, augmented for Rackful requests.
11
+ #
12
+ # This class mixes in module `StatusCodes` for convenience, as explained in the
13
+ # {StatusCodes StatusCodes documentation}.
7
14
  class Request < Rack::Request
8
15
 
16
+ include StatusCodes
9
17
 
10
18
  def initialize *args
11
19
  super( *args )
12
20
  end
13
21
 
14
22
 
15
- =begin markdown
16
- Shortcut to {Server#resource_at}.
17
-
18
- (see Server#resource_at)
19
- @return (see Server#resource_at)
20
- @raise (see Server#resource_at)
21
- =end
22
- def resource_at *args
23
- env['rackful.server'].resource_at( *args )
23
+ # Calls the code block passed to the {#initialize constructor}.
24
+ # @param uri [URI::HTTP, String]
25
+ # @return [Resource]
26
+ # @raise [HTTP404NotFound]
27
+ def resource_at( uri )
28
+ uri = uri.kind_of?( URI::Generic ) ? uri.dup : URI(uri).normalize
29
+ uri.query = nil
30
+ retval = env['rackful.resource_registry'].call( uri )
31
+ raise HTTP404NotFound unless retval
32
+ retval
24
33
  end
25
34
 
26
-
27
- =begin markdown
28
- Similar to the HTTP/1.1 `Content-Location:` header. Contains the canonical url
29
- of the requested resource, which may differ from {#url}.
30
-
31
- If parameter +full_path+ is provided, than this is used instead of the current
32
- request’s full path (which is the path plus optional query string).
33
- @return [URI::Generic]
34
- =end
35
- def canonical_uri
36
- env['rackful.canonical_uri'] ||= URI( self.url ).normalize
35
+ # The current request’s main resource.
36
+ #
37
+ # As a side effect, {#canonical_uri} can be changed.
38
+ # @return [Resource]
39
+ # @raise [HTTP404NotFound] from {#resource_at}
40
+ def resource
41
+ @rackful_request_resource ||= begin
42
+ c = URI(url).normalize
43
+ retval = resource_at(c)
44
+ c += retval.uri
45
+ c.query = query_string unless query_string.empty?
46
+ env['rackful.canonical_uri'] = c
47
+ retval
48
+ end
37
49
  end
38
50
 
39
51
 
40
- =begin markdown
41
- The canonical url of the requested resource. This may differ from {#url}.
42
-
43
- @todo Change URI::Generic into URI::HTTP
44
- @param uri [URI::Generic, String]
45
- @return [URI::Generic, String] `uri`
46
- =end
47
- def canonical_uri=( uri )
48
- env['rackful.canonical_uri'] =
49
- uri.kind_of?( URI::Generic ) ? URI(uri) : URI( uri ).normalize
50
- uri
52
+ # Similar to the HTTP/1.1 `Content-Location:` header. Contains the canonical url
53
+ # of the requested resource, which may differ from {#url}.
54
+ #
55
+ # If parameter +full_path+ is provided, than this is used instead of the current
56
+ # request’s full path (which is the path plus optional query string).
57
+ # @return [URI::Generic]
58
+ def canonical_uri
59
+ env['rackful.canonical_uri'] || URI( self.url ).normalize
51
60
  end
52
61
 
53
62
 
54
- =begin markdown
55
- Assert all <tt>If-*</tt> request headers.
56
- @return [void]
57
- @raise [HTTP304NotModified, HTTP400BadRequest, HTTP404NotFound, HTTP412PreconditionFailed]
58
- with the following meanings:
59
-
60
- - `304 Not Modified`
61
- - `400 Bad Request` Couldn't parse one or more <tt>If-*</tt> headers, or a
62
- weak validator comparison was requested for methods other than `GET` or
63
- `HEAD`.
64
- - `404 Not Found`
65
- - `412 Precondition Failed`
66
- @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
67
- for details about weak and strong validator comparison.
68
- @todo Implement support for the `If-Range:` header.
69
- =end
70
- def assert_if_headers resource
63
+ # The canonical url of the requested resource. This may differ from {#url}.
64
+ #
65
+ # @todo Change URI::Generic into URI::HTTP
66
+ # @param uri [URI::Generic, String]
67
+ # @return [URI::Generic, String] `uri`
68
+ # def canonical_uri=( uri )
69
+ # env['rackful.canonical_uri'] =
70
+ # uri.kind_of?( URI::Generic ) ? uri.dup : URI( uri ).normalize
71
+ # uri
72
+ # end
73
+
74
+
75
+ # Assert all <tt>If-*</tt> request headers.
76
+ # @return [void]
77
+ # @raise [HTTP304NotModified, HTTP400BadRequest, HTTP404NotFound, HTTP412PreconditionFailed]
78
+ # with the following meanings:
79
+ #
80
+ # - `304 Not Modified`
81
+ # - `400 Bad Request` Couldn't parse one or more <tt>If-*</tt> headers, or a
82
+ # weak validator comparison was requested for methods other than `GET` or
83
+ # `HEAD`.
84
+ # - `404 Not Found`
85
+ # - `412 Precondition Failed`
86
+ # @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
87
+ # for details about weak and strong validator comparison.
88
+ # @todo Implement support for the `If-Range:` header.
89
+ def assert_if_headers
71
90
  #raise HTTP501NotImplemented, 'If-Range: request header is not supported.' \
72
91
  # if env.key? 'HTTP_IF_RANGE'
73
- empty = resource.empty?
92
+ begin
93
+ empty = resource.empty?
94
+ rescue HTTP404NotFound => e
95
+ empty = true
96
+ end
74
97
  etag =
75
98
  if ! empty && resource.respond_to?(:get_etag)
76
99
  resource.get_etag
@@ -146,47 +169,6 @@ Assert all <tt>If-*</tt> request headers.
146
169
  end
147
170
 
148
171
 
149
- =begin markdown
150
- The best media type to represent a resource, given the current HTTP request.
151
- @param resource [Resource] the resource to represent
152
- @param require_match [Boolean] this flag determines what must happen if the
153
- client sent an `Accept:` header, and we cannot serve any of the acceptable
154
- media types. **`TRUE`** means that an {HTTP406NotAcceptable} exception is
155
- raised. **`FALSE`** means that the content-type with the highest quality is
156
- returned.
157
- @return [String] content-type
158
- @raise [HTTP406NotAcceptable]
159
- @api private
160
- =end
161
- def best_content_type( resource, require_match = true )
162
- q_values = self.q_values
163
- if q_values.empty?
164
- return resource.all_serializers.values.sort_by(&:last).last[0]::CONTENT_TYPES[0]
165
- end
166
- matches = []
167
- q_values.each {
168
- |accept_media_type, accept_quality|
169
- resource.all_serializers.each_pair {
170
- |content_type, v|
171
- quality = v[1]
172
- media_type = content_type.split(';').first.strip
173
- if File.fnmatch( accept_media_type, media_type )
174
- matches << [ content_type, accept_quality * quality ]
175
- end
176
- }
177
- }
178
- if matches.empty?
179
- if require_match
180
- raise( HTTP406NotAcceptable, resource.all_serializers.keys() )
181
- else
182
- return resource.all_serializers.values.sort_by(&:last).
183
- last.first::CONTENT_TYPES.first
184
- end
185
- end
186
- matches.sort_by(&:last).last.first
187
- end
188
-
189
-
190
172
  # Hash of acceptable media types and their qualities.
191
173
  #
192
174
  # This method parses the HTTP/1.1 `Accept:` header. If no acceptable media
@@ -212,13 +194,11 @@ The best media type to represent a resource, given the current HTTP request.
212
194
  end # def accept
213
195
 
214
196
 
215
- =begin markdown
216
- @!method if_match()
217
- Parses the HTTP/1.1 `If-Match:` header.
218
- @return [nil, Array<String>]
219
- @see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
220
- @see #if_none_match
221
- =end
197
+ # @!method if_match()
198
+ # Parses the HTTP/1.1 `If-Match:` header.
199
+ # @return [nil, Array<String>]
200
+ # @see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
201
+ # @see #if_none_match
222
202
  def if_match none = false
223
203
  header = env["HTTP_IF_#{ none ? 'NONE_' : '' }MATCH"]
224
204
  return nil unless header
@@ -232,23 +212,19 @@ Parses the HTTP/1.1 `If-Match:` header.
232
212
  end
233
213
 
234
214
 
235
- =begin markdown
236
- Parses the HTTP/1.1 `If-None-Match:` header.
237
- @return [nil, Array<String>]
238
- @see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
239
- @see #if_match
240
- =end
215
+ # Parses the HTTP/1.1 `If-None-Match:` header.
216
+ # @return [nil, Array<String>]
217
+ # @see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
218
+ # @see #if_match
241
219
  def if_none_match
242
220
  self.if_match true
243
221
  end
244
222
 
245
223
 
246
- =begin markdown
247
- @!method if_modified_since()
248
- @return [nil, Time]
249
- @see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
250
- @see #if_unmodified_since
251
- =end
224
+ # @!method if_modified_since()
225
+ # @return [nil, Time]
226
+ # @see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
227
+ # @see #if_unmodified_since
252
228
  def if_modified_since unmodified = false
253
229
  header = env["HTTP_IF_#{ unmodified ? 'UN' : '' }MODIFIED_SINCE"]
254
230
  return nil unless header
@@ -261,29 +237,25 @@ Parses the HTTP/1.1 `If-None-Match:` header.
261
237
  end
262
238
 
263
239
 
264
- =begin markdown
265
- @return [nil, Time]
266
- @see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
267
- @see #if_modified_since
268
- =end
240
+ # @return [nil, Time]
241
+ # @see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
242
+ # @see #if_modified_since
269
243
  def if_unmodified_since
270
244
  self.if_modified_since true
271
245
  end
272
246
 
273
247
 
274
- =begin markdown
275
- Does any of the tags in `etags` match `etag`?
276
- @param etag [#to_s]
277
- @param etags [#to_a]
278
- @example
279
- etag = '"foo"'
280
- etags = [ 'W/"foo"', '"bar"' ]
281
- validate_etag etag, etags
282
- #> true
283
- @return [Boolean]
284
- @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
285
- for details about weak and strong validator comparison.
286
- =end
248
+ # Does any of the tags in `etags` match `etag`?
249
+ # @param etag [#to_s]
250
+ # @param etags [#to_a]
251
+ # @example
252
+ # etag = '"foo"'
253
+ # etags = [ 'W/"foo"', '"bar"' ]
254
+ # validate_etag etag, etags
255
+ # #> true
256
+ # @return [Boolean]
257
+ # @see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
258
+ # for details about weak and strong validator comparison.
287
259
  def validate_etag etag, etags
288
260
  etag = etag.to_s
289
261
  match = etags.to_a.detect do
@@ -304,6 +276,5 @@ Does any of the tags in `etags` match `etag`?
304
276
  end
305
277
 
306
278
 
307
- end # class Request
308
-
279
+ end # class Rackful::Request
309
280
  end # module Rackful
@@ -1,21 +1,80 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'forwardable'
3
+ # Required for parsing:
4
+ require 'rackful/global.rb'
5
+
6
+ # Required for running:
4
7
 
5
8
  module Rackful
6
9
 
7
10
  # Abstract superclass for resources served by {Server}.
11
+ #
12
+ # This class mixes in module `StatusCodes` for convenience, as explained in the
13
+ # {StatusCodes StatusCodes documentation}.
8
14
  # @see Server
9
15
  # @todo better documentation
10
16
  # @abstract Realizations must implement…
11
- class Resource
12
-
13
- class << self
17
+ #
18
+ # @!method do_METHOD( Request, Rack::Response )
19
+ # HTTP/1.1 method handler.
20
+ #
21
+ # To handle certain HTTP/1.1 request methods, resources must implement methods
22
+ # called `do_<HTTP_METHOD>`.
23
+ # @example Handling `PATCH` requests
24
+ # def do_PATCH request, response
25
+ # response['Content-Type'] = 'text/plain'
26
+ # response.body = [ 'Hello world!' ]
27
+ # end
28
+ # @abstract
29
+ # @return [void]
30
+ # @raise [HTTPStatus, RuntimeError]
31
+ #
32
+ # @!attribute [r] get_etag
33
+ # The ETag of this resource.
34
+ #
35
+ # If your classes implement this method, then an `ETag:` response
36
+ # header is generated automatically when appropriate. This allows clients to
37
+ # perform conditional requests, by sending an `If-Match:` or
38
+ # `If-None-Match:` request header. These conditions are then asserted
39
+ # for you automatically.
40
+ #
41
+ # Make sure your entity tag is a properly formatted string. In ABNF:
42
+ #
43
+ # entity-tag = [ "W/" ] quoted-string
44
+ # quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
45
+ # qdtext = <any TEXT except <">>
46
+ # quoted-pair = "\" CHAR
47
+ #
48
+ # @abstract
49
+ # @return [String]
50
+ # @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
51
+ #
52
+ # @!attribute [r] get_last_modified
53
+ # Last modification of this resource.
54
+ #
55
+ # If your classes implement this method, then a `Last-Modified:` response
56
+ # header is generated automatically when appropriate. This allows clients to
57
+ # perform conditional requests, by sending an `If-Modified-Since:` or
58
+ # `If-Unmodified-Since:` request header. These conditions are then asserted
59
+ # for you automatically.
60
+ # @abstract
61
+ # @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
62
+ # timestamp is a strong validator.
63
+ # @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
64
+ #
65
+ # @!method destroy()
66
+ # @return [Hash, nil] an optional header hash.
67
+ module Resource
68
+
69
+ include StatusCodes
70
+
71
+ module ClassMethods
14
72
 
15
73
 
16
74
  # Meta-programmer method.
17
75
  # @example Have your resource rendered in XML and JSON
18
76
  # class MyResource
77
+ # include Rackful::Resource
19
78
  # add_serializer MyResource2XML
20
79
  # add_serializer MyResource2JSON, 0.5
21
80
  # end
@@ -26,124 +85,124 @@ class Resource
26
85
  quality = quality.to_f
27
86
  quality = 1.0 if quality > 1.0
28
87
  quality = 0.0 if quality < 0.0
29
- s = [serializer, quality]
30
- serializer::CONTENT_TYPES.each {
31
- |content_type|
32
- self.serializers[content_type.to_s] = s
33
- }
88
+ sq = [serializer, quality]
89
+ serializer.content_types.each do
90
+ |content_type|
91
+ unless ( old = serializers[content_type] ) and # @formatter:off
92
+ old[1] > quality # @formatter:on
93
+ serializers[content_type] = sq
94
+ end
95
+ end
34
96
  self
35
97
  end
36
98
 
37
99
 
38
100
  # Meta-programmer method.
101
+ #
102
+ # A parser is an object or a class that implements to the following two methods:
103
+ #
104
+ # 1. (Array<String>) media_types()
105
+ # 2. (void) parse(Request, Resource)
106
+ #
107
+ # The first call returns a list of media types this parser can parse, for example:
108
+ # `[ 'text/html', 'application/xhtml+xml' ]`. The second call parses a request
109
+ # in the context of a certain resource.
39
110
  # @example Have your resource accept XHTML in `PUT` requests
40
111
  # class MyResource
41
112
  # include Rackful::Resource
42
113
  # add_parser Rackful::Parser::XHTML, :PUT
43
114
  # end
44
- # @param parser [Class] an implementation (ie. subclass) of {Parser}
115
+ # @param parser [#parse, #media_types] an implementation (ie. subclass) of {Parser}
45
116
  # @param method [#to_sym] For example: `:PUT` or `:POST`
46
117
  # @return [self]
47
118
  def add_parser parser, method = :PUT
48
119
  method = method.to_sym
49
- self.parsers[method] ||= []
50
- self.parsers[method] << parser
51
- self.parsers[method].uniq!
120
+ parsers[method] ||= []
121
+ parser.media_types.each do
122
+ |mt|
123
+ parsers[method] << [mt, parser]
124
+ end
125
+ parsers[method].uniq!
52
126
  self
53
127
  end
54
128
 
55
129
 
56
- # All parsers added to _this_ class. Ie. not including parsers added
57
- # to parent classes.
58
- # @return [Hash{Symbol => Array<Class>}] A hash of lists of {Parser} classes,
59
- # indexed by HTTP method.
60
- # @api private
61
- def parsers
62
- # The single '@' on the following line is on purpose!
63
- @rackful_resource_parsers ||= {}
64
- end
65
-
66
-
67
- # All serializers added to _this_ class. Ie. not including serializers added
68
- # to parent classes.
69
- # @return [Hash{Serializer => Float}]
70
- # @api private
71
- def serializers
72
- # The single '@' on the following line is on purpose!
73
- @rackful_resource_serializers ||= {}
74
- end
75
-
76
-
77
130
  # All parsers for this class, including parsers added to parent classes.
78
131
  # The result of this method is cached, which will interfere with code reloading.
79
- # @param method [#to_sym] For example: `:PUT` or `:POST`
80
132
  # @return [Hash{Symbol => Array<Class>}]
81
133
  # @api private
82
134
  def all_parsers
83
135
  # The single '@' on the following line is on purpose!
84
136
  @rackful_resource_all_parsers ||=
85
137
  if self.superclass.respond_to?(:all_parsers)
86
- self.parsers.merge( self.superclass.all_parsers ) do
138
+ self.superclass.all_parsers.merge( parsers ) do
87
139
  |key, oldval, newval|
88
140
  ( oldval + newval ).uniq
89
141
  end
90
142
  else
91
- self.parsers
143
+ parsers
92
144
  end
93
145
  end
94
146
 
95
147
 
96
148
  # All serializers for this class, including those added to parent classes.
97
149
  # The result of this method is cached, which will interfere with code reloading.
98
- # @return [Hash{Serializer => Float}] The float indicates the quality of each
99
- # serializer in the interval [0,1]
150
+ # @return [Hash{ String( content_type ) => Array( Serializer, Float(quality) ) }]
100
151
  # @api private
101
152
  def all_serializers
102
153
  # The single '@' on the following line is on purpose!
103
154
  @rackful_resource_all_serializers ||=
104
155
  if self.superclass.respond_to?(:all_serializers)
105
- self.superclass.all_serializers.merge( self.serializers ) do
156
+ self.superclass.all_serializers.merge( serializers ) do
106
157
  |key, oldval, newval|
107
158
  newval[1] >= oldval[1] ? newval : oldval
108
159
  end
109
160
  else
110
- self.serializers
161
+ serializers
111
162
  end
112
163
  end
113
164
 
165
+ private
114
166
 
115
- end # module ClassMethods
167
+ # All parsers added to _this_ class. Ie. not including parsers added
168
+ # to parent classes.
169
+ # @return [Hash{Symbol => Array<Class>}] A hash of lists of {Parser} classes,
170
+ # indexed by HTTP method.
171
+ # @api private
172
+ def parsers
173
+ # The single '@' on the following line is on purpose!
174
+ @rackful_resource_parsers ||= {}
175
+ end
116
176
 
117
177
 
118
- extend Forwardable
119
- def_delegators 'self.class', :parsers, :serializers, :all_parsers, :all_serializers
178
+ # All serializers added to _this_ class. Ie. not including serializers added
179
+ # to parent classes.
180
+ # @return [Hash{ String( content_type ) => Array( Serializer, Float(quality) ) }]
181
+ # @api private
182
+ def serializers
183
+ # The single '@' on the following line is on purpose!
184
+ @rackful_resource_serializers ||= {}
185
+ end
120
186
 
121
187
 
122
- def initialize( uri )
123
- @uri = uri.kind_of?( URI::Generic ) ? uri.dup : URI(uri.to_s).normalize
124
- #self.uri = uri
125
- end
188
+ end # module ClassMethods
126
189
 
127
190
 
128
- =begin markdown
129
- @param request [Request]
130
- @param content_type [String, nil] If omitted, you get the best serializer
131
- available, which may not be acceptable by the client.
132
- @return [Serializer]
133
- =end
134
- def serializer request, content_type = nil
135
- content_type ||= request.best_content_type( self, false )
136
- self.class.all_serializers[content_type][0].new( request, self, content_type )
191
+ # This callback includes all methods of {ClassMethods} into all classes that
192
+ # include {Resource}, to make them available as a tiny DSL.
193
+ # @api private
194
+ def self.included(base)
195
+ base.extend ClassMethods
137
196
  end
138
197
 
139
198
 
140
- =begin markdown
141
- The best media type for the response body, given the current HTTP request.
142
- @param request [Rackful::Request]
143
- @return [Parser, nil] a {Parser}, or nil if the request entity is empty
144
- @raise [HTTP415UnsupportedMediaType] if no parser can be found for the request entity
145
- =end
146
- def parser request
199
+ # Parse and “execute” the request body.
200
+ # @param request [Rackful::Request]
201
+ # @param response [Rack::Response]
202
+ # @return [Parser, nil] a {Parser}, or nil if the request entity is empty
203
+ # @raise [HTTP415UnsupportedMediaType] if no parser can be found for the request entity
204
+ # @api private
205
+ def parse request, response
147
206
  unless request.content_length ||
148
207
  'chunked' == request.env['HTTP_TRANSFER_ENCODING']
149
208
  raise HTTP411LengthRequired
@@ -151,40 +210,25 @@ The best media type for the response body, given the current HTTP request.
151
210
  request_media_type = request.media_type.to_s
152
211
  supported_media_types = []
153
212
  all_parsers = self.class.all_parsers[ request.request_method.to_sym ] || []
154
- all_parsers.each do |p|
155
- p::MEDIA_TYPES.each do |parser_media_type|
156
- if File.fnmatch( parser_media_type, request_media_type )
157
- return p.new( request, self )
158
- end
159
- supported_media_types << parser_media_type
213
+ all_parsers.each do |mt, p|
214
+ if File.fnmatch( mt, request_media_type, File::FNM_PATHNAME )
215
+ return p.parse( request, response, self )
160
216
  end
217
+ supported_media_types << mt
161
218
  end
219
+ raise( HTTP405MethodNotAllowed, self.http_methods ) if supported_media_types.empty?
162
220
  raise( HTTP415UnsupportedMediaType, supported_media_types.uniq )
163
221
  end
164
222
 
165
223
 
166
- =begin markdown
167
- @!method do_METHOD( Request, Rack::Response )
168
- HTTP/1.1 method handler.
169
-
170
- To handle certain HTTP/1.1 request methods, resources must implement methods
171
- called `do_<HTTP_METHOD>`.
172
- @example Handling `PATCH` requests
173
- def do_PATCH request, response
174
- response['Content-Type'] = 'text/plain'
175
- response.body = [ 'Hello world!' ]
176
- end
177
- @abstract
178
- @return [void]
179
- @raise [HTTPStatus, RuntimeError]
180
- =end
224
+ # The canonical path of this resource.
225
+ # @return [URI]
226
+ attr_reader :uri
181
227
 
182
228
 
183
- =begin markdown
184
- The canonical path of this resource.
185
- @return [URI]
186
- =end
187
- attr_reader :uri
229
+ def uri=( uri )
230
+ @uri = uri.kind_of?(URI::Generic) ? uri.dup : URI(uri).normalize
231
+ end
188
232
 
189
233
 
190
234
  def title
@@ -192,16 +236,14 @@ The canonical path of this resource.
192
236
  end
193
237
 
194
238
 
195
- =begin markdown
196
- Does this resource _exist_?
197
-
198
- For example, a client can `PUT` to a URL that doesn't refer to a resource
199
- yet. In that case, your {Server#initialize resource registry} can
200
- produce an empty resource to handle the `PUT` request. `HEAD` and `GET`
201
- requests will still yield `404 Not Found`.
202
-
203
- @return [Boolean] The default implementation returns `false`.
204
- =end
239
+ # Does this resource _exist_?
240
+ #
241
+ # For example, a client can `PUT` to a URL that doesn't refer to a resource
242
+ # yet. In that case, your {Server#initialize resource registry} can
243
+ # produce an empty resource to handle the `PUT` request. `HEAD` and `GET`
244
+ # requests will still yield `404 Not Found`.
245
+ #
246
+ # @return [Boolean] The default implementation returns `false`.
205
247
  def empty?
206
248
  false
207
249
  end
@@ -212,58 +254,13 @@ requests will still yield `404 Not Found`.
212
254
  end
213
255
 
214
256
 
215
- =begin markdown
216
- @!attribute [r] get_etag
217
- The ETag of this resource.
218
-
219
- If your classes implement this method, then an `ETag:` response
220
- header is generated automatically when appropriate. This allows clients to
221
- perform conditional requests, by sending an `If-Match:` or
222
- `If-None-Match:` request header. These conditions are then asserted
223
- for you automatically.
224
-
225
- Make sure your entity tag is a properly formatted string. In ABNF:
226
-
227
- entity-tag = [ "W/" ] quoted-string
228
- quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
229
- qdtext = <any TEXT except <">>
230
- quoted-pair = "\" CHAR
231
-
232
- @abstract
233
- @return [String]
234
- @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
235
- =end
236
-
237
-
238
- =begin markdown
239
- @!attribute [r] get_last_modified
240
- Last modification of this resource.
241
257
 
242
- If your classes implement this method, then a `Last-Modified:` response
243
- header is generated automatically when appropriate. This allows clients to
244
- perform conditional requests, by sending an `If-Modified-Since:` or
245
- `If-Unmodified-Since:` request header. These conditions are then asserted
246
- for you automatically.
247
- @abstract
248
- @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the
249
- timestamp is a strong validator.
250
- @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
251
- =end
252
258
 
253
-
254
- =begin markdown
255
- @!method destroy()
256
- @return [Hash, nil] an optional header hash.
257
- =end
258
-
259
-
260
- =begin markdown
261
- List of all HTTP/1.1 methods implemented by this resource.
262
-
263
- This works by inspecting all the {#do_METHOD} methods this object implements.
264
- @return [Array<Symbol>]
265
- @api private
266
- =end
259
+ # List of all HTTP/1.1 methods implemented by this resource.
260
+ #
261
+ # This works by inspecting all the {#do_METHOD} methods this object implements.
262
+ # @return [Array<Symbol>]
263
+ # @api private
267
264
  def http_methods
268
265
  r = []
269
266
  if self.empty?
@@ -284,33 +281,29 @@ This works by inspecting all the {#do_METHOD} methods this object implements.
284
281
  end
285
282
 
286
283
 
287
- =begin markdown
288
- Handles an OPTIONS request.
289
-
290
- As a courtesy, this module implements a default handler for OPTIONS
291
- requests. It creates an `Allow:` header, listing all implemented HTTP/1.1
292
- methods for this resource. By default, an `HTTP/1.1 204 No Content` is
293
- returned (without an entity body).
294
-
295
- Feel free to override this method at will.
296
- @return [void]
297
- @raise [HTTP404NotFound] `404 Not Found` if this resource is empty.
298
- =end
284
+ # Handles an OPTIONS request.
285
+ #
286
+ # As a courtesy, this module implements a default handler for OPTIONS
287
+ # requests. It creates an `Allow:` header, listing all implemented HTTP/1.1
288
+ # methods for this resource. By default, an `HTTP/1.1 204 No Content` is
289
+ # returned (without an entity body).
290
+ #
291
+ # Feel free to override this method at will.
292
+ # @return [void]
293
+ # @raise [HTTP404NotFound] `404 Not Found` if this resource is empty.
299
294
  def http_OPTIONS request, response
300
295
  response.status = Rack::Utils.status_code :no_content
301
296
  response.header['Allow'] = self.http_methods.join ', '
302
297
  end
303
298
 
304
299
 
305
- =begin markdown
306
- Handles a HEAD request.
307
-
308
- This default handler for HEAD requests calls {#http\_GET}, and
309
- then strips off the response body.
310
-
311
- Feel free to override this method at will.
312
- @return [self]
313
- =end
300
+ # Handles a HEAD request.
301
+ #
302
+ # This default handler for HEAD requests calls {#http\_GET}, and
303
+ # then strips off the response body.
304
+ #
305
+ # Feel free to override this method at will.
306
+ # @return [self]
314
307
  def http_HEAD request, response
315
308
  self.http_GET request, response
316
309
  response['Content-Length'] =
@@ -323,21 +316,18 @@ Feel free to override this method at will.
323
316
  end
324
317
 
325
318
 
326
- =begin markdown
327
- @api private
328
- @param request [Rackful::Request]
329
- @param response [Rack::Response]
330
- @return [void]
331
- @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
332
- =end
319
+ # @api private
320
+ # @param request [Rackful::Request]
321
+ # @param response [Rack::Response]
322
+ # @return [void]
323
+ # @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
333
324
  def http_GET request, response
334
325
  raise HTTP404NotFound if self.empty?
335
326
  # May throw HTTP406NotAcceptable:
336
- content_type = request.best_content_type( self )
337
- response['Content-Type'] = content_type
327
+ serializer = self.serializer( request )
328
+ response['Content-Type'] = serializer.content_type
338
329
  response.status = Rack::Utils.status_code( :ok )
339
330
  response.headers.merge! self.default_headers
340
- serializer = self.serializer( request, content_type )
341
331
  if serializer.respond_to? :headers
342
332
  response.headers.merge!( serializer.headers )
343
333
  end
@@ -345,12 +335,47 @@ Feel free to override this method at will.
345
335
  end
346
336
 
347
337
 
348
- =begin markdown
349
- Wrapper around {#do_METHOD #do_GET}
350
- @api private
351
- @return [void]
352
- @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
353
- =end
338
+ # The best serializer to represent this resource, given the current HTTP request.
339
+ # @param request [Request] the current request
340
+ # @param require_match [Boolean] this flag determines what must happen if the
341
+ # client sent an `Accept:` header, and we cannot serve any of the acceptable
342
+ # media types. **`TRUE`** means that an {HTTP406NotAcceptable} exception is
343
+ # raised. **`FALSE`** means that the content-type with the highest quality is
344
+ # returned.
345
+ # @return [Serializer]
346
+ # @raise [HTTP406NotAcceptable]
347
+ def serializer( request, require_match = true )
348
+ q_values = request.q_values # Array<Array(type, quality)>
349
+ default_serializer = # @formatter:off
350
+ # Hash{ String( content_type ) => Array( Serializer, Float(quality) ) }
351
+ self.class.all_serializers.
352
+ # Array< Array( Serializer, Float(quality) ) >
353
+ values.sort_by(&:last).
354
+ # Serializer
355
+ last.first # @formatter:on
356
+ best_match = [ default_serializer, 0.0, default_serializer.content_types.first ]
357
+ q_values.each do
358
+ |accept_media_type, accept_quality|
359
+ self.class.all_serializers.each_pair do
360
+ |content_type, sq|
361
+ media_type = content_type.split(/\s*;\s*/).first
362
+ if File.fnmatch( accept_media_type, media_type, File::FNM_PATHNAME ) and
363
+ best_match.nil? || best_match[1] < ( accept_quality * sq[1] )
364
+ best_match = [ sq[0], sq[1], content_type ]
365
+ end
366
+ end
367
+ end
368
+ if require_match and best_match[1] <= 0.0
369
+ raise( HTTP406NotAcceptable, self.class.all_serializers.keys() )
370
+ end
371
+ best_match[0].new(request, self, best_match[2])
372
+ end
373
+
374
+
375
+ # Wrapper around {#do_METHOD #do_GET}
376
+ # @api private
377
+ # @return [void]
378
+ # @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
354
379
  def http_DELETE request, response
355
380
  raise HTTP404NotFound if self.empty?
356
381
  raise HTTP405MethodNotAllowed, self.http_methods unless
@@ -362,26 +387,61 @@ Wrapper around {#do_METHOD #do_GET}
362
387
  end
363
388
 
364
389
 
365
- =begin markdown
366
- @api private
367
- @return [void]
368
- @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the resource doesn't implement the `PUT` method.
369
- =end
390
+ # @api private
391
+ # @return [void]
392
+ # @raise [HTTP404NotFound, HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
393
+ # resource doesnt implement the `PATCH` method or can’t handle the provided
394
+ # request body media type.
395
+ def http_PATCH request, response
396
+ raise HTTP404NotFound if self.empty?
397
+ response.status = :no_content
398
+ begin
399
+ parse(request, response)
400
+ rescue HTTP405MethodNotAllowed => e
401
+ raise e unless self.respond_to? :do_PATCH
402
+ self.do_PATCH( request, response )
403
+ end
404
+ response.headers.merge! self.default_headers
405
+ end
406
+
407
+
408
+ # @api private
409
+ # @return [void]
410
+ # @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
411
+ # resource doesn’t implement the `POST` method or can’t handle the provided
412
+ # request body media type.
413
+ def http_POST request, response
414
+ begin
415
+ parse(request, response)
416
+ rescue HTTP405MethodNotAllowed => e
417
+ raise e unless self.respond_to? :do_POST
418
+ self.do_POST( request, response )
419
+ end
420
+ end
421
+
422
+
423
+ # @api private
424
+ # @return [void]
425
+ # @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
426
+ # resource doesn’t implement the `PUT` method or can’t handle the provided
427
+ # request body media type.
370
428
  def http_PUT request, response
371
- raise HTTP405MethodNotAllowed, self.http_methods unless self.respond_to? :do_PUT
372
429
  response.status = Rack::Utils.status_code( self.empty? ? :created : :no_content )
373
- self.do_PUT( request, response )
430
+ begin
431
+ parse(request, response)
432
+ rescue HTTP405MethodNotAllowed => e
433
+ raise e unless self.respond_to? :do_PUT
434
+ self.do_PUT( request, response )
435
+ end
374
436
  response.headers.merge! self.default_headers
375
437
  end
376
438
 
377
439
 
378
- =begin markdown
379
- Wrapper around {#do_METHOD}
380
- @api private
381
- @return [void]
382
- @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement
383
- the request method.
384
- =end
440
+ # Wrapper around {#do_METHOD}
441
+ # @api private
442
+ # @return [void]
443
+ # @raise [HTTPStatus] `405 Method Not Allowed` if the resource doesn't implement
444
+ # the request method.
385
445
  def http_method request, response
386
446
  method = request.request_method.to_sym
387
447
  if ! self.respond_to?( :"do_#{method}" )
@@ -391,9 +451,7 @@ Wrapper around {#do_METHOD}
391
451
  end
392
452
 
393
453
 
394
- =begin markdown
395
- Adds `ETag:` and `Last-Modified:` response headers.
396
- =end
454
+ # Adds `ETag:` and `Last-Modified:` response headers.
397
455
  def default_headers
398
456
  r = {}
399
457
  r['ETag'] = self.get_etag \
@@ -404,7 +462,5 @@ Adds `ETag:` and `Last-Modified:` response headers.
404
462
  end
405
463
 
406
464
 
407
- end # module Resource
408
-
409
-
465
+ end # module Rackful::Resource
410
466
  end # module Rackful