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

[Proposal] Add validation messages to Specs #83

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/norm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ defmodule Norm do
Spec.build(predicate)
end

defmacro spec(predicate, message) do
Spec.build(predicate, message)
end

@doc ~S"""
Creates a re-usable schema. Schema's are open which means that all keys are
optional and any non-specified keys are passed through without being conformed.
Expand Down
23 changes: 21 additions & 2 deletions lib/norm/conformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ defmodule Norm.Conformer do
# This module provides an api for conforming values and a protocol for
# conformable types

def conform(specs, input) when is_list(specs) do
Enum.reduce(specs, {}, fn s, acc ->
case acc do
{} ->
Norm.Conformer.Conformable.conform(s, input, [])

{:ok, _} ->
Norm.Conformer.Conformable.conform(s, input, [])

{:error, e} ->
{:error, e}
end
end)
end

def conform(spec, input) do
Norm.Conformer.Conformable.conform(spec, input, [])
end
Expand All @@ -15,8 +30,12 @@ defmodule Norm.Conformer do
|> update_in([:error], &List.flatten(&1))
end

def error(path, input, msg) do
%{path: path, input: input, spec: msg}
def error(path, input, spec_msg) do
%{path: path, input: input, spec: spec_msg}
end

def error(path, input, spec_msg, validation_msg) do
%{path: path, input: input, spec: spec_msg, message: validation_msg}
end

def error_to_msg(%{path: path, input: input, spec: msg}) do
Expand Down
23 changes: 22 additions & 1 deletion lib/norm/core/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ defmodule Norm.Core.Spec do
Or
}

defstruct predicate: nil, generator: nil, f: nil
defstruct predicate: nil, generator: nil, f: nil, message: nil

def build(quoted, message) do
built_spec = build(quoted)

quote do
%Spec{unquote(built_spec) | message: unquote(message)}
end
end

def build({:or, _, [left, right]}) do
l = build(left)
Expand Down Expand Up @@ -131,6 +139,19 @@ defmodule Norm.Core.Spec do
end

defimpl Norm.Conformer.Conformable do
def conform(%{f: f, predicate: pred, message: msg}, input, path) do
case f.(input) do
true ->
{:ok, input}

false ->
{:error, [Norm.Conformer.error(path, input, pred, msg)]}

_ ->
raise ArgumentError, "Predicates must return a boolean value"
end
end

def conform(%{f: f, predicate: pred}, input, path) do
case f.(input) do
true ->
Expand Down
34 changes: 34 additions & 0 deletions test/norm/core/spec_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ defmodule Norm.Core.SpecTest do
def match?(x, given), do: x == given
end

describe "WIP - spec/2" do
test "WIP - can include validation messages" do
is_binary_spec = spec(is_binary(), "is not binary")

{:error, [error]} = conform(1, is_binary_spec)

assert error.message == "is not binary"
end

test "WIP - can chain specs and their validation messages" do
# NOTE: This isn't really a concern of the Spec module but I want to keep things centralized for this POC
is_string_spec = [
spec(is_binary(), "is not binary"),
spec(&(String.length(&1) > 1), "must have more than 2 characters")
]

{:error, [error]} = conform("", is_string_spec)

assert error.message == "must have more than 2 characters"
end

test "WIP - chained specs fail fast to not invoke following specs" do
# NOTE: This isn't really a concern of the Spec module but I want to keep things centralized for this POC
really_bad_spec = [
spec(&(&1 && false), "I didn't like that"),
spec(&(&1 && raise ""), "I really won't like this")
]

{:error, [error]} = conform("", really_bad_spec)

assert error.message == "I didn't like that"
end
end

describe "spec/1" do
test "can compose specs with 'and'" do
hex = spec(is_binary() and (&String.starts_with?(&1, "#")))
Expand Down