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

uri[R]

The canonical path of this resource. @return [URI]

Public Class Methods

included(base) click to toggle source

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

default_headers() click to toggle source

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
empty?() click to toggle source

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
http_DELETE(request, response) click to toggle source

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
http_GET(request, response) click to toggle source

@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
http_HEAD(request, response) click to toggle source

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
http_OPTIONS(request, response) click to toggle source

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
http_PATCH(request, response) click to toggle source

@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
http_POST(request, response) click to toggle source

@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
http_PUT(request, response) click to toggle source

@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
http_method(request, response) click to toggle source

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
http_methods() click to toggle source

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(request, response) click to toggle source

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
serializer( request, require_match = true ) click to toggle source

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
title() click to toggle source
# File lib/rackful/resource.rb, line 234
def title
  self.uri.segments.last || self.class.to_s
end
to_rackful() click to toggle source

@todo documentation

# File lib/rackful/resource.rb, line 252
def to_rackful
  self
end
uri=( uri ) click to toggle source
# File lib/rackful/resource.rb, line 229
def uri=( uri )
  @uri = uri.kind_of?(URI::Generic) ? uri.dup : URI(uri).normalize
end