Litmus

Data validation in Elixir

Installation

If available in Hex, the package can be installed by adding litmus to your list of dependencies in mix.exs:

def deps do
[
{:litmus, "~> 0.3.0"}
]
end

Usage

Litmus validates data against a predefined schema with the Litmus.validate/2 function.

If the data is valid, the function returns {:ok, data}. The data returned will be coerced according to the schema defined.

If the data passed does not follow the rules defined in the schema, the function returns {:error, error_message}. It will also return an error when receiving a field that has not been specified in the provided schema.

iex> schema = %{
...> "id" => %Litmus.Type.Any{required: true},
...> "username" => %Litmus.Type.String{
...> min_length: 6,
...> required: true
...> },
...> "pin" => %Litmus.Type.Number{
...> min: 1000,
...> max: 9999,
...> required: true
...> },
...> "new_user" => %Litmus.Type.Boolean{
...> truthy: ["1"],
...> falsy: ["0"]
...> },
...> "account_ids" => %Litmus.Type.List{
...> max_length: 3,
...> type: :number
...> }
...> }
iex> params = %{"id" => 1, "username" => "user@123", "pin" => 1234, "new_user" => "1", "account_ids" => [1, 3, 9]}
iex> Litmus.validate(params, schema)
{:ok, %{"id" => 1, "new_user" => true, "pin" => 1234, "username" => "user@123", "account_ids" => [1, 3, 9]}}
iex> schema = %{"id" => %Litmus.Type.Any{}}
iex> params = %{"password" => 1}
iex> Litmus.validate(params, schema)
{:error, "password is not allowed"}

Currently, we support the following data types:

Data Types Supported

Litmus.Type.Any

The Any module contains options that will be common to all data types. It supports the following options:

iex> schema = %{"id" => %Litmus.Type.Any{required: true}}
iex> params = %{"id" => 1}
iex> Litmus.validate(params, schema)
{:ok, %{"id" => 1}}
iex> params = %{}
iex> Litmus.validate(params, schema)
{:error, "id is required"}

Litmus.Type.Boolean

The Boolean module contains options that will validate Boolean data types. It converts truthy and falsy values to true or false. It supports the following options:

iex> schema = %{
...> "new_user" => %Litmus.Type.Boolean{
...> truthy: ["1"],
...> falsy: ["0"]
...> }
...> }
iex> params = %{"new_user" => "1"}
iex> Litmus.validate(params, schema)
{:ok, %{"new_user" => true}}
iex> params = %{"new_user" => 0}
iex> Litmus.validate(params, schema)
{:error, "new_user must be a boolean"}

Litmus.Type.List

The List module contains options that will validate List data types. It supports the following options:

iex> schema = %{
...> "ids" => %Litmus.Type.List{
...> min_length: 1,
...> max_length: 5
...> },
...> "course_numbers" => %Litmus.Type.List{
...> length: 3,
...> type: :number
...> }
...> }
iex> params = %{"ids" => [1, "a"], "course_numbers" => [500, 523, 599]}
iex> Litmus.validate(params, schema)
{:ok, %{"ids" => [1, "a"], "course_numbers" => [500, 523, 599]}}
iex> params = %{"ids" => [1, "a"], "course_numbers" => [500, "523", 599]}
iex> Litmus.validate(params, schema)
{:error, "course_numbers must be a list of numbers"}

Litmus.Type.Number

The Number module contains options that will validate Number data types. It converts "stringified" numerical values to numbers. It supports the following options:

iex> schema = %{
...> "id" => %Litmus.Type.Number{
...> integer: true
...> },
...> "gpa" => %Litmus.Type.Number{
...> min: 0,
...> max: 4
...> }
...> }
iex> params = %{"id" => "123", "gpa" => 3.8}
iex> Litmus.validate(params, schema)
{:ok, %{"id" => 123, "gpa" => 3.8}}
iex> params = %{"id" => "123.456", "gpa" => 3.8}
iex> Litmus.validate(params, schema)
{:error, "id must be an integer"}

Litmus.Type.String

The String module contains options that will validate String data types. It converts boolean and number values to strings. It will also convert nil value to empty string. It supports the following options:

iex> schema = %{
...> "username" => %Litmus.Type.String{
...> min_length: 3,
...> max_length: 10,
...> trim: true
...> },
...> "password" => %Litmus.Type.String{
...> length: 6,
...> regex: %Litmus.Type.String.Regex{
...> pattern: ~r/^[a-zA-Z0-9_]*$/,
...> error_message: "password must be alphanumeric"
...> }
...> }
...> }
iex> params = %{"username" => " user123 ", "password" => "root01"}
iex> Litmus.validate(params, schema)
{:ok, %{"username" => "user123", "password" => "root01"}}
iex> params = %{"username" => " user123 ", "password" => "ro!_@1"}
iex> Litmus.validate(params, schema)
{:error, "password must be alphanumeric"}

Plug

Litmus comes with a Plug for easy integration with Plug's built-in router. You can automatically validate query parameters and body parameters by passing the litmus_query and litmus_body private options to each route. When declaring the plug you must include a on_error/2 function to be called when validation fails. It is recommended that you initialize this Plug between the :match and :dispatch plugs. If you want processing to stop on a validation error, be sure to halt the request with Plug.Conn.halt/1.

Example

defmodule MyRouter do
use Plug.Router
plug(Plug.Parsers, parsers: [:urlencoded, :multipart])
plug(:match)
plug(Litmus.Plug, on_error: &__MODULE__.on_error/2)
plug(:dispatch)
@schema %{
"id" => %Litmus.Type.Number{
required: true
}
}
get "/test", private: %{litmus_query: @schema} do
Plug.Conn.send_resp(conn, 200, "items")
end
post "/test", private: %{litmus_body: @schema} do
Plug.Conn.send_resp(conn, 200, "items")
end
def on_error(conn, error_message) do
conn
|> Plug.Conn.send_resp(400, error_message)
|> Plug.Conn.halt()
end
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/litmus.