Current section

Files

Jump to
optimal lib optimal.ex
Raw

lib/optimal.ex

defmodule Optimal do
@moduledoc """
Documentation for Optimal.
"""
@type schema() :: Optimal.Schema.t()
@type error :: {atom, String.t()}
@type validation_result :: {:ok, Keyword.t()} | {:error, [error]}
@doc "See `Optimal.Schema.new/1`"
defdelegate schema(opts), to: Optimal.Schema, as: :new
defdelegate schema(), to: Optimal.Schema, as: :new
@doc "See `Optimal.Schema.merge/2`"
defdelegate merge(left, right), to: Optimal.Schema
defdelegate merge(left, right, opts), to: Optimal.Schema
@doc "See `Optimal.Doc.document/2`"
defdelegate document(schema, opts), to: Optimal.Doc
defdelegate document(schema), to: Optimal.Doc
@doc """
Validates opts according to a schema or the constructor for a schema. Raises on invalid opts.
iex> Optimal.validate!([reticulate_splines?: true], opts: [:reticulate_splines?])
[reticulate_splines?: true]
iex> Optimal.validate!([reticulate_splines?: true], opts: [:load_textures?], extra_keys?: true)
[reticulate_splines?: true]
iex> schema = Optimal.schema(opts: [:reticulate_splines?], required: [:reticulate_splines?], extra_keys?: true)
...> Optimal.validate!([reticulate_splines?: true, hack_interwebs?: true], schema)
[reticulate_splines?: true, hack_interwebs?: true]
iex> Optimal.validate!([], opts: [:reticulate_splines?], required: [:reticulate_splines?])
** (ArgumentError) Opt Validation Error: reticulate_splines? - is required
"""
@spec validate!(opts :: Keyword.t(), schema :: schema()) :: Keyword.t() | no_return
def validate!(opts, schema = %Optimal.Schema{}) do
case validate(opts, schema) do
{:ok, opts} ->
opts
{:error, errors} ->
message = message(errors)
raise ArgumentError, message
end
end
def validate!(opts, schema_config) do
validate!(opts, schema(schema_config))
end
@spec validate(opts :: Keyword.t(), schema :: Optimal.Schema.t()) ::
{:ok, Keyword.t()} | {:error, [error]}
def validate(opts, schema) when is_map(opts) do
validate(Enum.into(opts, []), schema)
end
def validate(opts, schema) when is_list(opts) do
with_defaults =
Enum.reduce(schema.defaults, opts, fn {default, value}, opts ->
Keyword.put_new(opts, default, value)
end)
{:ok, with_defaults}
|> validate_required(with_defaults, schema)
|> validate_types(with_defaults, schema)
|> validate_extra_keys(with_defaults, schema)
|> validate_custom(schema)
end
def validate(_opts, _schema) do
{:error, [{:opts, "opts must be a keyword list or a map."}]}
end
defp validate_custom(
validation_result = {:ok, opts},
schema = %{custom: [{field, custom} | rest]}
) do
result =
case custom.(opts[field], field, opts, schema) do
true ->
validation_result
false ->
add_errors(validation_result, {field, "failed a custom validation"})
:ok ->
validation_result
{:ok, updated_opts} ->
{:ok, updated_opts}
{:error, error_or_errors} ->
add_errors(validation_result, error_or_errors)
[] ->
validation_result
errors when is_list(errors) ->
add_errors(validation_result, errors)
end
validate_custom(result, %{schema | custom: rest})
end
defp validate_custom(validation_result, _schema), do: validation_result
defp validate_types(validation_result, opts, %{types: types}) do
Enum.reduce(types, validation_result, fn {field, type}, result ->
cond do
!Keyword.has_key?(opts, field) ->
result
Optimal.Type.matches_type?(type, opts[field]) ->
result
true ->
message = "must be of type " <> sanitize_type(type)
add_errors(result, {field, message})
end
end)
end
defp sanitize_type(%struct{}), do: sanitize_type({:struct, struct})
defp sanitize_type(term), do: inspect(term)
defp validate_required(validation_result, opts, %{required: required}) do
Enum.reduce(required, validation_result, fn key, result ->
if is_nil(opts[key]) do
add_errors(result, {key, "is required"})
else
result
end
end)
end
defp validate_extra_keys(validation_result, _opts, %{extra_keys?: true}),
do: validation_result
defp validate_extra_keys(validation_result, opts, %{opts: keys}) do
extra_keys =
opts
|> Keyword.keys()
|> Kernel.--(keys)
Enum.reduce(
extra_keys,
validation_result,
&add_errors(&2, {&1, "is not allowed (no extra keys)"})
)
end
defp add_errors({:ok, _opts}, error_or_errors) do
add_errors({:error, []}, error_or_errors)
end
defp add_errors({:error, existing_errors}, error_or_errors) do
errors = List.wrap(error_or_errors)
{:error, errors ++ existing_errors}
end
defp message(errors) do
short_messages =
errors
|> Enum.map(&short_message/1)
|> Enum.join(", ")
"Opt Validation Error: " <> "#{short_messages}"
end
defp short_message({path, message}) when is_list(path) do
path =
path
|> Enum.with_index()
|> Enum.map(fn {elem, i} ->
if i == 0 do
elem
else
"[:#{elem}]"
end
end)
|> Enum.join()
"#{path} - #{message}"
end
defp short_message({field, message}) do
"#{field} - #{message}"
end
end