Skip to content
Merged
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
14 changes: 14 additions & 0 deletions lib/drops/predicates/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ defmodule Drops.Predicates.Helpers do
end
end

def apply_predicate({:cast, input_type, output_type, cast_opts}, {:ok, value}) do
caster = cast_opts[:caster] || Drops.Casters

try do
casted_value =
apply(caster, :cast, [input_type, output_type, value] ++ cast_opts)

{:ok, casted_value}

rescue
exception -> {:error, [input: value, predicate: :cast, args: [Exception.message(exception)]]}
end
end

def apply_predicate(_, {:error, _} = error) do
error
end
Expand Down
48 changes: 48 additions & 0 deletions lib/drops/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,43 @@ defmodule Drops.Type do
]}
iex> Enum.map(errors, &to_string/1)
["unit_price must be greater than 0"]

## Casting

defmodule IntegerString do
use Drops.Type, cast(:string) |> integer()
end

iex> defmodule IntegerStringContract do
...> use Drops.Contract
...>
...> schema do
...> %{
...> number: IntegerString
...> }
...> end
...> end
iex> IntegerStringContract.conform(%{number: "1"})
{:ok, %{number: 1}}

defmodule IntegerOrIntegerString do
use Drops.Type, union([:integer, cast(:string) |> integer()])
end

iex> defmodule IntegerOrIntegerStringContract do
...> use Drops.Contract
...>
...> schema do
...> %{
...> number: IntegerOrIntegerString,
...> }
...> end
...> end
iex> IntegerOrIntegerStringContract.conform(%{number: "1"})
{:ok, %{number: 1}}
iex> IntegerOrIntegerStringContract.conform(%{number: 1})
{:ok, %{number: 1}}

"""
@doc since: "0.2.0"

Expand Down Expand Up @@ -266,6 +303,7 @@ defmodule Drops.Type do
def infer_primitive(map) when is_map(map), do: :map
def infer_primitive(name) when is_atom(name), do: name
def infer_primitive({:type, {name, _}}), do: name
def infer_primitive({:cast, {_input_type, output_type, _cast_opts}}), do: infer_primitive(output_type)
def infer_primitive(_), do: nil

@doc false
Expand All @@ -285,6 +323,16 @@ defmodule Drops.Type do
[predicate(:type?, [type])]
end

def infer_constraints({:cast, {{:type, {input_type, input_predicates}}, {:type, {output_type, output_predicates}}, cast_opts}}) do
{:and,
Enum.concat([
infer_constraints({:type, {input_type, input_predicates}}),
[{:cast, input_type, output_type, cast_opts}],
infer_constraints({:type, {output_type, output_predicates}})
])
}
end

@doc false
def predicate({:match?, %Regex{} = regex}) do
{:predicate, {:match?, {:regex, regex.source, regex.opts}}}
Expand Down
8 changes: 8 additions & 0 deletions lib/drops/type/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ defmodule Drops.Type.DSL do
{:cast, {type(input_type), output_type, cast_opts}}
end

def type({:cast, {input_type, output_type, cast_opts}}, more_predicates) when is_tuple(input_type) do
{:cast, {input_type, type(output_type, more_predicates), cast_opts}}
end

@doc ~S"""
Returns a union type specification.

Expand Down Expand Up @@ -465,4 +469,8 @@ defmodule Drops.Type.DSL do
def map(predicates) when is_list(predicates) do
type(:map, predicates)
end

def map({:cast, _} = cast_spec, predicates \\ []) do
type(cast_spec, map(predicates))
end
end
8 changes: 8 additions & 0 deletions test/drops/type_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ defmodule Drops.TypeTest do
use Drops.Type, union([:integer, :float], gt?: 0)
end

defmodule IntegerString do
use Drops.Type, cast(:string) |> integer()
end

defmodule IntegerOrIntegerString do
use Drops.Type, union([:integer, cast(:string) |> integer()])
end

doctest Drops.Type

describe "type registry" do
Expand Down
Loading