Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composing schemas #37

Open
edkelly303 opened this issue Jan 23, 2020 · 3 comments
Open

Composing schemas #37

edkelly303 opened this issue Jan 23, 2020 · 3 comments

Comments

@edkelly303
Copy link

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!

@keathley
Copy link
Member

keathley commented Feb 1, 2020

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.

@edkelly303
Copy link
Author

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!

@wojtekmach
Copy link
Contributor

wojtekmach commented Feb 4, 2020

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?/2 and Enum.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants