Skip to content

Commit

Permalink
Better coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksei Matiushkin committed Feb 5, 2024
1 parent 7aa2a79 commit 875ea2b
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 22 deletions.
9 changes: 2 additions & 7 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
locals_without_parens = [
all: :*,
check: 1,
check: 2,
property: 1,
property: 2
]
locals_without_parens = [shape: 1]

[
import_deps: [:stream_data],
inputs: [
".formatter.exs",
"mix.exs",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ end
I suggest adding [`boundary`](https://hexdocs.pm/boundary) as a dependency since that is used in this project.

## Changelog
* `1.2.3` — Several `coerce/1` and `validate/1` clauses, default coercers
* `1.2.2` — `Estructura.Flattenable`
* `1.2.1` — Generators for `:datetime` and `:date`
* `1.2.0` — `Estructura.Nested` would attempt to split keys by a delimiter if instructed
Expand Down
66 changes: 66 additions & 0 deletions lib/estructura/coercers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Estructura.Coercer do
@moduledoc """
Behaviour for coercion delegates. Instead of implementing the coercion handlers
in `Estructura.Nested` inplace, one might do
```elixir
coerce do
defdelegate foo.bar.created_at(value), to: :date
end
```
"""
@callback coerce(value) :: {:ok, value} | {:error, any()} when value: term()
end

defmodule Estructura.Coercers.Integer do
@moduledoc "Default coercer for `:integer`, coercing strings and floats by rounding"

@behaviour Estructura.Coercer
@impl Estructura.Coercer

def coerce(value) when is_integer(value), do: {:ok, value}

def coerce(value) when is_binary(value) do
case Integer.parse(value) do
{int, ""} -> {:ok, int}
{_int, remainder} -> {:error, "Trailing garbage: ‹#{remainder}›"}
:error -> {:error, "Invalid value: ‹#{inspect(value)}›"}
end
end

def coerce(value) when is_float(value), do: {:ok, round(value)}
end

defmodule Estructura.Coercers.Date do
@moduledoc "Default coercer for `:date`, coercing strings (_ISO8601_) and integers (_epoch_)"

@behaviour Estructura.Coercer
@impl Estructura.Coercer

def coerce(%Date{} = value), do: {:ok, value}

def coerce(value) when is_binary(value) do
Date.from_iso8601(value)
end
end

defmodule Estructura.Coercers.Datetime do
@moduledoc "Default coercer for `:datetime`, coercing strings (_ISO8601_) and integers (_epoch_)"

@behaviour Estructura.Coercer
@impl Estructura.Coercer

def coerce(%DateTime{} = value), do: {:ok, value}

def coerce(value) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, result, 0} -> {:ok, result}
{:ok, _result, offset} -> {:error, "Unsupported offset (#{offset})"}
error -> error
end
end

def coerce(value) when is_integer(value) do
DateTime.from_unix(value)
end
end
6 changes: 3 additions & 3 deletions lib/estructura/flattenable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ defprotocol Estructura.Flattenable do
name: nil
}
iex|%_{}|2 ▶ Estructura.Flattenable.flatten %Estructura.User{}, coupler: "-", except: ~w|address-street data-age|
iex|%_{}|2 ▶ Estructura.Flattenable.flatten(%Estructura.User{}, coupler: "-", except: ~w|address-street data-age|)
%{"address-city" => nil, "birthday" => nil, "created_at" => nil, "name" => nil}
iex|%_{}|3 ▶ Estructura.Flattenable.flatten %Estructura.User{}, only: ~w|address_street data_age|
iex|%_{}|3 ▶ Estructura.Flattenable.flatten(%Estructura.User{}, only: ~w|address_street data_age|)
%{"address_street_house" => nil, "data_age" => nil}
iex|%_{}|4 ▶ Estructura.Flattenable.flatten %Estructura.User{}, only: ~w|address|
iex|%_{}|4 ▶ Estructura.Flattenable.flatten(%Estructura.User{}, only: ~w|address|)
%{"address_city" => nil, "address_street_house" => nil}
```
Expand Down
15 changes: 14 additions & 1 deletion lib/estructura/hooks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ defmodule Estructura.Hooks do
def put!(%__MODULE__{unquote(key) => _} = data, unquote(key), value) do
case put(data, unquote(key), value) do
{:ok, updated_data} -> updated_data
{:error, reason} -> raise ArgumentError, reason
{:error, reason} when is_binary(reason) -> raise ArgumentError, reason
{:error, reason} -> raise ArgumentError, inspect(reason)
end
end

Expand Down Expand Up @@ -197,6 +198,18 @@ defmodule Estructura.Hooks do
end
end

# shape =
# with true <- Module.open?(module),
# %{} = nested <- Module.get_attribute(module, :__estructura_nested__),
# do: Map.get(nested, :shape),
# else: (_ -> %{})

# IO.inspect(
# fields: fields,
# all_fields: all_fields,
# shape: shape
# )

Module.create(coercible, [doc | callbacks], __ENV__)

behaviour_clause = quote(do: @behaviour(unquote(coercible)))
Expand Down
22 changes: 21 additions & 1 deletion lib/estructura/nested.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ defmodule Estructura.Nested do
Module.put_attribute(
__MODULE__,
:__estructura_nested__,
Map.put(nested, unquote(name), opts)
Map.update(nested, unquote(name), opts, &(&1 ++ opts))
)
end
end
Expand Down Expand Up @@ -281,6 +281,26 @@ defmodule Estructura.Nested do
]}
end

def reshape({:defdelegate, meta, [{def, submeta, args}, [to: destination]]}, action, module) do
destination =
case destination do
nickname when is_atom(nickname) ->
Module.concat(Estructura.Coercers, nickname |> Atom.to_string() |> Macro.camelize())

other ->
other
end

{acc, def} = expand_def(module, def)
wrapped_call = [[do: {{:., submeta, [destination, action]}, submeta, args}]]

{{acc, {action, def}},
[
{:@, meta, [{:impl, [], [true]}]},
{:def, meta, [{:"#{action}_#{def}", submeta, args} | wrapped_call]}
]}
end

@spec slice(module(), atom(), map(), %{required(module()) => definitions()}) ::
Macro.output() | {atom(), {:estructura, module()}}
defp slice(module, name, %{} = fields, impls) do
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Estructura.MixProject do
use Mix.Project

@app :estructura
@version "1.2.2"
@version "1.2.3"

def project do
[
Expand Down
20 changes: 11 additions & 9 deletions test/support/structs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ defmodule Estructura.User do
end
end
coerce do
def name(value) when is_binary(value), do: {:ok, value}
def name(value) when is_atom(value), do: {:ok, Atom.to_string(value)}
end
validate do
def address.street.house(house), do: {:ok, house}
end
Expand Down Expand Up @@ -154,20 +159,17 @@ defmodule Estructura.User do
end
end

def created_at(%DateTime{} = value), do: {:ok, value}

def created_at(value) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, result, 0} -> {:ok, result}
{:ok, _result, offset} -> {:error, "Unsupported offset (#{offset})"}
error -> error
end
end
defdelegate created_at(value), to: :datetime

def birthday(%Date{} = value), do: {:ok, value}
def birthday(value) when is_binary(value), do: Date.from_iso8601(value)
end

coerce do
def name(value) when is_binary(value), do: {:ok, value}
def name(value) when is_atom(value), do: {:ok, Atom.to_string(value)}
end

validate do
def address.street.house(house), do: {:ok, house}
end
Expand Down

0 comments on commit 875ea2b

Please sign in to comment.