-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
236 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
defmodule MyBackend do | ||
use Drops.Contract.Messages.Backend | ||
|
||
def text(:type?, type, input) do | ||
"#{inspect(input)} received but it must be a #{type}" | ||
end | ||
|
||
def text(:filled?, _input) do | ||
"cannot be empty" | ||
end | ||
end | ||
defmodule UserContract do | ||
use Drops.Contract, message_backend: MyBackend | ||
|
||
schema do | ||
%{ | ||
required(:name) => string(:filled?), | ||
required(:email) => string(:filled?) | ||
} | ||
end | ||
end | ||
UserContract.conform(%{name: "", email: 312}) |> UserContract.errors() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
defmodule Drops.Contract.Messages do | ||
defmodule Error do | ||
alias __MODULE__ | ||
|
||
defstruct [:path, :text, :opts] | ||
|
||
defimpl String.Chars, for: Error do | ||
def to_string(%Error{path: path, text: text}) do | ||
"#{Enum.join(path, ".")} #{text}" | ||
end | ||
end | ||
end | ||
|
||
@moduledoc ~S""" | ||
Messages Backends are used to generate error messages from error results. | ||
## Examples | ||
iex> defmodule MyBackend do | ||
...> use Drops.Contract.Messages.Backend | ||
...> | ||
...> def text(:type?, type, input) do | ||
...> "#{inspect(input)} received but it must be a #{type}" | ||
...> end | ||
...> | ||
...> def text(:filled?, _input) do | ||
...> "cannot be empty" | ||
...> end | ||
...> end | ||
iex> defmodule UserContract do | ||
...> use Drops.Contract, message_backend: MyBackend | ||
...> | ||
...> schema do | ||
...> %{ | ||
...> required(:name) => string(:filled?), | ||
...> required(:email) => string(:filled?) | ||
...> } | ||
...> end | ||
...> end | ||
iex> UserContract.conform(%{name: "", email: 312}) |> UserContract.errors() | ||
[ | ||
%Drops.Contract.Messages.Error{ | ||
path: [:email], | ||
text: "312 received but it must be a string", | ||
opts: %{args: [:string, 312], predicate: :type?} | ||
}, | ||
%Drops.Contract.Messages.Error{ | ||
path: [:name], | ||
text: "cannot be empty", | ||
opts: %{args: [""], predicate: :filled?} | ||
} | ||
] | ||
""" | ||
defmodule Backend do | ||
@callback text(atom(), any()) :: String.t() | ||
@callback text(atom(), any(), any()) :: String.t() | ||
|
||
defmacro __using__(_opts) do | ||
quote do | ||
@behaviour Drops.Contract.Messages.Backend | ||
|
||
def errors(results) do | ||
Enum.map(results, &error/1) | ||
end | ||
|
||
defp error({:error, {path, predicate, [value, input] = args}}) do | ||
%Error{ | ||
path: path, | ||
text: text(predicate, value, input), | ||
opts: %{ | ||
predicate: predicate, | ||
args: args | ||
} | ||
} | ||
end | ||
|
||
defp error({:error, {path, predicate, [input] = args}}) do | ||
%Error{ | ||
path: path, | ||
text: text(predicate, input), | ||
opts: %{ | ||
predicate: predicate, | ||
args: args | ||
} | ||
} | ||
end | ||
end | ||
end | ||
end | ||
|
||
defmodule DefaultBackend do | ||
use Backend | ||
|
||
@text_mapping %{ | ||
type?: %{ | ||
integer: "must be an integer", | ||
float: "must be a float", | ||
boolean: "must be boolean", | ||
list: "must be a list", | ||
map: "must be a map", | ||
string: "must be a string", | ||
atom: "must be an atom", | ||
date: "must be a date", | ||
date_time: "must be a date time", | ||
time: "must be a time" | ||
}, | ||
filled?: "must be filled", | ||
empty?: "must be empty", | ||
eql?: "must be equal to %input%", | ||
not_eql?: "must not be equal to %input%", | ||
lt?: "must be less than %input%", | ||
gt?: "must be greater than %input%", | ||
lteq?: "must be less than or equal to %input%", | ||
gteq?: "must be greater than or equal to %input%", | ||
min_size?: "size cannot be less than %input%", | ||
max_size?: "size cannot be greater than %input%", | ||
size?: "size must be %input%", | ||
even?: "must be even", | ||
odd?: "must be odd", | ||
match?: "must match %input%", | ||
includes?: "must include %input%", | ||
excludes?: "must exclude %input%", | ||
in?: "must be one of: %input%", | ||
} | ||
|
||
@impl true | ||
def text(predicate, _input) do | ||
@text_mapping[predicate] | ||
end | ||
|
||
@impl true | ||
def text(:type?, type, _input) do | ||
@text_mapping[:type?][type] | ||
end | ||
|
||
@impl true | ||
def text(:in?, values, _input) do | ||
String.replace(@text_mapping[:in?], "%input%", Enum.join(values, ", ")) | ||
end | ||
|
||
@impl true | ||
def text(predicate, value, _input) do | ||
String.replace(@text_mapping[predicate], "%input%", to_string(value)) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
defmodule Drops.Contract.MessagesTest do | ||
use Drops.ContractCase | ||
|
||
doctest Drops.Contract.Messages | ||
|
||
describe "errors/1" do | ||
contract do | ||
schema do | ||
%{ | ||
required(:name) => string(:filled?), | ||
required(:age) => integer(gt?: 18), | ||
optional(:role) => string(in?: ["admin", "user"]) | ||
} | ||
end | ||
end | ||
|
||
test "returns errors from a type? predicate", %{contract: contract} do | ||
result = contract.conform(%{name: "Jane Doe", age: "twenty"}) | ||
|
||
assert [error = %{path: path, opts: opts}] = contract.errors(result) | ||
|
||
assert path == [:age] | ||
assert opts == %{predicate: :type?, args: [:integer, "twenty"]} | ||
assert to_string(error) == "age must be an integer" | ||
end | ||
|
||
test "returns errors from a predicate with no args", %{contract: contract} do | ||
result = contract.conform(%{name: "", age: 21}) | ||
|
||
assert [error = %{path: path, opts: opts}] = contract.errors(result) | ||
|
||
assert path == [:name] | ||
assert opts == %{predicate: :filled?, args: [""]} | ||
assert to_string(error) == "name must be filled" | ||
end | ||
|
||
test "returns errors from a predicate with args", %{contract: contract} do | ||
result = contract.conform(%{name: "Jane", age: 12}) | ||
|
||
assert [error = %{path: path, opts: opts}] = contract.errors(result) | ||
|
||
assert path == [:age] | ||
assert opts == %{predicate: :gt?, args: [18, 12]} | ||
assert to_string(error) == "age must be greater than 18" | ||
end | ||
|
||
test "returns errors from in? with a list of valid values", %{contract: contract} do | ||
result = contract.conform(%{name: "Jane", age: 19, role: "oops"}) | ||
|
||
assert [error = %{path: path, opts: opts}] = contract.errors(result) | ||
|
||
assert path == [:role] | ||
assert opts == %{predicate: :in?, args: [["admin", "user"], "oops"]} | ||
assert to_string(error) == "role must be one of: admin, user" | ||
end | ||
end | ||
end |