module Rackful::Resource
Abstract superclass for resources served by {Server}.
This class mixes in module ‘StatusCodes` for convenience, as explained in the {StatusCodes StatusCodes
documentation}. @see Server
@todo better documentation @abstract Realizations must implement…
@!method do_METHOD( Request
, Rack::Response )
HTTP/1.1 method handler. To handle certain HTTP/1.1 request methods, resources must implement methods called `do_<HTTP_METHOD>`. @example Handling `PATCH` requests def do_PATCH request, response response['Content-Type'] = 'text/plain' response.body = [ 'Hello world!' ] end @abstract @return [void] @raise [HTTPStatus, RuntimeError]
@!attribute [r] get_etag
The ETag of this resource. If your classes implement this method, then an `ETag:` response header is generated automatically when appropriate. This allows clients to perform conditional requests, by sending an `If-Match:` or `If-None-Match:` request header. These conditions are then asserted for you automatically. Make sure your entity tag is a properly formatted string. In ABNF: entity-tag = [ "W/" ] quoted-string quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) qdtext = <any TEXT except <">> quoted-pair = "\" CHAR @abstract @return [String] @see http://tools.ietf.org/html/rfc2616#section-14.19 RFC2616 section 14.19
@!attribute [r] get_last_modified
Last modification of this resource. If your classes implement this method, then a `Last-Modified:` response header is generated automatically when appropriate. This allows clients to perform conditional requests, by sending an `If-Modified-Since:` or `If-Unmodified-Since:` request header. These conditions are then asserted for you automatically. @abstract @return [Array<(Time, Boolean)>] The timestamp, and a flag indicating if the timestamp is a strong validator. @see http://tools.ietf.org/html/rfc2616#section-14.29 RFC2616 section 14.29
@!method destroy()
@return [Hash, nil] an optional header hash.
Attributes
The canonical path of this resource. @return [URI]
Public Class Methods
This callback includes all methods of {ClassMethods} into all classes that include {Resource}, to make them available as a tiny DSL. @api private
# File lib/rackful/resource.rb, line 194 def self.included(base) base.extend ClassMethods end
Public Instance Methods
Adds ‘ETag:` and `Last-Modified:` response headers.
# File lib/rackful/resource.rb, line 455 def default_headers r = {} r['ETag'] = self.get_etag \ if self.respond_to?( :get_etag ) r['Last-Modified'] = self.get_last_modified[0].httpdate \ if self.respond_to?( :get_last_modified ) r end
Does this resource exist?
For example, a client can ‘PUT` to a URL that doesn’t refer to a resource yet. In that case, your {Server#initialize resource registry} can produce an empty resource to handle the ‘PUT` request. `HEAD` and `GET` requests will still yield `404 Not Found`.
@return [Boolean] The default implementation returns ‘false`.
# File lib/rackful/resource.rb, line 247 def empty? false end
Wrapper around {#do_METHOD do_GET} @api private @return [void] @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
# File lib/rackful/resource.rb, line 379 def http_DELETE request, response raise HTTP404NotFound if self.empty? raise HTTP405MethodNotAllowed, self.http_methods unless self.respond_to?( :destroy ) response.status = Rack::Utils.status_code( :no_content ) if headers = self.destroy( request, response ) response.headers.merge! headers end end
@api private @param request [Rackful::Request] @param response [Rack::Response] @return [void] @raise [HTTP404NotFound, HTTP405MethodNotAllowed]
# File lib/rackful/resource.rb, line 324 def http_GET request, response raise HTTP404NotFound if self.empty? # May throw HTTP406NotAcceptable: serializer = self.serializer( request ) response['Content-Type'] = serializer.content_type response.status = Rack::Utils.status_code( :ok ) response.headers.merge! self.default_headers if serializer.respond_to? :headers response.headers.merge!( serializer.headers ) end response.body = serializer end
Handles a HEAD request.
This default handler for HEAD requests calls {#http_GET}, and then strips off the response body.
Feel free to override this method at will. @return [self]
# File lib/rackful/resource.rb, line 307 def http_HEAD request, response self.http_GET request, response response['Content-Length'] = response.body.reduce(0) do |memo, s| memo + bytesize(s) end.to_s # Is this really necessary? Doesn't Rack automatically strip the response # body for HEAD requests? response.body = [] end
Handles an OPTIONS request.
As a courtesy, this module implements a default handler for OPTIONS requests. It creates an ‘Allow:` header, listing all implemented HTTP/1.1 methods for this resource. By default, an `HTTP/1.1 204 No Content` is returned (without an entity body).
Feel free to override this method at will. @return [void] @raise [HTTP404NotFound] ‘404 Not Found` if this resource is empty.
# File lib/rackful/resource.rb, line 294 def http_OPTIONS request, response response.status = Rack::Utils.status_code :no_content response.header['Allow'] = self.http_methods.join ', ' end
@api private @return [void] @raise [HTTP404NotFound, HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
resource doesn’t implement the `PATCH` method or can’t handle the provided request body media type.
# File lib/rackful/resource.rb, line 395 def http_PATCH request, response raise HTTP404NotFound if self.empty? response.status = :no_content begin parse(request, response) rescue HTTP405MethodNotAllowed => e raise e unless self.respond_to? :do_PATCH self.do_PATCH( request, response ) end response.headers.merge! self.default_headers end
@api private @return [void] @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
resource doesn’t implement the `POST` method or can’t handle the provided request body media type.
# File lib/rackful/resource.rb, line 413 def http_POST request, response begin parse(request, response) rescue HTTP405MethodNotAllowed => e raise e unless self.respond_to? :do_POST self.do_POST( request, response ) end end
@api private @return [void] @raise [HTTP415UnsupportedMediaType, HTTP405MethodNotAllowed] if the
resource doesn’t implement the `PUT` method or can’t handle the provided request body media type.
# File lib/rackful/resource.rb, line 428 def http_PUT request, response response.status = Rack::Utils.status_code( self.empty? ? :created : :no_content ) begin parse(request, response) rescue HTTP405MethodNotAllowed => e raise e unless self.respond_to? :do_PUT self.do_PUT( request, response ) end response.headers.merge! self.default_headers end
Wrapper around {#do_METHOD} @api private @return [void] @raise [HTTPStatus] ‘405 Method Not Allowed` if the resource doesn’t implement
the request method.
# File lib/rackful/resource.rb, line 445 def http_method request, response method = request.request_method.to_sym if ! self.respond_to?( :"do_#{method}" ) raise HTTP405MethodNotAllowed, self.http_methods end self.send( :"do_#{method}", request, response ) end
List of all HTTP/1.1 methods implemented by this resource.
This works by inspecting all the {#do_METHOD} methods this object implements. @return [Array<Symbol>] @api private
# File lib/rackful/resource.rb, line 264 def http_methods r = [] if self.empty? if self.class.all_media_types[:PUT] r << :PUT end else r.push( :OPTIONS, :HEAD, :GET ) r << :DELETE if self.respond_to?( :destroy ) end self.class.public_instance_methods.each do |instance_method| if /\Ado_([A-Z]+)\z/ === instance_method r << $1.to_sym end end r end
Parse and “execute” the request body. @param request [Rackful::Request] @param response [Rack::Response] @return [Parser, nil] a {Parser}, or nil if the request entity is empty @raise [HTTP415UnsupportedMediaType] if no parser can be found for the request entity @api private
# File lib/rackful/resource.rb, line 205 def parse request, response unless request.content_length || 'chunked' == request.env['HTTP_TRANSFER_ENCODING'] raise HTTP411LengthRequired end request_media_type = request.media_type.to_s supported_media_types = [] all_parsers = self.class.all_parsers[ request.request_method.to_sym ] || [] all_parsers.each do |mt, p| if File.fnmatch( mt, request_media_type, File::FNM_PATHNAME ) return p.parse( request, response, self ) end supported_media_types << mt end raise( HTTP405MethodNotAllowed, self.http_methods ) if supported_media_types.empty? raise( HTTP415UnsupportedMediaType, supported_media_types.uniq ) end
The best serializer to represent this resource, given the current HTTP request. @param request [Request] the current request @param require_match [Boolean] this flag determines what must happen if the
client sent an `Accept:` header, and we cannot serve any of the acceptable media types. **`TRUE`** means that an {HTTP406NotAcceptable} exception is raised. **`FALSE`** means that the content-type with the highest quality is returned.
@return [Serializer] @raise [HTTP406NotAcceptable]
# File lib/rackful/resource.rb, line 347 def serializer( request, require_match = true ) q_values = request.q_values # Array<Array(type, quality)> default_serializer = # @formatter:off # Hash{ String( content_type ) => Array( Serializer, Float(quality) ) } self.class.all_serializers. # Array< Array( Serializer, Float(quality) ) > values.sort_by(&:last). # Serializer last.first # @formatter:on best_match = [ default_serializer, 0.0, default_serializer.content_types.first ] q_values.each do |accept_media_type, accept_quality| self.class.all_serializers.each_pair do |content_type, sq| media_type = content_type.split(/\s*;\s*/).first if File.fnmatch( accept_media_type, media_type, File::FNM_PATHNAME ) and best_match.nil? || best_match[1] < ( accept_quality * sq[1] ) best_match = [ sq[0], sq[1], content_type ] end end end if require_match and best_match[1] <= 0.0 raise( HTTP406NotAcceptable, self.class.all_serializers.keys() ) end best_match[0].new(request, self, best_match[2]) end
# File lib/rackful/resource.rb, line 234 def title self.uri.segments.last || self.class.to_s end
@todo documentation
# File lib/rackful/resource.rb, line 252 def to_rackful self end
# File lib/rackful/resource.rb, line 229 def uri=( uri ) @uri = uri.kind_of?(URI::Generic) ? uri.dup : URI(uri).normalize end