Skip to content

Commit

Permalink
dataspec loaders extra
Browse files Browse the repository at this point in the history
  • Loading branch information
Giovanni Visciano committed Aug 21, 2021
1 parent 4345282 commit 463922d
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 52 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,21 +167,21 @@ DataSpecs.load(
The type of the custom loader function is

```elixir
(value(), custom_type_loaders(), [type_params_loader()] -> value())
(value(), custom_type_loaders(), [type_loader_fun()] -> value())
```

for example a custom `MapSet.t/1` loader could be implement as:

```elixir
def custom_mapset_loader(value, custom_type_loaders, [type_params_loader]) do
def custom_mapset_loader(value, custom_type_loaders, [type_loader_fun]) do
case Enumerable.impl_for(value) do
nil ->
{:error, ["can't convert #{inspect(value)} to a MapSet.t/1, value not enumerable"]}

_ ->
value
|> Enum.to_list()
|> Loaders.list(custom_type_loaders, [type_params_loader])
|> Loaders.list(custom_type_loaders, [type_loader_fun])
|> case do
{:ok, loaded_value} ->
{:ok, MapSet.new(loaded_value)}
Expand All @@ -194,7 +194,7 @@ end
```

The custom loader take the input value, check it's enumerable and then builds a `MapSet`
over the items of the input value. It takes as argument a list of `type_params_loader()` associated
over the items of the input value. It takes as argument a list of `type_loader_fun()` associated
with the type parameters.

For example, let's say we have:
Expand Down
12 changes: 3 additions & 9 deletions lib/dataspecs.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
defmodule DataSpecs do
@moduledoc File.read!("README.md")

alias DataSpecs.Typespecs
alias DataSpecs.{Types, Typespecs}

@type value() :: any()
@type reason :: [String.t() | reason()]
@type type_id :: atom()
@type type_ref :: {module(), type_id()}
@type custom_type_ref :: {module(), type_id(), arity()}
@type type_params_loader :: (value(), custom_type_loaders(), [type_params_loader] -> value())
@type custom_type_loaders :: %{custom_type_ref() => type_params_loader()}
@spec load(Types.value(), Types.type_ref(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, any()}

@doc """
Loads a value that should conform to a typespec
Expand Down Expand Up @@ -42,7 +37,6 @@ defmodule DataSpecs do
surname: "Smith"
}
"""
@spec load(value(), type_ref(), custom_type_loaders(), [type_params_loader()]) :: {:error, reason()} | {:ok, value()}
def load(value, {module, type_id}, custom_type_loaders \\ %{}, type_params_loaders \\ []) do
loader = Typespecs.loader(module, type_id, length(type_params_loaders))
loader.(value, custom_type_loaders, type_params_loaders)
Expand Down
73 changes: 71 additions & 2 deletions lib/dataspecs/loaders.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
defmodule DataSpecs.Loaders do
@moduledoc false
@moduledoc """
Type loaders for Erlang builtin types
"""

alias DataSpecs.Types

@spec any(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, any()}

def any(value, _custom_type_loaders, _type_params_loaders) do
{:ok, value}
end

@spec atom(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, atom()}

def atom(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_atom(value) ->
Expand All @@ -23,6 +33,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec boolean(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, boolean()}

def boolean(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_boolean(value) ->
Expand All @@ -33,6 +46,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec binary(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, binary()}

def binary(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_binary(value) ->
Expand All @@ -43,6 +59,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec pid(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, pid()}

def pid(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_pid(value) ->
Expand All @@ -53,6 +72,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec reference(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, reference()}

def reference(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_reference(value) ->
Expand All @@ -63,6 +85,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec number(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, number()}

def number(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_number(value) ->
Expand All @@ -73,6 +98,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec float(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, float()}

def float(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_number(value) ->
Expand All @@ -83,6 +111,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec integer(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, integer()}

def integer(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_integer(value) ->
Expand All @@ -93,6 +124,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec neg_integer(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, neg_integer()}

def neg_integer(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_integer(value) and value < 0 ->
Expand All @@ -103,6 +137,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec non_neg_integer(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, non_neg_integer()}

def non_neg_integer(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_integer(value) and value >= 0 ->
Expand All @@ -113,6 +150,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec pos_integer(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, pos_integer()}

def pos_integer(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_integer(value) and value > 0 ->
Expand All @@ -123,6 +163,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec range(integer(), integer(), Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, integer()}

def range(lower, upper, value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_integer(value) and lower <= value and value <= upper ->
Expand All @@ -133,6 +176,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec union(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, any()}

def union(value, custom_type_loaders, type_params_loaders) do
type_params_loaders
|> Enum.reduce_while({:error, []}, fn loader, {:error, errors} ->
Expand All @@ -154,6 +200,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec empty_list(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, []}

def empty_list(value, _custom_type_loaders, _type_params_loaders) do
case value do
[] ->
Expand All @@ -164,6 +213,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec nonempty_list(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, nonempty_list()}

def nonempty_list(value, custom_type_loaders, type_params_loaders) do
case value do
[_ | _] ->
Expand All @@ -174,6 +226,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec list(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, list()}

def list(value, custom_type_loaders, type_params_loaders) do
case value do
value when is_list(value) ->
Expand Down Expand Up @@ -212,6 +267,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec empty_map(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, %{}}

def empty_map(value, _custom_type_loaders, []) do
if value == %{} do
{:ok, value}
Expand All @@ -220,6 +278,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec map_field_required(map(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, {map(), map(), Types.reason()}}

def map_field_required(map, custom_type_loaders, [type_key_loader, type_value_loader]) do
map_field_optional(map, custom_type_loaders, [type_key_loader, type_value_loader])
|> case do
Expand All @@ -234,6 +295,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec map_field_optional(map(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, {map(), map(), Types.reason()}}

def map_field_optional(map, custom_type_loaders, [type_key_loader, type_value_loader]) do
case map do
map when is_struct(map) ->
Expand Down Expand Up @@ -262,7 +326,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec tuple_any(any, any, any) :: {:error, [<<_::64, _::_*8>>, ...]} | {:ok, tuple}
@spec tuple_any(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, tuple()}

def tuple_any(value, _custom_type_loaders, _type_params_loaders) do
case value do
value when is_tuple(value) ->
Expand All @@ -273,6 +339,9 @@ defmodule DataSpecs.Loaders do
end
end

@spec tuple(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, tuple()}

def tuple(value, custom_type_loaders, type_params_loaders) do
tuple_type_size = length(type_params_loaders)

Expand Down
53 changes: 53 additions & 0 deletions lib/dataspecs/loaders/extra.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule DataSpecs.Loaders.Extra do
@moduledoc """
Type loaders for Elixir types
"""

alias DataSpecs.{Loaders, Types}

@spec mapset(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, MapSet.t()}

@doc """
Type loader for Elixir MapSet.t(T).
Expect an Enumarable value of type T, returns a MapSet.t(T).
"""
def mapset(value, custom_type_loaders, [type_params_loader]) do
case Enumerable.impl_for(value) do
nil ->
{:error, ["can't convert #{inspect(value)} to a MapSet.t/1, value not enumerable"]}

_ ->
value
|> Enum.to_list()
|> Loaders.list(custom_type_loaders, [type_params_loader])
|> case do
{:ok, loaded_value} ->
{:ok, MapSet.new(loaded_value)}

{:error, errors} ->
{:error, ["can't convert #{inspect(value)} to a MapSet.t/1", errors]}
end
end
end

@spec isodatetime(Types.value(), Types.custom_type_loaders(), [Types.type_loader_fun()]) ::
{:error, Types.reason()} | {:ok, DateTime.t()}

@doc """
Type loader for Elixir DateTime.t().
Expect an iso8601 datetime string value, returns a DateTime.t().
"""
def isodatetime(value, _custom_type_loaders, []) do
with {:is_binary, true} <- {:is_binary, is_binary(value)},
{:from_iso8601, {:ok, datetime, _}} <- {:from_iso8601, DateTime.from_iso8601(value)} do
{:ok, datetime}
else
{:is_binary, false} ->
{:error, ["can't convert #{inspect(value)} to a DateTime.t/0"]}

{:from_iso8601, {:error, reason}} ->
{:error, ["can't convert #{inspect(value)} to a DateTime.t/0 (#{inspect(reason)})"]}
end
end
end
13 changes: 13 additions & 0 deletions lib/dataspecs/types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule DataSpecs.Types do
@moduledoc """
Common types
"""

@type value() :: any()
@type reason :: [String.t() | reason()]
@type type_id :: atom()
@type type_ref :: {module(), type_id()}
@type custom_type_ref :: {module(), type_id(), arity()}
@type type_loader_fun :: (value(), custom_type_loaders(), [type_loader_fun] -> value())
@type custom_type_loaders :: %{custom_type_ref() => type_loader_fun()}
end
Loading

0 comments on commit 463922d

Please sign in to comment.