Packages

Norm is a system for specifying the structure of data. It can be used for validation and for generation of data. Norm does not provide any set of predicates and instead allows you to re-use any of your existing validations.

Current section

Files

Jump to
norm lib norm.ex
Raw

lib/norm.ex

defmodule Norm do
@moduledoc """
Norm provides a set of functions for specifying data.
"""
alias Norm.Conformer
alias Norm.Generatable
alias Norm.Spec
alias Norm.Spec.{
Alt,
Selection,
}
alias Norm.Schema
alias Norm.MismatchError
alias Norm.GeneratorError
@doc ~S"""
Verifies that the payload conforms to the specification
## Examples:
iex> conform(42, spec(is_integer()))
{:ok, 42}
iex> conform(42, spec(fn x -> x == 42 end))
{:ok, 42}
iex> conform(42, spec(&(&1 >= 0)))
{:ok, 42}
iex> conform(42, spec(&(&1 >= 100)))
{:error, ["val: 42 fails: &(&1 >= 100)"]}
iex> conform("foo", spec(is_integer()))
{:error, ["val: \"foo\" fails: is_integer()"]}
"""
def conform(input, spec) do
Conformer.conform(spec, input)
end
@doc ~S"""
Verifies that the payload conforms to the specification or raises a Mismatch
error
"""
def conform!(input, spec) do
case Conformer.conform(spec, input) do
{:ok, input} -> input
{:error, errors} -> raise MismatchError, errors
end
end
@doc ~S"""
Checks if the value conforms to the spec and returns a boolean.
iex> valid?(42, spec(is_integer()))
true
iex> valid?("foo", spec(is_integer()))
false
"""
def valid?(input, spec) do
case Conformer.conform(spec, input) do
{:ok, _} -> true
{:error, _} -> false
end
end
@doc ~S"""
Creates a generator from a spec or predicate.
iex> gen(spec(is_integer())) |> Enum.take(3) |> Enum.all?(&is_integer/1)
true
iex> gen(spec(is_binary())) |> Enum.take(3) |> Enum.all?(&is_binary/1)
true
iex> gen(spec(&(&1 > 0)))
** (Norm.GeneratorError) Unable to create a generator for: &(&1 > 0)
"""
def gen(spec) do
case Generatable.gen(spec) do
{:ok, generator} -> generator
{:error, error} -> raise GeneratorError, error
end
end
@doc ~S"""
Creates a new spec. Specs can be created from any existing predicates or
anonymous functions. Specs must return a boolean value.
Predicates can be arbitrarily composed using the `and` and `or` keywords.
## Examples:
iex> conform!(21, spec(is_integer()))
21
iex> conform!(21, spec(is_integer() and &(&1 >= 21)))
21
iex> conform("21", spec(is_integer() and &(&1 >= 21)))
{:error, ["val: \"21\" fails: is_integer()"]}
iex> conform!(:foo, spec(is_atom() or is_binary()))
:foo
iex> conform!("foo", spec(is_atom() or is_binary()))
"foo"
iex> conform(21, spec(is_atom() or is_binary()))
{:error, ["val: 21 fails: is_atom()", "val: 21 fails: is_binary()"]}
"""
defmacro spec(predicate) do
spec = Spec.build(predicate)
quote do
unquote(spec)
end
end
@doc ~S"""
Creates a re-usable schema.
"""
def schema(input) do
Schema.build(input)
end
@doc ~S"""
Choices between alternative predicates or patterns. The patterns must be tagged with an atom.
When conforming data to this specification the data is returned as a tuple with the tag.
## Examples
iex> conform!("foo", alt(s: spec(is_binary()), a: spec(is_atom())))
{:s, "foo"}
iex> conform!(:foo, alt(s: spec(is_binary()), a: spec(is_atom())))
{:a, :foo}
iex> conform!(123, alt(num: spec(is_integer()), str: spec(is_binary())))
{:num, 123}
iex> conform!("foo", alt(num: spec(is_integer()), str: spec(is_binary())))
{:str, "foo"}
iex> conform(true, alt(num: spec(is_integer()), str: spec(is_binary())))
{:error, ["in: :num val: true fails: is_integer()", "in: :str val: true fails: is_binary()"]}
"""
def alt(specs) when is_list(specs) do
%Alt{specs: specs}
end
@doc ~S"""
Specifies a selection of keys from a schema. This allows callsites to
define what keys must be available from the input.
"""
def selection(%Schema{}=schema) do
Selection.new(schema, :all)
end
def selection(%Schema{}=schema, path) do
Selection.new(schema, path)
end
# @doc ~S"""
# Concatenates a sequence of predicates or patterns together. These predicates
# must be tagged with an atom. The conformed data is returned as a
# keyword list.
# iex> conform!([31, "Chris"], cat(age: integer?(), name: string?()))
# [age: 31, name: "Chris"]
# iex> conform([true, "Chris"], cat(age: integer?(), name: string?()))
# {:error, ["in: [0] at: :age val: true fails: integer?()"]}
# iex> conform([31, :chris], cat(age: integer?(), name: string?()))
# {:error, ["in: [1] at: :name val: :chris fails: string?()"]}
# iex> conform([31], cat(age: integer?(), name: string?()))
# {:error, ["in: [1] at: :name val: nil fails: Insufficient input"]}
# """
# def cat(opts) do
# fn path, input ->
# results =
# opts
# |> Enum.with_index
# |> Enum.map(fn {{tag, spec}, i} ->
# val = Enum.at(input, i)
# if val do
# {tag, spec.(path ++ [{:index, i}], val)}
# else
# {tag, {:error, [error(path ++ [{:index, i}], nil, "Insufficient input")]}}
# end
# end)
# errors =
# results
# |> Enum.filter(fn {_, {result, _}} -> result == :error end)
# |> Enum.map(fn {tag, {_, errors}} -> {tag, errors} end)
# |> Enum.flat_map(fn {tag, errors} -> Enum.map(errors, &(%{&1 | at: tag})) end)
# if Enum.any?(errors) do
# {:error, errors}
# else
# {:ok, Enum.map(results, fn {tag, {_, data}} -> {tag, data} end)}
# end
# end
# end
end