PhoenixParams v1.1.0 PhoenixParams

A plug for Phoenix applications for validating HTTP request params.

Example usage:

defmodule ApiWeb.UserController do
  use ApiWeb, :controller
  plug Api.Plugs.Requests.User.Index when action in [:index]

  def index(conn, params) do
    # params is now a map with transformed values
    # when params names are declared as atoms in the request definition
    # params will be a map with atom keys
    user = params.user
    # ...
  end
end

defmodule Api.Plugs.Requests.User.Index do
  use Api.Plugs.Request, error_view: ApiWeb.ErrorView

  param :format,
        type: String,
        default: "json",
        in: ~w[json csv]

  param :date,
        type: Date,
        required: true,
        source: :body,
        validator: &__MODULE__.validate_date/1

  param :merchant_id,
        type: Integer,
        numericality: %{greater_than: 0}

  param :email,
        type: [String],
        validator: &__MODULE__.validate_email/1

  global_validator &__MODULE__.ensure_mid_or_email/1

  #
  # Date validators
  #

  def validate_date(date) do
    # return {:error, message} if invalid
    # otherwise the validation passes
  end

  #
  # Email validators
  #

  def validate_email({:error, _}), do: :noop

  # Invoke on separate elements
  def validate_email(list) when is_list(list) do
    validate_each(list, &validate_email/1)
  end

  def validate_email(email) do
    email =~ ~r/..../ || {:error, "is not a valid email address"}
  end

  #
  # Global validators
  #

  def ensure_mid_or_email({:error, _}) do
    params[:merchant_id] || params[:email] ||
      {:error, "merchant id or email required"}
  end
end

Synopsis: param , where:

  • name - either an atom or binary
  • options - a keyword list: type - mandatory. See below for possible values. required - optional. Either true or false (default). nested - optional. Either true or false (default). More info on nested types below validator - optional. A custom validator function in the form &Module.function/arity source - optional. Either :path, :body, :query or :auto (default) default - optional. Default param value.

Supported types of the param are:

Types can be wrapped in [], indicating the value is an array. Example:

  • [String]
  • [Integer]

Custom types are also supported. Example:

defmodule Requests.Index do
  use Api.Plugs.Request

  typedef Phone, &Coercers.phone/1
  typedef Device, &Coercers.device/1

  param :landline, type: Phone, required: true
  param :device, type: Device
end

defmodule Coercers do
  def phone(value) do
    # transform your value here to anything
  end

  # ...
end

Nested types are also supported. Example:

defmodule Requests.Shared.Address do
  param :country,
        type: String,
        required: true

  # ...
end

defmodule Requests.Index do
  param :address,
        type: Requests.Shared.Address,
        nested: true
end

Several OOTB validations exist:

  • numericality - validates numbers. Accepts a keyword list with :gt, :gte, :lt, :lte and/or :eq
  • in - validates the presence of anything in a list
  • length - validates length of a String. Accepts a keyword list with :gt, :gte, :lt, :lte and/or :eq
  • size - validates the number of elements in a list
  • regex - validates the string against a regex pattern

The package is designed to be a “plug” and:

  • it changes the input map’s string keys to atoms whenever the param names are defined as atoms
  • it discards undefined params
  • it changes (coerces) the values to whatever type they correspond to This means that a definition like param :age, type: Integer will transform an input %{"name": "baba", "age": "79"} to %{age: 79} The original, unchanged params, are still accessible through Plug’s conn.body_params and conn.query_params.
  • requires the below function to be defined in an Phoenix error view:

    def render(“400.json”, %{conn: %{assigns: %{validation_failed: errors}}}) do

    errors

    end

When the type is specified as an array, (eg. [Integer]), the validator will receive the entire array. This is done on purpose, but you can take advantage of the exposed validate_each/2 function to invoke it on each element, returning properly formatted error message:

param :merchant_id,
      type: [Integer],
      required: true,
      validator: &__MODULE__.checkmid/1

# Invoke validation on each separate element
def checkmid(list) when is_list(list) do
  validate_each(list, params, &checkmid/2)
end

# Validate element
def checkmid(mid) do
  mid > 0 || {:error, "must be positive"}
end

Errors reported by validate_each include which element failed validation:

"element at index 0: must be positive"

Finally, there is the global_validator macro, which allows you to define a callback to be invoked if all individual parameter validations passed successfully. This is useful in cases where the context validity is not dictated by the sole value of a single parameter, but rather a combination. E.g. mutually-exclusive params, at-least-one-of params, etc. are all example cases in which the request entity itself is either valid or not. The callback should accept exactly 1 argument — the request params, after coercion. Anything return value, different from {:error, reason} will be considered a pass.

The single argument expected by the __using__ macro is the error view module (usually YourAppNameWeb.ErrorView)

Link to this section Summary

Link to this section Functions

Link to this macro global_validator(func_ref, opts \\ []) (macro)
Link to this macro param(name, opts) (macro)
Link to this macro typedef(coercer_name, coercer_ref) (macro)