plug_rest v0.14.0 PlugRest.Resource behaviour View Source

Define callbacks and REST semantics for a Resource behaviour

Based on Cowboy's cowboy_rest module. It operates on a Plug connection and a handler module which implements one or more of the optional callbacks.

For example, the route:

resource "/users/:username", MyApp.UserResource

will invoke the init/2 function of MyApp.UserResource if it exists and then continue executing to determine the state of the resource. By default the resource must implement a to_html content handler which returns a "text/html" representation of the resource.

defmodule MyApp.UserResource do
  use PlugRest.Resource

  def init(conn, state) do
    {:ok, conn, state}
  end

  def allowed_methods(conn, state) do
    {["GET"], conn, state}
  end

  def resource_exists(%{params: params} = conn, _state)
    username = params["username"]
    # Look up user
    state = %{name: "John Doe", username: username}
    {true, conn, state}
  end

  def content_types_provided(conn, state) do
    {[{"text/html", :to_html}], conn, state}
  end

  def to_html(conn, %{name: name} = state) do
    {"<p>Hello, #{name}</p>", conn, state}
  end
end

Each callback accepts a %Plug.Conn{} struct and the current state of the resource, and returns a three-element tuple of the form {value, conn, state}.

The resource callbacks are named below, along with their default values. Some functions are skipped if they are undefined. Others have no default value.

allowed_methods        : ["GET", "HEAD", "OPTIONS"]
allow_missing_post     : false
charsets_provided      : skip
content_types_accepted : none
content_types_provided : [{{"text", "html", %{}}, :to_html}]
delete_completed       : true
delete_resource        : false
expires                : nil
forbidden              : false
generate_etag          : nil
is_authorized          : true
is_conflict            : false
known_methods          : ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
languages_provided     : skip
last_modified          : nil
malformed_request      : false
moved_permanently      : false
moved_temporarily      : false
multiple_choices       : false
options                : :ok
previously_existed     : false
resource_exists        : true
service_available      : true
uri_too_long           : false
valid_content_headers  : true
valid_entity_length    : true
variances              : []

You must also define the content handler callbacks that are specified through content_types_accepted/2 and content_types_provided/2. It is conventional to name the functions after the content types that they handle, such as from_html and to_html.

The handler function which provides a representation of the resource must return a three element tuple of the form {body, conn, state}, where body is one of:

  • binary(), which will be sent with send_resp/3
  • {:chunked, Enum.t}, which will use send_chunked/2
  • {:file, binary()}, which will use send_file/3

You can halt the resource handling from any callback and return a manual response like so:

response = send_resp(conn, status_code, resp_body)
{:stop, response, state}

The content accepted handlers defined in content_types_accepted will be called for POST, PUT, and PATCH requests. By default, the response body will be empty. If desired, you can set the response body like so:

conn2 = put_rest_body(conn, "#{conn.method} was successful")
{true, conn2, state}

Configuration

You can change some defaults by configuring the :plug_rest app in your config.exs file.

To change the default known_methods for all Resources:

config :plug_rest,
  known_methods: ["GET", "HEAD", "OPTIONS", "TRACE"]

If a Resource implements the known_methods callback, that list always takes precedence over the default list.

Plug Pipeline

You can create a custom Plug pipeline within your resource using Plug.Builder:

defmodule MessageResource do
  use PlugRest.Resource

  # Add the Builder to your resource
  use Plug.Builder

  # Add your custom plugs
  plug :hello

  # Finally, call the :rest plug to start executing the REST callbacks
  plug :rest

  # REST Callbacks
  def to_html(conn, state) do
    {conn.private.message, conn, state}
  end

  # Example custom plug function
  def hello(conn, _opts) do
    put_private(conn, :message, "Hello")
  end
end

Link to this section Summary

Types

The callback accepting a representation of the resource for a content-type

A WWW-Authenticate header value

A charset written in lowercase

A %Plug.Conn{} struct representing the connection

A content-type accepted handler, comprising a media type and acccept callback

A content-type provided handler, comprising a media type and provide callback

An entity tag

The name of an HTTP header

A language tag written in lowercase

A representation of a content-type match

An HTTP method written in uppercase

An ok callback value

The callback providing a representation of the resource for a content-type

A Module adopting the PlugRest.Resource behaviour

The state of the resource

A stop callback value

A URI

Functions

Returns the REST response body if it has been set

Manually sets the REST response body in the connection

Executes the REST state machine with a connection and resource

Callbacks

Returns whether POST is allowed when the resource doesn't exist

Returns the list of allowed methods

Returns the list of charsets the resource provides

Returns the list of content-types the resource accepts

Returns the list of content-types the resource provides

Returns whether the delete action has been completed

Deletes the resource

Returns the date of expiration of the resource

Returns whether access to the resource is forbidden

Returns the entity tag of the resource

Sets up the connection and handler state before other REST callbacks

Returns whether the user is authorized to perform the action

Returns whether the PUT action results in a conflict

Returns the list of known methods

Returns the list of languages the resource provides

Returns the date of last modification of the resource

Returns whether the request is malformed

Returns whether the resource was permanently moved

Returns whether the resource was temporarily moved

Returns whether there are multiple representations of the resource

Handles a request for information

Returns whether the resource existed previously

Returns whether the resource exists

Returns whether the service is available

Returns whether the requested URI is too long

Returns whether the content-* headers are valid

Returns whether the request body length is within acceptable boundaries

Return the list of headers that affect the representation of the resource

Link to this section Types

Link to this type

accept_resource()

View Source
accept_resource() :: atom()

The callback accepting a representation of the resource for a content-type

Link to this type

auth_head()

View Source
auth_head() :: binary()

A WWW-Authenticate header value

A charset written in lowercase

A %Plug.Conn{} struct representing the connection

Link to this type

content_type_a()

View Source
content_type_a() :: {binary() | media_type(), accept_resource()}

A content-type accepted handler, comprising a media type and acccept callback

Link to this type

content_type_p()

View Source
content_type_p() :: {binary() | media_type(), provide_resource()}

A content-type provided handler, comprising a media type and provide callback

Link to this type

etag()

View Source
etag() :: binary() | {:weak | :strong, binary()}

An entity tag

Examples

# ETag: W/"etag-header-value"
{:weak, "etag-header-value"}

# ETag: "etag-header-value"
{:strong, "etag-header-value"}

# ETag: "etag-header-value"
{"\"etag-header-value\""}
Link to this type

header_name()

View Source
header_name() :: binary()

The name of an HTTP header

Link to this type

language()

View Source
language() :: binary()

A language tag written in lowercase

Link to this type

media_type()

View Source
media_type() :: {binary(), binary(), %{required(binary()) => binary()} | :*}

A representation of a content-type match

An HTTP method written in uppercase

An ok callback value

Link to this type

provide_resource()

View Source
provide_resource() :: atom()

The callback providing a representation of the resource for a content-type

Link to this type

resource()

View Source
resource() :: atom()

A Module adopting the PlugRest.Resource behaviour

The state of the resource

Link to this type

stop()

View Source
stop() :: {:stop, conn(), state()}

A stop callback value

A URI

Link to this section Functions

Link to this function

get_rest_body(conn)

View Source
get_rest_body(conn()) :: binary() | nil

Returns the REST response body if it has been set

Link to this function

put_rest_body(conn, resp_body)

View Source
put_rest_body(conn(), binary()) :: conn()

Manually sets the REST response body in the connection

Link to this function

upgrade(conn, resource, resource_state)

View Source
upgrade(conn(), resource(), state()) :: conn()

Executes the REST state machine with a connection and resource

Accepts a Plug.Conn struct, a PlugRest.Resource module, and the initial state of the resource, and executes the REST state machine.

Link to this section Callbacks

Link to this callback

allow_missing_post(conn, state)

View Source (optional)
allow_missing_post(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether POST is allowed when the resource doesn't exist

  • Methods: POST
  • Default: false

This function will be called when resource_exists is false and the request method is POST. Returning true means the missing resource can process the enclosed representation, and the resource's content accepted handler will be invoked.

Returning true means POST should update an existing resource and create one if it is missing.

Returning false means POST to a missing resource will send 404 Not Found.

Examples

def allow_missing_post(conn, state) do
  {true, conn, state}
end
Link to this callback

allowed_methods(conn, state)

View Source (optional)
allowed_methods(conn(), state()) :: {[method()], conn(), state()} | stop()

Returns the list of allowed methods

  • Methods: all
  • Default: ["GET", "HEAD", "OPTIONS"]

Methods are case sensitive and should be given in uppercase.

If the request uses a method that is not allowed, the resource will respond 405 Method Not Allowed.

Examples

def allowed_methods(conn, state) do
  {["GET,", "HEAD", "OPTIONS"], conn, state}
end
Link to this callback

charsets_provided(conn, state)

View Source (optional)
charsets_provided(conn(), state()) :: {[charset()], conn(), state()} | stop()

Returns the list of charsets the resource provides

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: Skip to the next step if undefined.

The list must be ordered by priority.

The first charset will be chosen if the client does not send an accept-charset header, or the first that matches.

The charset should be returned as a lowercase string.

Examples

def charsets_provided(conn, state) do
  {["utf-8"], conn, state}
end
Link to this callback

content_types_accepted(conn, state)

View Source (optional)
content_types_accepted(conn(), state()) ::
  {[content_type_a()], conn(), state()} | stop()

Returns the list of content-types the resource accepts

  • Methods: POST, PUT, PATCH
  • Default: Crash if undefined.

The list must be ordered by priority.

Each content-type can be given either as a string like "text/html"; or a tuple in the form {type, subtype, params}, where params can be %{} (no params acceptable), :* (all params acceptable), or a map of acceptable params %{"level" => "1"}.

If no content types match, a 415 Unsupported Media Type response will be sent.

Examples

def content_types_accepted(conn, state) do
  {[{"application/json", :from_json}], conn, state}
end

The content accepted handler value is the name of the callback that will be called if the content-type matches. It is defined as follows.

  • Value type: true | {true, URL} | false
  • Default: Crash if undefined.

Process the request body

This function should create or update the resource based on the request body and the method used. Consult the Plug.Conn and Plug.Parsers docs for information on parsing and reading the request body params.

Returning true means the process was successful. Returning {true, URL} means a new resource was created at that location.

Returning false will send a 400 Bad Request response.

If a response body must be sent, the appropriate media-type, charset and language can be manipulated using Plug.Conn. The body can be set using put_rest_body/2.

Examples

# post accepted
def from_json(conn, :success = state) do
  conn = put_rest_body(conn, "{\"status\": \"ok\"}")
  {true, conn, state}
end

# post create and redirect
def from_json(conn, :redirect = state) do
  {{true, "new_url/1234"}, conn, state}
end

# post error
def from_json(conn, :error = state) do
  {false, conn, state}
end
Link to this callback

content_types_provided(conn, state)

View Source (optional)
content_types_provided(conn(), state()) ::
  {[content_type_p()], conn(), state()} | stop()

Returns the list of content-types the resource provides

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: [{{"text", "html", %{}}, :to_html}]

The list must be ordered by priority.

Each content-type can be given either as a string like "text/html"; or a tuple in the form {type, subtype, params}, where params can be %{} (no params acceptable), :* (all params acceptable), or a map of acceptable params %{"level" => "1"}.

PlugRest will choose the content-type through content negotiation with the client.

If content negotiation fails, a 406 Not Acceptable response will be sent.

Examples

def content_types_provided(conn, state) do
  {[{"application/json", :to_json}], conn, state}
end

The content provided handler names a function that will return a representation of the resource using that content-type. It is defined as follows.

  • Methods: GET, HEAD
  • Value type: binary() | {:chunked, Enum.t} | {:file, binary()}
  • Default: Crash if undefined.

Return the response body.

Examples

def to_json(conn, state) do
  {"{}", conn, state}
end
Link to this callback

delete_completed(conn, state)

View Source (optional)
delete_completed(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the delete action has been completed

  • Methods: DELETE
  • Default: true

This function is called after a successful delete_resource. Returning true means the delete has completed. Returning false means the request was accepted but may not have finished, and responds with 202 Accepted.

Examples

def delete_completed(conn, state) do
  {true, conn, state}
end
Link to this callback

delete_resource(conn, state)

View Source (optional)
delete_resource(conn(), state()) :: {boolean(), conn(), state()} | stop()

Deletes the resource

  • Methods: DELETE
  • Default: false

Returning true means the delete request can be enacted. Returning false will send a 500 error.

Examples

def delete_resource(conn, state) do
  {true, conn, state}
end
Link to this callback

expires(conn, state)

View Source (optional)
expires(conn(), state()) ::
  {:calendar.datetime() | binary() | nil, conn(), state()} | stop()

Returns the date of expiration of the resource

  • Methods: GET, HEAD
  • Default: nil

This date will be sent as the value of the expires header. The date can be specified as a datetime() tuple or a string.

Examples

def expires(conn, state) do
  {{{2012, 9, 21}, {22, 36, 14}}, conn, state}
end
Link to this callback

forbidden(conn, state)

View Source (optional)
forbidden(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether access to the resource is forbidden

  • Methods: all
  • Default: false

Returning true will send a 403 Forbidden response.

Examples

def forbidden(conn, state) do
  {false, conn, state}
end
Link to this callback

generate_etag(conn, state)

View Source (optional)
generate_etag(conn(), state()) ::
  {etag(), conn(), state()} | {nil, conn(), state()} | stop()

Returns the entity tag of the resource

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: nil

This value will be sent as the value of the etag header.

Examples

# ETag: W/"etag-header-value"
def generate_etag(conn, state) do
  {{:weak, "etag-header-value"}, conn, state}
end

# ETag: "etag-header-value"
def generate_etag(conn, state) do
  {{:strong, "etag-header-value"}, conn, state}
end

# ETag: "etag-header-value"
def generate_etag(conn, state) do
  {"\"etag-header-value\""}, conn, state}
end
Link to this callback

init(conn, state)

View Source (optional)
init(conn(), state()) :: ok() | stop()

Sets up the connection and handler state before other REST callbacks

  • Methods: all
  • Default: :ok

Examples

  def init(conn, state) do
    {:ok, conn, state}
  end
Link to this callback

is_authorized(conn, state)

View Source (optional)
is_authorized(conn(), state()) ::
  {true | {false, auth_head()}, conn(), state()} | stop()

Returns whether the user is authorized to perform the action

  • Methods: all
  • Default: true

Returning {false, binary()} will send a 401 Unauthorized response. The value of the binary() will be set as the WWW-authenticate header.

Examples

def is_authorized(conn, state) do
  {true, conn, state}
end
Link to this callback

is_conflict(conn, state)

View Source (optional)
is_conflict(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the PUT action results in a conflict

  • Methods: PUT
  • Default: false

Returning true will send a 409 Conflict response.

Examples

def is_conflict(conn, state) do
  {false, conn, state}
end
Link to this callback

known_methods(conn, state)

View Source (optional)
known_methods(conn(), state()) :: {[method()], conn(), state()} | stop()

Returns the list of known methods

  • Methods: all
  • Default: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]

Specifies the full list of HTTP methods known by the server, even if they aren't allowed in this resource.

The default list can be configured in config.exs:

config :plug_rest,
  known_methods: ["GET", "HEAD", "OPTIONS", "TRACE"]

If a Resource implements the known_methods callback, that list always takes precedence over the default list.

Methods are case sensitive and should be given in uppercase.

Examples

def known_methods(conn, state) do
  {["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
   conn, state}
end
Link to this callback

languages_provided(conn, state)

View Source (optional)
languages_provided(conn(), state()) :: {[language()], conn(), state()} | stop()

Returns the list of languages the resource provides

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: Skip to the next step if undefined.

The first language will be chosen if the client does not send an accept-language header, or the first that matches.

The language should be returned as a lowercase binary.

Examples

def languages_provided(conn, state) do
  {["en"], conn, state}
end
Link to this callback

last_modified(conn, state)

View Source (optional)
last_modified(conn(), state()) ::
  {:calendar.datetime(), conn(), state()} | {nil, conn(), state()} | stop()

Returns the date of last modification of the resource

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: nil

Returning a datetime() tuple will set the last-modified header and be used for comparison in conditional if-modified-since and if-unmodified-since requests.

Examples

def last_modified(conn, state) do
  {{{2012, 9, 21}, {22, 36, 14}}, conn, state}
end
Link to this callback

malformed_request(conn, state)

View Source (optional)
malformed_request(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the request is malformed

  • Methods: all
  • Default: false

Returning true will send a 400 Bad Request response.

Examples

def malformed_request(conn, state) do
  {false, conn, state}
end
Link to this callback

moved_permanently(conn, state)

View Source (optional)
moved_permanently(conn(), state()) ::
  {{true, uri()} | false, conn(), state()} | stop()

Returns whether the resource was permanently moved

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: false

Returning {true, URI} will send a 301 Moved Permanently response with the URI in the Location header.

Examples

def moved_permanently(conn, state) do
  {{true, "/new_location"}, conn, state}
end
Link to this callback

moved_temporarily(conn, state)

View Source (optional)
moved_temporarily(conn(), state()) ::
  {{true, uri()} | false, conn(), state()} | stop()

Returns whether the resource was temporarily moved

  • Methods: GET, HEAD, POST, PATCH, DELETE
  • Default: false

Returning {true, URI} will send a 307 Temporary Redirect response with the URI in the Location header.

Examples

def moved_temporarily(conn, state) do
  {{true, "/new_location"}, conn, state}
end
Link to this callback

multiple_choices(conn, state)

View Source (optional)
multiple_choices(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether there are multiple representations of the resource

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: false

Returning true means that multiple representations of the resource are possible and one cannot be chosen automatically. This will send a 300 Multiple Choices response. The response body should include information about the different representations using set_rest_body/2. The content-type that was already negotiated can be retrieved by calling:

[content-type] = get_resp_header(conn, "content-type")

Examples

def multiple_choices(conn, state) do
  {false, conn, state}
end
Link to this callback

options(conn, state)

View Source (optional)
options(conn(), state()) :: {:ok, conn(), state()} | stop()

Handles a request for information

  • Methods: OPTIONS
  • Default: true

The response should inform the client the communication options available for this resource.

By default, PlugRest will send a 200 OK response with the list of supported methods in the Allow header.

Examples

def options(conn, state) do
  {:ok, conn, state}
end
Link to this callback

previously_existed(conn, state)

View Source (optional)
previously_existed(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the resource existed previously

  • Methods: GET, HEAD, POST, PATCH, DELETE
  • Default: false

Returning true will invoke moved_permanently and moved_temporarily to determine whether to send a 301 Moved Permanently, 307 Temporary Redirect, or 410 Gone response.

Examples

def previously_existed(conn, state) do
  {false, conn, state}
end
Link to this callback

resource_exists(conn, state)

View Source (optional)
resource_exists(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the resource exists

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: true

Returning false will send a 404 Not Found response, unless the method is POST and allow_missing_post is true.

Examples

def resource_exists(conn, state) do
  {true, conn, state}
end
Link to this callback

service_available(conn, state)

View Source (optional)
service_available(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the service is available

  • Methods: all
  • Default: true

Use this to confirm all backend systems are up.

Returning false will send a 503 Service Unavailable response.

Examples

def service_available(conn, state) do
  {true, conn, state}
end
Link to this callback

uri_too_long(conn, state)

View Source (optional)
uri_too_long(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the requested URI is too long

  • Methods: all
  • Default: false

Returning true will send a 414 Request-URI Too Long response.

Examples

def uri_too_long(conn, state) do
  {false, conn, state}
end
Link to this callback

valid_content_headers(conn, state)

View Source (optional)
valid_content_headers(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the content-* headers are valid

  • Methods: all
  • Default: true

This functions should check for invalid or unknown content-* headers.

Returning false will send a 501 Not Implemented response.

Examples

def valid_content_headers(conn, state) do
  {true, conn, state}
end
Link to this callback

valid_entity_length(conn, state)

View Source (optional)
valid_entity_length(conn(), state()) :: {boolean(), conn(), state()} | stop()

Returns whether the request body length is within acceptable boundaries

  • Methods: all
  • Default: true

Returning false will send a 413 Request Entity Too Large response.

Examples

def valid_entity_length(conn, state) do
  {true, conn, state}
end
Link to this callback

variances(conn, state)

View Source (optional)
variances(conn(), state()) :: {[header_name()], conn(), state()} | stop()

Return the list of headers that affect the representation of the resource

  • Methods: GET, HEAD, POST, PUT, PATCH, DELETE
  • Default: []

This function may return a list of strings saying which headers should be included in the response's Vary header.

PlugRest will automatically add the Accept, Accept-language and Accept-charset headers to the list if the respective functions were defined in the resource.

Examples

# vary: user-agent
def variances(conn, state) do
  {["user-agent"], conn, state}
end