Goal ⚽

A library for parsing and validating parameters. It takes the params (e.g. from an Phoenix controller action), 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, it being Ecto-based, and it validates data using pure functions instead of building embedded Ecto.Schema in the background.

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 regexes.

Installation

Add goal to the list of dependencies in mix.exs:

def deps do
[
{:goal, "~> 0.1.0"}
]
end

Usage

There are several patterns you can choose to validate parameters using Goal.

Using module attributes

defmodule MyApp.SomeController do
import Goal
@schema %{
id: [format: :uuid, required: true],
name: [min: 3, max: 20, required: true]
}
def create(conn, params) do
with {:ok, attrs} <- validate_params(params, @schema) do
...
end
end
end

Using private functions

defmodule MyApp.SomeController do
import Goal
def create(conn, params) do
with {:ok, attrs} <- validate_params(params, schema()) do
...
end
end
defp schema do
%{
id: [format: :uuid, required: true],
name: [min: 3, max: 20, required: true]
}
end
end

Using 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
end
end
end

Features

Defining validations

Define field types with :type:

The default field type is :string. That means you don't have to define this field in the schema if the value will be a string.

Define map fields with :properties.

Define string validations:

Define integer validations:

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, 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

Goal efficiently builds error changesets for nested maps. There is no limitation on depth. If the schema is becoming too verbose, you could consider splitting up the schema into reusable components.

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]}}}}}

Using 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 as:

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]}}}}}

Human-readable error messages

Use Goal.traverse_errors/2 to build readable errors. Ecto and Phoenix by default use Ecto.Changeset.traverse_errors/2, which works for embedded Ecto schemas but not for the plain nested maps used by Goal.

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

Roadmap

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! 🙇