View Source Goal (goal v0.1.1)

Goal is a parameter validation library based on Ecto.

Goal takes the params (e.g. from an Phoenix controller), validates them against a schema, and returns an atom-based map or an error changeset. It's based on Ecto, so every validation that you have for database fields can be applied in validating parameters.

Goal is different from other validation libraries because of its syntax, being Ecto-based, and validating data using functions from Ecto.Changeset instead of building embedded Ecto.Schemas in the background.

Additionally, Goal allows you to configure your own regexes. This is helpful in case of backward compatibility, where Goal's defaults might not match your production system's behavior.

usage

Usage

Goal's entry point is Goal.validate_params/2, which receives the parameters and a validation schema. The parameters must be a map, and can be string-based or atom-based. Goal needs a validation schema (also a map) to parse and validate the parameters. You can build one with the defschema macro:

defmodule MyApp.SomeController do
  import Goal
  import Goal.Syntax

  def create(conn, params) do
    with {:ok, attrs} <- validate_params(params, schema()) do
      ...
    end
  end

  defp schema do
    defschema do
      required :uuid, :string, format: :uuid
      required :name, :string, min: 3, max: 3
      optional :age, :integer, min: 0, max: 120
      optional :gender, :enum, values: ["female", "male", "non-binary"]

      optional :data, :map do
        required :color, :string
        optional :money, :decimal
        optional :height, :float
      end
    end
  end
end

The defschema macro converts the given structure into a validation schema at compile-time. You can also use the basic syntax like in the example below. The basic syntax is what defschema compiles to.

defmodule MyApp.SomeController do
  import Goal

  @schema %{
    id: [format: :uuid, required: true],
    name: [min: 3, max: 20, required: true],
    age: [type: :integer, min: 0, max: 120],
    gender: [type: :enum, values: ["female", "male", "non-binary"]],
    data: [
      type: :map,
      properties: %{
        color: [required: true],
        money: [type: :decimal],
        height: [type: :float]
      }
    ]
  }

  def create(conn, params) do
    with {:ok, attrs} <- validate_params(params, @schema) do
      ...
    end
  end
end

features

Features

bring-your-own-regex

Bring your own regex

Goal has sensible defaults for string format validation. If you'd like to use your own regex, e.g. for validating email addresses or passwords, then you can add your own regex in the configuration:

config :goal,
  uuid_regex: ~r/^[[:alpha:]]+$/,
  email_regex: ~r/^[[:alpha:]]+$/,
  password_regex: ~r/^[[:alpha:]]+$/,
  url_regex: ~r/^[[:alpha:]]+$/

deeply-nested-maps

Deeply nested maps

Goal efficiently builds error changesets for nested maps, and has support for lists of nested maps. There is no limitation on depth.

params = %{
  "nested_map" => %{
    "map" => %{
      "inner_map" => %{
        "id" => 123,
        "list" => [1, 2, 3]
      }
    }
  }
}

schema = %{
  nested_map: [
    type: :map,
    properties: %{
      inner_map: [
        type: :map,
        properties: %{
          map: [
            type: :map,
            properties: %{
              id: [type: :integer, required: true],
              list: [type: {:array, :integer}]
            }
          ]
        }
      ]
    }
  ]
}

iex(3)> Goal.validate_params(params, schema)
{:ok, %{nested_map: %{inner_map: %{map: %{id: 123, list: [1, 2, 3]}}}}}

use-defschema-to-reduce-boilerplate

Use defschema to reduce boilerplate

Goal provides a macro called Goal.Syntax.defschema/1 to build validation schemas without all the boilerplate code. The previous example of deeply nested maps can be rewritten to:

import Goal.Syntax

params = %{...}

schema =
  defschema do
    optional :nested_map, :map do
      optional :inner_map, :map do
        optional :map, :map do
          required :id, :integer
          optional :list, {:array, :integer}
        end
      end
    end
  end

iex(3)> Goal.validate_params(params, schema)
{:ok, %{nested_map: %{inner_map: %{map: %{id: 123, list: [1, 2, 3]}}}}}

readable-error-messages

Readable error messages

Use Goal.traverse_errors/2 to build readable errors. Phoenix by default uses Ecto.Changeset.traverse_errors/2, which works for embedded Ecto schemas but not for the plain nested maps used by Goal. Goal's traverse_errors/2 is compatible with (embedded) Ecto.Schemas, so you don't have to make any changes to your existing logic.

def translate_errors(changeset) do
  Goal.traverse_errors(changeset, &translate_error/1)
end

available-validations

Available validations

The field types and available validations are:

Field typeValidationsDescription
:string:equalsstring value
:isstring length
:minminimum string length
:maxmaximum string length
:trimoolean to remove leading and trailing spaces
:squishboolean to trim and collapse spaces
:format:uuid, :email, :password, :url
:subsetlist of required strings
:includedlist of allowed strings
:excludedlist of disallowed strings
:integer:equalsinteger value
:isinteger value
:minminimum integer value
:maxmaximum integer value
:greater_thanminimum integer value
:less_thanmaximum integer value
:greater_than_or_equal_tominimum integer value
:less_than_or_equal_tomaximum integer value
:equal_tointeger value
:not_equal_tointeger value
:subsetlist of required integers
:includedlist of allowed integers
:excludedlist of disallowed integers
:floatall of the integer validations
:decimalall of the integer validations
:boolean:equalsboolean value
:date:equalsdate value
:time:equalstime value
:enum:valueslist of allowed values
:map:propertiesuse :properties to define the fields
{:array, :map}:propertiesuse :properties to define the fields
{:array, inner_type}inner_type can be any of the basic types
More basic typesSee Ecto.Schema for the full list

The default basic type is :string. You don't have to define this field if you are using the basic syntax.

All field types, exluding :map and {:array, :map}, can use :equals, :subset, :included, :excluded validations.

credits

Credits

This library is based on Ecto and I had to copy and adapt Ecto.Changeset.traverse_errors/2. Thanks for making such an awesome library! 🙇

Link to this section Summary

Functions

Traverses changeset errors and applies the given function to error messages.

Validates parameters against a schema.

Link to this section Types

@type error() :: {String.t(), Keyword.t()}
@type params() :: map()
@type schema() :: map()

Link to this section Functions

Link to this function

traverse_errors(changeset, msg_func)

View Source
@spec traverse_errors(
  Ecto.Changeset.t(),
  (error() -> binary()) | (Ecto.Changeset.t(), atom(), error() -> binary())
) :: %{required(atom()) => [term()]}

Traverses changeset errors and applies the given function to error messages.

examples

Examples

iex> traverse_errors(changeset, fn {msg, opts} ->
...>   Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
...>     opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
...>   end)
...> end)
%{title: ["should be at least 3 characters"]}
Link to this function

validate_params(params, schema)

View Source
@spec validate_params(params(), schema()) ::
  {:ok, map()} | {:error, Ecto.Changeset.t()}

Validates parameters against a schema.

examples

Examples

iex> validate_params(%{"email" => "jane@example.com"}, %{email: [format: :email]})
{:ok, %{email: "jane@example.com"}}

iex> validate_params(%{"email" => "invalid"}, %{email: [format: :email]})
{:error, %Ecto.Changeset{valid?: false, errors: [email: {"has invalid format", ...}]}}