defmodule Ewebmachine.Handlers do @type conn :: Plug.Conn.t @type state :: any @type halt :: {:halt, 200..599} @moduledoc """ Implement the functions described below to make decisions in the [HTTP decision tree](http_diagram.png) : - `service_available/2` - `resource_exists/2` - `is_authorized/2` - `forbidden/2` - `allow_missing_post/2` - `malformed_request/2` - `uri_too_long/2` - `known_content_type/2` - `valid_content_headers/2` - `valid_entity_length/2` - `options/2` - `allowed_methods/2` - `known_methods/2` - `content_types_provided/2` - `content_types_accepted/2` - `delete_resource/2` - `delete_completed/2` - `post_is_create/2` - `create_path/2` - `base_uri/2` - `process_post/2` - `language_available/2` - `charsets_provided/2` - `encodings_provided/2` - `variances/2` - `is_conflict/2` - `multiple_choices/2` - `previously_existed/2` - `moved_permanently/2` - `moved_temporarily/2` - `last_modified/2` - `expires/2` - `generate_etag/2` - `validate_content_checksum/2` - `ping/2` - Body-producing function, see `to_html/2` (but any function name can be used, as referenced by `content_types_provided/2` - POST/PUT processing function, see `from_json/2` (but any function name can be used, as referenced by `content_types_accepted/2` All the handlers have the same signature : ``` (conn :: Plug.Conn.t,state :: any)->{response :: any | {:halt,200..599},conn :: Plug.Conn.t, state :: any} ``` where every handler implementation : - can change or halt the plug `conn` passed as argument - can change the user state object passed from on handler to another in its arguments - returns something which will make decision in the HTTP decision tree (see documentation of functions in this module to see expected results and effects) - can return `{:halt,200..599}` to end the ewebmachine automate execution, but do not `halt` the `conn`, so the plug pipeline can continue. So each handler implementation is actually a "plug" returning a response giving information allowing to make the good response code and path in the HTTP specification. ## Usage ## The following modules will help you to construct these handlers and use them : - `Ewebmachine.Builder.Handlers` gives you macros and helpers to define the handlers and automatically create the plug to add them to your `conn.private[:resource_handlers]` - `Ewebmachine.Plug.Run` run the HTTP decision tree executing the handler implementations described in its `conn.private[:resource_handlers]`. The initial user `state` is taken in `conn.private[:machine_init]` """ @doc """ Returning non-true values will result in `503 Service Unavailable`. Default: `true` """ @spec service_available(conn,state) :: {boolean | halt,conn,state} def service_available(conn,state), do: {true,conn,state} @doc """ Returning non-true values will result in `404 Not Found`. Default: `true` """ @spec resource_exists(conn,state) :: {boolean | halt,conn,state} def resource_exists(conn,state), do: {true,conn,state} @doc """ If this returns anything other than `true`, the response will be `401 Unauthorized`. The return value will be used as the value in the `WWW-Authenticate` header, for example `Basic realm="Webmachine"`. Default: `true` """ @spec is_authorized(conn,state) :: {boolean | halt,conn,state} def is_authorized(conn,state), do: {true,conn,state} @doc """ Returning true will result in 403 Forbidden. Default: `false` """ @spec forbidden(conn,state) :: {boolean | halt,conn,state} def forbidden(conn,state), do: {false,conn,state} @doc """ If the resource accepts POST requests to nonexistent resources, then this should return `true`. Default: `false` """ @spec allow_missing_post(conn,state) :: {boolean | halt,conn,state} def allow_missing_post(conn,state), do: {false,conn,state} @doc """ Returning true will result in 400 Bad Request. Default: `false` """ @spec malformed_request(conn,state) :: {boolean | halt,conn,state} def malformed_request(conn,state), do: {false,conn,state} @doc """ Returning true will result in 414 Request-URI Too Long. Default: `false` """ @spec uri_too_long(conn,state) :: {boolean | halt,conn,state} def uri_too_long(conn,state), do: {false,conn,state} @doc """ Returning false will result in 415 Unsupported Media Type. Default: `true` """ @spec known_content_type(conn,state) :: {boolean | halt,conn,state} def known_content_type(conn,state), do: {true,conn,state} @doc """ Returning false will result in 501 Not Implemented. Default: `true` """ @spec valid_content_headers(conn,state) :: {boolean | halt,conn,state} def valid_content_headers(conn,state), do: {true,conn,state} @doc """ Returning false will result in 413 Request Entity Too Large. Default: `false` """ @spec valid_entity_length(conn,state) :: {boolean | halt,conn,state} def valid_entity_length(conn,state), do: {true,conn,state} @doc """ If the OPTIONS method is supported and is used, the return value of this function is expected to be a list of pairs representing header names and values that should appear in the response. """ @spec options(conn,state) :: {[{String.t,String.t}] | halt,conn,state} def options(conn,state), do: {[],conn,state} @doc """ If a Method not in this list is requested, then a 405 Method Not Allowed will be sent. Note that these are all-caps Strings (binary). Default: `["GET", "HEAD"]` """ @spec allowed_methods(conn,state) :: {[String.t] | halt,conn,state} def allowed_methods(conn,state), do: {["GET", "HEAD"],conn,state} @doc """ Override the known methods accepted by your automate Default: `["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS"]` """ @spec known_methods(conn,state) :: {[String.t] | halt,conn,state} def known_methods(conn,state), do: {["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS"],conn,state} @doc """ This should return a key value tuple enumerable where the key is the content-type format and the value is an atom naming the function which can provide a resource representation in that media type. Content negotiation is driven by this return value. For example, if a client request includes an Accept header with a value that does not appear as a first element in any of the return tuples, then a 406 Not Acceptable will be sent. Default: `[{"text/html", to_html}]` """ @spec content_types_provided(conn,state) :: {[{String.Chars.t,atom}] | Enum.t | halt,conn,state} def content_types_provided(conn,state), do: {[{"text/html", :to_html}],conn,state} @doc """ This is used similarly to content_types_provided, except that it is for incoming resource representations -- for example, PUT requests. Handler functions usually want to use `Plug.read_body(conn)` to access the incoming request body. Default: `[]` """ @spec content_types_accepted(conn,state) :: {[{String.Chars.t,atom}] | Enum.t | halt,conn,state} def content_types_accepted(conn,state), do: {[],conn,state} @doc """ This is called when a DELETE request should be enacted, and should return `true` if the deletion succeeded. """ @spec delete_resource(conn,state) :: {boolean | halt,conn,state} def delete_resource(conn,state), do: {false,conn,state} @doc """ This is only called after a successful `delete_resource` call, and should return `false` if the deletion was accepted but cannot yet be guaranteed to have finished. """ @spec delete_completed(conn,state) :: {boolean | halt,conn,state} def delete_completed(conn,state), do: {true,conn,state} @doc """ If POST requests should be treated as a request to put content into a (potentially new) resource as opposed to being a generic submission for processing, then this function should return true. If it does return `true`, then `create_path` will be called and the rest of the request will be treated much like a PUT to the Path entry returned by that call. Default: `false` """ @spec post_is_create(conn,state) :: {boolean | halt,conn,state} def post_is_create(conn,state), do: {false,conn,state} @doc """ This will be called on a POST request if `post_is_create` returns true. It is an error for this function not to produce a Path if post_is_create returns true. The Path returned should be a valid URI part. """ @spec create_path(conn,state) :: {nil | String.t | halt,conn,state} def create_path(conn,state), do: {nil,conn,state} @doc """ The base URI used in the location header on resource creation (when `post_is_create` is `true`), will be prepended to the `create_path` """ @spec base_uri(conn,state) :: {String.t | halt,conn,state} def base_uri(conn,state), do: {"#{conn.scheme}://#{conn.host}#{port_suffix(conn.scheme,conn.port)}",conn,state} defp port_suffix(:http,80), do: "" defp port_suffix(:https,443), do: "" defp port_suffix(_,port), do: ":#{port}" @doc """ If `post_is_create` returns `false`, then this will be called to process any POST requests. If it succeeds, it should return `true`. """ @spec process_post(conn,state) :: {boolean | halt,conn,state} def process_post(conn,state), do: {false,conn,state} @doc """ return false if language in `Plug.Conn.get_resp_header(conn,"accept-language")` is not available. """ @spec language_available(conn,state) :: {boolean | halt,conn,state} def language_available(conn,state), do: {true,conn,state} @doc """ If this is anything other than the atom `:no_charset`, it must be a `{key,value}` Enumerable where `key` is the charset and `value` is a callable function in the resource which will be called on the produced body in a GET and ensure that it is in Charset. Default: `:no_charset` """ @spec charsets_provided(conn,state) :: {:no_charset | [{String.Chars.t,(binary->binary)}] | Enum.t | halt,conn,state} def charsets_provided(conn,state), do: {:no_charset,conn,state} ## this atom causes charset-negotation to short-circuit ## the default setting is needed for non-charset responses such as image/png ## an example of how one might do actual negotiation ## [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}]; @doc """ This must be a `{key,value}` Enumerable where `key` is a valid content encoding and `value` is a callable function in the resource which will be called on the produced body in a GET and ensure that it is so encoded. One useful setting is to have the function check on method, and on GET requests return: ``` [identity: &(&1), gzip: &:zlib.gzip/1] ``` as this is all that is needed to support gzip content encoding. Default: `[{"identity", fn X-> X end}]` """ @spec encodings_provided(conn,state) :: {[{String.Chars.t,(binary->binary)}] | Enum.t | halt,conn,state} def encodings_provided(conn,state), do: {[{"identity", &(&1)}],conn,state} # this is handy for auto-gzip of GET-only resources: # [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}]; @doc """ If this function is implemented, it should return a list of strings with header names that should be included in a given response's Vary header. The standard conneg headers (`Accept`, `Accept-Encoding`, `Accept-Charset`, `Accept-Language`) do not need to be specified here as Webmachine will add the correct elements of those automatically depending on resource behavior. Default : `[]` """ @spec variances(conn,state) :: {[String.t] | halt,conn,state} def variances(conn,state), do: {[],conn,state} @doc """ If this returns `true`, the client will receive a 409 Conflict. Default : `false` """ @spec is_conflict(conn,state) :: {boolean | halt,conn,state} def is_conflict(conn,state), do: {false,conn,state} @doc """ If this returns `true`, then it is assumed that multiple representations of the response are possible and a single one cannot be automatically chosen, so a `300 Multiple Choices` will be sent instead of a `200 OK`. Default: `false` """ @spec multiple_choices(conn,state) :: {boolean | halt,conn,state} def multiple_choices(conn,state), do: {false,conn,state} @doc """ If this returns `true`, the `moved_permanently` and `moved_temporarily` callbacks will be invoked to determine whether the response should be `301 Moved Permanently`, `307 Temporary Redirect`, or `410 Gone`. Default: `false` """ @spec previously_existed(conn,state) :: {boolean | halt,conn,state} def previously_existed(conn,state), do: {false,conn,state} @doc """ If this returns `{true, uri}`, the client will receive a `301 Moved Permanently` with `uri` in the Location header. Default: `false` """ @spec moved_permanently(conn,state) :: {boolean | halt,conn,state} def moved_permanently(conn,state), do: {false,conn,state} @doc """ If this returns `{true, uri}`, the client will receive a `307 Temporary Redirect` with `uri` in the Location header. Default: `false` """ @spec moved_temporarily(conn,state) :: {boolean | halt,conn,state} def moved_temporarily(conn,state), do: {false,conn,state} @doc """ If this returns a `datetime()` (`{{day,month,year},{h,m,s}}`, it will be used for the `Last-Modified` header and for comparison in conditional requests. Default: `nil` """ @spec last_modified(conn,state) :: {nil | {{day::integer,month::integer,year::integer},{hour::integer,min::integer,sec::integer}} | halt,conn,state} def last_modified(conn,state), do: {nil,conn,state} @doc """ If not `nil`, set the expires header Default: `nil` """ @spec expires(conn,state) :: {nil | {{day::integer,month::integer,year::integer},{hour::integer,min::integer,sec::integer}} | halt,conn,state} def expires(conn,state), do: {nil,conn,state} @doc """ If not `nil`, it will be used for the ETag header and for comparison in conditional requests. Default: `nil` """ @spec generate_etag(conn,state) :: {nil | binary | halt,conn,state} def generate_etag(conn,state), do: {nil,conn,state} @doc """ if `content-md5` header exists: - If `:not_validated`, test if input body validate `content-md5`, - if return `false`, then return a bad request Useful if content-md5 validation does not imply only raw md5 hash """ @spec validate_content_checksum(conn,state) :: {:not_validated | boolean | halt,conn,state} def validate_content_checksum(conn,state), do: {:not_validated,conn,state} @doc """ Must be present and returning `pong` to prove that handlers are well linked to the automate """ @spec ping(conn,state) :: {:pang | :pong | halt,conn,state} def ping(conn,state), do: {:pang,conn,state} @doc """ Last handler, always called. Response is ignored except if it is a `halt`. """ @spec finish_request(conn,state) :: {any | halt, conn, state} def finish_request(conn,state), do: {false,conn,state} @doc """ Example body-producing function, function atom name must be referenced in `content_types_provided/2`. - If the result is an `Enumerable` of `iodata`, then the HTTP response will be a chunk encoding response where each chunk on element of the enumeration. - If the result is an iodata, then it is used as the HTTP response body """ @spec to_html(conn,state) :: {iodata | Enum.t | halt,conn,state} def to_html(conn,state), do: {"