# Optimal
A schema based `opt` validator. Its verbose, but I've tried many other data validation libraries, and their succinctness came with a cost when it came to features. There are a lot of optimizations and improvements that can be made, so contributions are very welcome.
View the documentation: [https://hexdocs.pm/optimal](https://hexdocs.pm/optimal)
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `optimal` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:optimal, "~> 0.2.0"}
]
end
```
## Examples
```elixir
# Allow no opts
Optimal.schema()
# Allow any opts
Optimal.schema(extra_keys?: true)
# Allow a specific set of opts
Optimal.schema(opts: [:foo, :bar, :baz])
# Allow specific types
Optimal.schema(opts: [foo: :int, bar: :string, baz: :pid])
# Require certain opts
Optimal.schema(opts, [foo: :int, bar: :string, baz: :pid], required: [:foo, :bar])
# Provide defaults for arguments (defaults will have to pass any type validation)
# If they provide they key, but a `nil` value, the default is *not* used.
Optimal.schema(opts, [foo: :int, bar: :string, baz: :boolean], defaults: [baz: true])
# Allow only specific values for certain opts
Optimal.schema(opts, [foo: :int], allow_values: [foo: [1, 2, 3]])
# Or as an enum type
Optimal.schema(opts, [foo: {:enum, [1, 2, 3]}])
# Custom validations
# Read below for more info
def custom(field_value, field_name, all_opts, schema) do
if is_special(field_value) do
:ok
else
[{field_name, "must be special"}]
end
end
Optimal.schema(opts, [foo: :integer, bar: :string], custom: [&custom/4])
```
## Types
### Scalar Types
* :any
* :atom
* :binary
* :bitstring
* :boolean
* :float
* :function
* :int
* :integer
* :keyword
* :list
* :string
* :map
* :nil
* :number
* :pid
* :port
* :reference
* :tuple
* :struct
### Composite/Complex Types
* `{:keyword, value_type}` - Keyword where all values are of type `value_type`
* `{:list, type}` - List where all values are of type `value_type`
* `{:function, arity}` - A function with the arity given by `arity`
* `{:struct, Some.Struct`} - An instance of `Some.Struct`
* `%Some.Struct{}` - Same as `{:struct, Some.Struct}`
* `{:enum, [value1, value2]}` - Allows any value in the list.
## Custom Validations
Custom validations have the ability to add arbitrary errors, and additionally they can modify the `opts` as they pass through. They are run in order, and unlike all built in validations, they are only run on valid opts.
### Examples
```elixir
# Simple (returning booleans)
def is_ten(field_value, _, _, _) do
field_value == 10
end
# Custom errors (ok/error tuples)
def is_ten(field_value, field, _, _) do
if field_value == 10 do
:ok
else
{:error, {field, "should really have equaled ten"}}
end
end
# Returning a list of errors
def greater_than_1_and_even(field_value, field, _, _) do
errors =
if field_value > 1 do
[]
else
[{field, "should be greater than 1}]
end
if Integer.is_even(field_value) do
errors
else
[{field, "should be even} | errors]
end
end
```
## Schema merging
This behavior is not set in stone, and will probably need to take a `strategy` option to support different kinds of merging opt schemas. We've noticed this is very helpful with certain patterns like building a DSL, where all forms of something take a certain set of args, but a specific form allows more args and/or additional types for that set arg.
```elixir
schema1 = Optimal.schema(opts: [foo: :int])
schema2 = Optimal.schema(opts: [foo: :string, bar: :int])
Optimal.merge(schema1, schema2) == Optimal.schema(opts: [foo: [:int, :string], bar: :int])
```