PhoenixApiToolkit.GenericRequestValidator (Phoenix API Toolkit v3.0.0) View Source

Request validator for generic (REST) requests. Meant to supplement database-level Ecto changesets. For example when creating a new entity, your database contexts / changesets will do their own validations and it would be useless to do so an extra time.

Suppose you have the following "users" resource:

  • GET /api/users
  • GET /api/users/{id}

You could have the following resource request validator and controller. The users context is not present here, but if it uses PhoenixApiToolkit.Ecto.DynamicFilters the processed query parameters can be passed straight on and will result in a single filtered query.

defmodule MyUsersRequestValidator do
  import Ecto.Changeset
  import PhoenixApiToolkit.Ecto.Validators
  import PhoenixApiToolkit.GenericRequestValidator

  @schema resource_schema(%{username: :string}, %{date_of_birth: :date})
  @entity_fields @schema |> get_entity_fields()

  def index_query(attrs) do
    @schema
    |> query_order_by(attrs)
    |> query_pagination(attrs)
    |> cast(attrs, @entity_fields)
  end
end

defmodule MyUsersController do
  use MyAppWeb, :controller
  import Plug.Conn

  alias MyUsersRequestValidator, as: ReqVal
  alias PhoenixApiToolkit.GenericRequestValidator, as: GenReqVal

  def index(conn, _params) do
    with %{valid?: true, changes: query_params} <- ReqVal.index_query(conn.query_params),
         users <- MyUsersContext.list(query_params) do
      conn |> send_resp(200, Jason.encode!(users))
    else
      _ -> conn |> send_resp(400, "idiot, your request is bad")
    end
  end

  def show(conn, _params) do
    with {valid?: true, changes: %{id: id}} <- GenReqVal.path_param(conn.path_params),
         user when not is_nil(user) <- MyUsersContext.get(id) do
      conn |> send_resp(200, Jason.encode!(user))
    else
      _ -> conn |> send_resp(400, "idiot, your request is bad")
    end
  end
end

Link to this section Summary

Types

A simple Ecto schema, embedded only, not coupled to a module or database entity

Functions

Get all the "non-meta" fields from a schema, that is, fields [:limit, :offset, :order_by, :lock_version] are filtered out.

Validates the path parameter of a generic GET request of a RESTful resource.

Validates the order_by query parameter of an index endpoint.

Validates the limit and offset query parameters of an index endpoint. If max_limit == nil, no maximum limit is enforced.

Creates a generic schema for a REST resource.

Link to this section Types

Specs

schema() :: {%{}, %{required(atom()) => atom()}}

A simple Ecto schema, embedded only, not coupled to a module or database entity

Link to this section Functions

Link to this function

get_entity_fields(schema)

View Source

Specs

get_entity_fields(schema()) :: [atom()]

Get all the "non-meta" fields from a schema, that is, fields [:limit, :offset, :order_by, :lock_version] are filtered out.

Examples

iex> resource_schema(%{first_name: :string}) |> get_entity_fields()
[:first_name, :id]

Specs

path_param(map()) :: Ecto.Changeset.t()

Validates the path parameter of a generic GET request of a RESTful resource.

Examples

# "id" is a required parameter
iex> path_param(%{}) |> Map.get(:valid?)
false

# "id" must be an integer
iex> path_param(%{"id" => "boom"}) |> Map.get(:valid?)
false

# "id" must be greater than 0
iex> path_param(%{"id" => 0}) |> Map.get(:valid?)
false

iex> path_param(%{"id" => 1}) |> Map.get(:valid?)
true
Link to this function

query_order_by(changeset, attrs, orderables)

View Source

Specs

query_order_by(map() | Ecto.Changeset.t() | schema(), map(), Enum.t()) ::
  Ecto.Changeset.t()

Validates the order_by query parameter of an index endpoint.

Examples

iex> resource_schema() |> query_order_by(%{"order_by" => "asc:last_name"}, ~w(last_name) |> MapSet.new())
#Ecto.Changeset<action: nil, changes: %{order_by: [asc: :last_name]}, errors: [], data: %{}, valid?: true>

See PhoenixApiToolkit.Ecto.Validators.validate_order_by/2 for more examples.

Link to this function

query_pagination(changeset, attrs, max_limit \\ 100, default_limit \\ 50)

View Source

Specs

query_pagination(
  map() | Ecto.Changeset.t() | schema(),
  map(),
  integer() | nil,
  integer() | nil
) :: Ecto.Changeset.t()

Validates the limit and offset query parameters of an index endpoint. If max_limit == nil, no maximum limit is enforced.

Examples

# the requested limit and offset should be in the range 0 - max_limit
iex> resource_schema() |> query_pagination(%{"limit" => 10}, 100)
#Ecto.Changeset<action: nil, changes: %{limit: 10}, errors: [], data: %{}, valid?: true>

iex> cs = resource_schema() |> query_pagination(%{"limit" => 150}, 100)
iex> cs.valid?
false

# a default limit can be set so that a default number of results is returned
iex> resource_schema() |> query_pagination(%{}, 100, 50)
#Ecto.Changeset<action: nil, changes: %{limit: 50}, errors: [], data: %{}, valid?: true>

# no max limit is enforced if `max_limit == nil`
iex> resource_schema() |> query_pagination(%{"limit" => 1_000_000}, nil)
#Ecto.Changeset<action: nil, changes: %{limit: 1000000}, errors: [], data: %{}, valid?: true>

# default limit can be disabled
iex> resource_schema() |> query_pagination(%{}, nil, nil)
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %{}, valid?: true>
Link to this function

resource_schema(extra_fields \\ %{}, comparables \\ %{})

View Source

Creates a generic schema for a REST resource.

In general, REST resources will support an integer id as a path parameter, and the index endpoint will support order_by, limit and offset. Additionally, some endpoints will support a lock_version for optimistic locking using Ecto.Changeset.optimistic_lock/3.

Additional fields can be passed along to the extra_fields parameter. Fields that can (usefully) be compared with smaller than / greater than comparisons can be passed in comparables. The value you pass AND a "_lt" (smaller than) and "_gte" (greater than or equal to) variant will be added to the schema.

Examples

# the result can be fed to cast/3
iex> resource_schema() |> Ecto.Changeset.cast(%{}, [])
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %{}, valid?: true>

# an extended schema can be created by providing a map of fields
iex> resource_schema(%{first_name: :string})
{%{}, %{first_name: :string, id: :integer, limit: :integer, lock_version: :integer, offset: :integer, order_by: :string}}

# fields passed to the "comparables" parameter are added literally AND with _lt and _gte variants
iex> resource_schema(%{}, %{date_of_birth: :date}) |> elem(1) |> Map.has_key?(:date_of_birth_lt)
true