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 conformer.ex
Raw

lib/norm/conformer.ex

defmodule Norm.Conformer do
@moduledoc false
# This module provides an api for conforming values and a protocol for
# conformable types
def conform(spec, input) do
Norm.Conformer.Conformable.conform(spec, input, [])
end
def group_results(results) do
results
|> Enum.reduce(%{ok: [], error: []}, fn {result, s}, acc ->
Map.put(acc, result, acc[result] ++ [s])
end)
|> update_in([:error], &List.flatten(&1))
end
def error(path, input, msg) do
%{path: path, input: input, spec: msg}
end
def error_to_msg(%{path: path, input: input, spec: msg}) do
path = if path == [], do: nil, else: "in: " <> build_path(path)
val = "val: #{format_val(input)}"
fails = "fails: #{msg}"
[val, path, fails]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
defp build_path(keys) do
keys
|> Enum.map(&format_val/1)
|> Enum.join("/")
end
defp format_val(nil), do: "nil"
defp format_val(msg) when is_binary(msg), do: "\"#{msg}\""
defp format_val(msg) when is_boolean(msg), do: "#{msg}"
defp format_val(msg) when is_atom(msg), do: ":#{msg}"
defp format_val(val) when is_map(val), do: inspect(val)
defp format_val({:index, i}), do: "[#{i}]"
defp format_val(t) when is_tuple(t), do: "#{inspect(t)}"
defp format_val(l) when is_list(l), do: "#{inspect(l)}"
defp format_val(msg), do: "#{msg}"
defprotocol Conformable do
@moduledoc false
# Defines a conformable type. Must take the type, current path, and input and
# return an success tuple with the conformed data or a list of errors.
def conform(spec, path, input)
end
end
defimpl Norm.Conformer.Conformable, for: Atom do
alias Norm.Conformer
def conform(atom, input, path) do
cond do
not is_atom(input) ->
{:error, [Conformer.error(path, input, "is not an atom.")]}
atom != input ->
{:error, [Conformer.error(path, input, "== :#{atom}")]}
true ->
{:ok, atom}
end
end
end
defimpl Norm.Conformer.Conformable, for: Tuple do
alias Norm.Conformer
alias Norm.Conformer.Conformable
def conform(spec, input, path) when is_tuple(input) and tuple_size(spec) != tuple_size(input) do
{:error, [Conformer.error(path, input, "incorrect tuple size")]}
end
def conform(spec, input, path) do
results =
spec
|> Tuple.to_list()
|> Enum.with_index()
|> Enum.map(fn {spec, i} -> Conformable.conform(spec, elem(input, i), path ++ [i]) end)
|> Conformer.group_results()
if Enum.any?(results.error) do
{:error, results.error}
else
{:ok, List.to_tuple(results.ok)}
end
end
end