Composing schemas
Thank you so much for this library!
I'm not sure if I just missed this, or if it's a bad idea, or if there's a better way to achieve it - but does Norm include an "and" for specs/schemas, corresponding to the "or" provided by one_of?
For example, say I have two schemas like:
quick_checks = schema(%{
id: spec(is_integer()),
name: spec(is_string())
})
expensive_checks = schema(%{
id: spec(some_complex_calculation()),
age: spec(another_complex_calculation())
})
Perhaps most of the time I only want to do the quick_checks, but on some occasions I want to do ALL the checks. Is there (/should there be) a way to compose the two schemas so that I end up with something like the following?
all_checks = schema(%{
id: spec(is_integer() and some_complex_calculation()),
name: spec(is_string()),
age: spec(another_complex_calculation())
})
I guess if you're using conform!/2 it doesn't really matter, because you can just do
data
|> conform!(quick_checks)
|> conform!(expensive_checks)
and you'll get an exception if something goes wrong.
But with conform/2 it's a bit trickier if you want to catch all the validation errors in one output. I wrote a helper function called conform_all, but it doesn't seem like a very elegant approach.
def conform_all(input, specs_and_schemas) do
Enum.reduce(specs_and_schemas, {:ok, input}, fn spec_or_schema, previous_result ->
case previous_result do
{:ok, input} ->
case conform(input, spec_or_schema) do
{:ok, input} -> {:ok, input}
{:error, new_errors} -> {:error, new_errors}
end
{:error, existing_errors} ->
case conform(input, spec_or_schema) do
{:ok, _input} -> {:error, existing_errors}
{:error, new_errors} -> {:error, existing_errors ++ new_errors}
end
end
end)
end
Apologies if I'm missed something obvious, and thanks so much again for Norm!
There are a couple of ways around this in Norm's current form. The most straightforward is to compose the maps before passing them to schema. Something like:
quick_checks = %{
id: spec(is_integer()),
name: spec(is_string())
}
expensive_checks = %{
id: spec(some_complex_calculation()),
age: spec(another_complex_calculation())
}
checks = schema(Map.merge(quick_checks, expensive_checks))
This obviously only works for schemas and is still pretty limited. We're working on a more general way to compose arbitrary specs. This might be something like an all_of function.
Thanks Chris, I hadn't thought of Map.merge - that's helpful.
If you do go down this road, I think all_of seems like exactly the right name for such a function - that's actually what I looked for in the docs, once I had seen one_of and coll_of.
Thanks again!
Speaking of all_of and one_of, perhaps worth deprecating one_of in favour of any_of? The rationale is twofold:
- it would match
Enum.any?/2andEnum.all?/2 - "one of" could be interpreted as "exactly one of", this is subtlety different than "any of". To be fair whenever I used
one_of, the predicates were mutually exclusive, but just mentioning this anyway.