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:
:required- Settingrequiredtotruewill cause a validation error when a field is not present or the value isnil. Allowed values for required aretrueandfalse. The default isfalse.
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:
:truthy- Allows additional values, i.e. truthy values to be considered valid booleans by converting them totrueduring validation. Allowed value is an array of strings, number or boolean values. The default is[true, "true"]:falsy- Allows additional values, i.e. falsy values to be considered valid booleans by converting them tofalseduring validation. Allowed value is an array of strings, number or boolean values. The default is[false, "false"]
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:
:min_length- Specifies the minimum list length. Allowed values are non-negative integers.:max_length- Specifies the maximum list length. Allowed values are non-negative integers.:length- Specifies the exact list length. Allowed values are non-negative integers.:type- Specifies the data type of elements in the list. Allowed values are are atoms:atom, :boolean, :number and :string. Default value isnil. Ifnil, any element type is allowed in the list.
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:
:min- Specifies the minimum value of the field.:max- Specifies the maximum value of the field.:integer- Specifies that the number must be an integer (no floating point). Allowed values aretrueandfalse. The default isfalse.
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:
:min_length- Specifies the minimum number of characters needed in the string. Allowed values are non-negative integers.:max_length- Specifies the maximum number of characters needed in the string. Allowed values are non-negative integers.:length- Specifies the exact number of characters needed in the string. Allowed values are non-negative integers.:regex- Specifies a Regular expression that a string must match. Allowed value is a struct consisting ofpatternanderror_message, wherepatternis aRegexanderror_messageis aString.t()value. Default value for pattern isnil. If no error_message is given, the default message returned on error is"#{field} must be in a valid format".:trim- Removes additional whitespaces in a string and returns the new value. Allowed values aretrueandfalse. The default isfalse.
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.