rackful 0.2.0 → 0.2.1

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.
@@ -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