Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
47 changes: 30 additions & 17 deletions lib/parameter/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,24 @@ defmodule Parameter.Field do
virtual: false
]

@type validator_type ::
(any(), any() -> :ok | {:error, any()})
| (any() -> :ok | {:error, any()})
| {(any(), list() -> :ok | {:error, any()}), list()}
| {(any(), any(), list() -> :ok | {:error, any()}), list()}
| nil

@type t :: %__MODULE__{
name: atom(),
key: binary(),
default: any(),
load_default: any(),
dump_default: any(),
on_load: fun() | nil,
on_dump: fun() | nil,
on_load: (any(), any() -> {:ok, any()} | {:error, any()}) | nil,
on_dump: (any(), any() -> {:ok, any()} | {:error, any()}) | nil,
type: Types.t(),
required: boolean(),
validator: fun() | nil,
validator: validator_type(),
virtual: boolean()
}

Expand Down Expand Up @@ -147,24 +154,30 @@ defmodule Parameter.Field do
{:error, "`default` opts should not be used with `load_default` or `dump_default`"}
end

defp on_load_valid?(on_load) do
function_valid?(on_load, 2, "on_load must be a function")
end
defp on_load_valid?(on_load) when is_function(on_load, 2) or is_nil(on_load),
do: :ok

defp on_dump_valid?(on_dump) do
function_valid?(on_dump, 2, "on_dump must be a function")
defp on_load_valid?(_on_load) do
{:error, "on_load must be a function with 2 arity"}
end

defp validator_valid?(validator) do
function_valid?(validator, 1, "validator must be a function")
end
defp on_dump_valid?(on_dump) when is_function(on_dump, 2) or is_nil(on_dump),
do: :ok

defp function_valid?(function, arity, _message)
when is_function(function, arity) or is_nil(function) or is_tuple(function) do
:ok
defp on_dump_valid?(_on_dump) do
{:error, "on_dump must be a function with 2 arity"}
end

defp function_valid?(_validator, arity, message) do
{:error, "#{message} with #{arity} arity"}
end
defp validator_valid?({validator, args})
when is_list(args) and (is_function(validator, 2) or is_function(validator, 3)),
do: :ok

defp validator_valid?(validator)
when is_function(validator, 1) or is_function(validator, 2) or is_nil(validator),
do: :ok

defp validator_valid?(_validator),
do:
{:error,
"validator must be a function with arity 1 or 2, or a tuple with a function and arguments"}
end
24 changes: 18 additions & 6 deletions lib/parameter/schema_fields.ex
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ defmodule Parameter.SchemaFields do
case operation_handler(meta, field, value, opts) do
{:ok, value} ->
validator
|> run_validator(value)
|> run_validator(value, meta.parent_input)
|> parse_validator_result(operation)

:ok ->
validator
|> run_validator(value)
|> run_validator(value, meta.parent_input)
|> parse_validator_result(operation)

error ->
Expand Down Expand Up @@ -178,15 +178,27 @@ defmodule Parameter.SchemaFields do
end
end

defp run_validator({func, args}, value) do
case apply(func, [value | [args]]) do
defp run_validator({func, args}, value, _parent_input) when is_function(func, 2),
do: do_run_validator(func, [value | [args]])

defp run_validator({func, args}, value, parent_input) when is_function(func, 3),
do: do_run_validator(func, [value, parent_input | [args]])

defp run_validator(func, value, _parent_input) when is_function(func, 1),
do: do_run_validator(func, [value])

defp run_validator(func, value, parent_input) when is_function(func, 2),
do: do_run_validator(func, [value, parent_input])

defp run_validator(func, value, parent_input) do
case func.(value, parent_input) do
:ok -> {:ok, value}
error -> error
end
end

defp run_validator(func, value) do
case func.(value) do
defp do_run_validator(validator, [value | _] = args) do
case apply(validator, args) do
:ok -> {:ok, value}
error -> error
end
Expand Down
26 changes: 26 additions & 0 deletions test/parameter/field_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ defmodule Parameter.FieldTest do
assert {:error, "on_load must be a function with 2 arity"} == Field.new(opts)
end

test "fails if on_load or on_dump are tuples" do
opts = [
name: :address,
type: :float,
key: "mainAddress",
required: true
]

on_load_opts =
Keyword.put(
opts,
:on_load,
{fn val -> {:ok, val} end, argument1: "arg1", argument2: "arg2"}
)

on_dump_opts =
Keyword.put(
opts,
:on_dump,
{fn val -> {:ok, val} end, argument1: "arg1", argument2: "arg2"}
)

assert {:error, "on_load must be a function with 2 arity"} == Field.new(on_load_opts)
assert {:error, "on_dump must be a function with 2 arity"} == Field.new(on_dump_opts)
end

test "fails if a default value used at the same time with load_default and dump_default" do
opts = [
name: :main_address,
Expand Down
105 changes: 105 additions & 0 deletions test/parameter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,68 @@ defmodule ParameterTest do
end
end

defmodule CrossFieldValidatorSchema do
use Parameter.Schema
alias Parameter.Validators

enum Breed do
value :polar, key: "polar"
value :grizzly, key: "grizzly"
value :american_black, key: "americanBlack"
value :asian_black, key: "asianBlack"
value :stuffed, key: "stuffed"
end

enum Color do
value :black, key: "black"
value :white, key: "white"
value :brown, key: "brown"
value :yellow, key: "yellow"
end

param do
field :bear_name, :string, validator: &__MODULE__.starts_with_capital_letter/1
field :age, :integer, validator: {&__MODULE__.pooh_must_be_old/3, min: 100}
field :color, __MODULE__.Color, validator: &__MODULE__.color_validator/2

field :breed, __MODULE__.Breed, required: true
end

def starts_with_capital_letter(value) do
if Regex.match?(~r/^[A-Z]/, value) do
:ok
else
{:error, "must start with a capital letter"}
end
end

def pooh_must_be_old(value, %{"bear_name" => "Pooh"}, min: min) do
if value >= min do
:ok
else
{:error, "pooh must be at least #{min} years old"}
end
end

def pooh_must_be_old(value, _input, _opts) do
{:ok, value}
end

def color_validator(:yellow, %{"breed" => :stuffed}), do: :ok
def color_validator(:yellow, _), do: {:error, "yellow bears can only be stuffed"}
def color_validator(:brown, %{"breed" => :grizzly}), do: :ok
def color_validator(:brown, _), do: {:error, "brown bears can only be grizzly"}
def color_validator(:white, %{"breed" => :polar}), do: :ok
def color_validator(:white, _), do: {:error, "white bears can only be polar"}

def color_validator(:black, %{"breed" => breed})
when breed in [:asian_black, :american_black],
do: :ok

def color_validator(:black, _),
do: {:error, "black bears can only be asian_black or american_black"}
end

defmodule VirtualFieldTestSchema do
use Parameter.Schema
alias Parameter.Validators
Expand Down Expand Up @@ -2164,5 +2226,48 @@ defmodule ParameterTest do
}
})
end

test "cross field validation should succeed for correct values" do
winnie_the_pooh = %{
"bear_name" => "Pooh",
"age" => 110,
"breed" => :stuffed,
"color" => :yellow
}

assert :ok == Parameter.validate(CrossFieldValidatorSchema, winnie_the_pooh)
end

test "cross field validation should fail for incorrect values" do
winnie_the_pooh = %{
"bear_name" => "Pooh",
"age" => 99,
"breed" => :stuffed,
"color" => :yellow
}

assert {:error, %{age: "pooh must be at least 100 years old"}} ==
Parameter.validate(CrossFieldValidatorSchema, winnie_the_pooh)

grizzly_bear = %{
"bear_name" => "Grizzly",
"age" => 5,
"breed" => :grizzly,
"color" => :black
}

assert {:error, %{color: "black bears can only be asian_black or american_black"}} ==
Parameter.validate(CrossFieldValidatorSchema, grizzly_bear)

wrong_name = %{
"bear_name" => "wrong",
"age" => 5,
"breed" => :grizzly,
"color" => :brown
}

assert {:error, %{bear_name: "must start with a capital letter"}} ==
Parameter.validate(CrossFieldValidatorSchema, wrong_name)
end
end
end