Skip to content

Commit

Permalink
Generators for :datetime and :date
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksei Matiushkin committed Feb 4, 2024
1 parent a5ccae9 commit d6abb0a
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 123 deletions.
196 changes: 97 additions & 99 deletions lib/estructura/hooks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -407,122 +407,120 @@ defmodule Estructura.Hooks do
@spec generator_ast(false | keyword()) :: Macro.t()
defp generator_ast(false), do: []

if match?({:module, StreamData}, Code.ensure_compiled(StreamData)) do
defp generator_ast([{_, _} | _] = types) do
fields = Keyword.keys(types)
defp generator_ast([{_, _} | _] = types) do
fields = Keyword.keys(types)

quote generated: true, location: :keep do
module = __MODULE__

defp fix_gen(many) when is_list(many), do: Enum.map(many, &fix_gen/1)

defp fix_gen(capture) when is_function(capture, 0),
do: with(info <- Function.info(capture), do: fix_gen({info[:module], info[:name]}))

defp fix_gen({key, {mod, fun, args} = value})
when is_atom(mod) and is_atom(fun) and is_list(args),
do: {key, fix_gen(value)}

defp fix_gen({key, {mod, fun}}) when is_atom(mod) and is_atom(fun),
do: fix_gen({key, {mod, fun, []}})
quote generated: true, location: :keep do
module = __MODULE__

defp fix_gen({mod, fun, args}) when is_atom(mod) and is_atom(fun) and is_list(args) do
{{:., [], [mod, fun]}, [], fix_gen(args)}
end
defp fix_gen(many) when is_list(many), do: Enum.map(many, &fix_gen/1)

defp fix_gen({mod, fun}) when is_atom(mod) and is_atom(fun), do: fix_gen({mod, fun, []})
defp fix_gen(value), do: Macro.escape(value)
defp fix_gen(capture) when is_function(capture, 0),
do: with(info <- Function.info(capture), do: fix_gen({info[:module], info[:name]}))

# @dialyzer {:nowarn_function, generation_leaf: 1}
defp generation_leaf(args),
do: {{:., [], [StreamData, :constant]}, [], [{:{}, [], args}]}
defp fix_gen({key, {mod, fun, args} = value})
when is_atom(mod) and is_atom(fun) and is_list(args),
do: {key, fix_gen(value)}

defp generation_clause({arg, gen}, acc) do
{{:., [], [StreamData, :bind]}, [], [gen, {:fn, [], [{:->, [], [[arg], acc]}]}]}
end
defp fix_gen({key, {mod, fun}}) when is_atom(mod) and is_atom(fun),
do: fix_gen({key, {mod, fun, []}})

defp generation_bound do
args =
Enum.map(unquote(types), fn {arg, gen} ->
{Macro.var(arg, nil), fix_gen(gen)}
end)
defp fix_gen({mod, fun, args}) when is_atom(mod) and is_atom(fun) and is_list(args) do
{{:., [], [mod, fun]}, [], fix_gen(args)}
end

init_args = Enum.map(args, &elem(&1, 0))
defp fix_gen({mod, fun}) when is_atom(mod) and is_atom(fun), do: fix_gen({mod, fun, []})
defp fix_gen(value), do: Macro.escape(value)

Enum.reduce(args, generation_leaf(init_args), &generation_clause/2)
end
# @dialyzer {:nowarn_function, generation_leaf: 1}
defp generation_leaf(args),
do: {{:., [], [StreamData, :constant]}, [], [{:{}, [], args}]}

defmacrop do_generation, do: generation_bound()

@doc "See `#{inspect(__MODULE__)}.__generator__/1`"
@spec __generator__() :: StreamData.t(%__MODULE__{})
def __generator__, do: __generator__(%__MODULE__{})

{usage, declarations} =
cond do
Module.has_attribute?(__MODULE__, :__estructura__) ->
{"`use Estructura`",
[
shape:
inspect(Module.get_attribute(__MODULE__, :__estructura__),
pretty: true,
width: 80
)
]}

Module.has_attribute?(__MODULE__, :__estructura_nested__) ->
estructura = Module.get_attribute(__MODULE__, :__estructura_nested__)
matcher = ~r/\n([[:space:]]*def.*end)\n]/usm

result =
[:coerce, :validate]
|> Enum.map(fn what ->
{what,
estructura
|> Map.get(what, [])
|> Enum.flat_map(fn {_who, ast} ->
matcher
|> Regex.scan(Macro.to_string(ast), capture: :all_but_first)
|> case do
[list] when is_list(list) -> list
_ -> []
end
end)
|> Enum.join("\n")}
end)
|> Keyword.put(:shape, inspect(estructura.shape, pretty: true, width: 80))

{"`use Estructura.Nested`", result}

true ->
{"N/A", []}
end
defp generation_clause({arg, gen}, acc) do
{{:., [], [StreamData, :bind]}, [], [gen, {:fn, [], [{:->, [], [[arg], acc]}]}]}
end

declarations =
declarations
|> Enum.reject(&match?({_, ""}, &1))
|> Enum.map_join("\n", fn {key, declaration} ->
"\n## #{key}\n```elixir\n#{declaration}\n```\n"
defp generation_bound do
args =
Enum.map(unquote(types), fn {arg, gen} ->
{Macro.var(arg, nil), fix_gen(gen)}
end)

@doc ~s"""
Returns the generator to be used in `StreamData`-powered property testing, based
on the specification given to #{usage}, which contained
init_args = Enum.map(args, &elem(&1, 0))

#{declarations}
Enum.reduce(args, generation_leaf(init_args), &generation_clause/2)
end

The argument given would be used as a template to generate new values.
"""
@spec __generator__(%__MODULE__{}) :: StreamData.t(%__MODULE__{})
def __generator__(%__MODULE__{} = this) do
do_generation()
|> StreamData.map(&Tuple.to_list/1)
|> StreamData.map(&Enum.zip(unquote(fields), &1))
|> StreamData.map(&struct(this, &1))
defmacrop do_generation, do: generation_bound()

{usage, declarations} =
cond do
Module.has_attribute?(__MODULE__, :__estructura__) ->
{"`use Estructura`",
[
shape:
inspect(Module.get_attribute(__MODULE__, :__estructura__),
pretty: true,
width: 80
)
]}

Module.has_attribute?(__MODULE__, :__estructura_nested__) ->
estructura = Module.get_attribute(__MODULE__, :__estructura_nested__)
matcher = ~r/\n([[:space:]]*def.*end)\n]/usm

result =
[:coerce, :validate]
|> Enum.map(fn what ->
{what,
estructura
|> Map.get(what, [])
|> Enum.flat_map(fn {_who, ast} ->
matcher
|> Regex.scan(Macro.to_string(ast), capture: :all_but_first)
|> case do
[list] when is_list(list) -> list
_ -> []
end
end)
|> Enum.join("\n")}
end)
|> Keyword.put(:shape, inspect(estructura.shape, pretty: true, width: 80))

{"`use Estructura.Nested`", result}

true ->
{"N/A", []}
end

defoverridable __generator__: 1
declarations =
declarations
|> Enum.reject(&match?({_, ""}, &1))
|> Enum.map_join("\n", fn {key, declaration} ->
"\n## #{key}\n```elixir\n#{declaration}\n```\n"
end)

@doc "See `#{inspect(__MODULE__)}.__generator__/1`"
@spec __generator__() :: StreamData.t(%__MODULE__{})
def __generator__, do: __generator__(%__MODULE__{})

@doc ~s"""
Returns the generator to be used in `StreamData`-powered property testing, based
on the specification given to #{usage}, which contained
#{declarations}
The argument given would be used as a template to generate new values.
"""
@spec __generator__(%__MODULE__{}) :: StreamData.t(%__MODULE__{})
def __generator__(%__MODULE__{} = this) do
do_generation()

Check warning on line 517 in lib/estructura/hooks.ex

View workflow job for this annotation

GitHub Actions / OTP 24.2 / Elixir 1.13

Estructura.StreamData.date/1 is undefined (module Estructura.StreamData is not available or is yet to be defined)

Check warning on line 517 in lib/estructura/hooks.ex

View workflow job for this annotation

GitHub Actions / OTP 24.2 / Elixir 1.13

Estructura.StreamData.datetime/1 is undefined (module Estructura.StreamData is not available or is yet to be defined)
|> StreamData.map(&Tuple.to_list/1)
|> StreamData.map(&Enum.zip(unquote(fields), &1))
|> StreamData.map(&struct(this, &1))
end

defoverridable __generator__: 1
end
end

Expand Down
30 changes: 21 additions & 9 deletions lib/estructura/nested.ex
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ defmodule Estructura.Nested do
{struct(), [atom()], [Exception.t()]}
defp do_from_map(acc, map, options) do
Enum.reduce(map, acc, fn
{key, %{} = map}, {into, path, errors} ->
{key, %{} = map}, {into, path, errors} when not is_struct(map) ->
{into, [_ | path], errors} =
do_from_map({into, [atomize(key) | path], errors}, map, options)

Expand Down Expand Up @@ -257,27 +257,27 @@ defmodule Estructura.Nested do
def reshape(defs, action, module) when is_list(defs),
do: Enum.map(defs, &reshape(&1, action, module))

def reshape({:def, meta, [{{:., _, _} = def, submeta, args} | rest]}, action, module) do
def reshape(
{:def, meta, [{:when, when_meta, [{def, submeta, args}, guard]} | rest]},
action,
module
) do
{acc, def} = expand_def(module, def)

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

def reshape(
{:def, meta, [{:when, when_meta, [{{:., _, _} = def, submeta, args}, guard]} | rest]},
action,
module
) do
def reshape({:def, meta, [{def, submeta, args} | rest]}, action, module) do
{acc, def} = expand_def(module, def)

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

Expand Down Expand Up @@ -340,6 +340,18 @@ defmodule Estructura.Nested do
end

@spec stream_data_type_for(simple_type_variants() | [simple_type_variants()]) :: mfargs()
defp stream_data_type_for({:datetime, opts}),
do: {Estructura.StreamData, :datetime, [opts]}

defp stream_data_type_for(:datetime),
do: stream_data_type_for({:datetime, []})

defp stream_data_type_for({:date, opts}),
do: {Estructura.StreamData, :date, [opts]}

defp stream_data_type_for(:date),
do: stream_data_type_for({:date, []})

defp stream_data_type_for({:constant, const}),
do: {StreamData, :constant, [const]}

Expand Down
10 changes: 5 additions & 5 deletions lib/estructura/transformer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,11 @@ defimpl Estructura.Transformer, for: BitString do
end
end

# defimpl Estructura.Transformer, for: [Date, Time, NaiveDateTime, DateTime] do
# def transform(input, _options) do
# @for.to_iso8601(value)
# end
# end
defimpl Estructura.Transformer, for: [Date, Time, NaiveDateTime, DateTime] do
def transform(value, _options) do
@for.to_iso8601(value)
end
end

# defimpl Estructura.Transformer, for: Decimal do
# def transform(input, _options) do
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule Estructura.MixProject do

defp deps do
[
{:stream_data, "~> 0.5", optional: true},
{:stream_data, "~> 0.5"},
{:jason, "~> 1.0", optional: true},
{:excoveralls, "~> 0.14", only: [:test, :ci], runtime: false},
{:credo, "~> 1.0", only: [:dev, :test, :ci]},
Expand Down
16 changes: 15 additions & 1 deletion test/estructura/nested_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ defmodule Estructura.Nested.Test do
assert Enum.all?(user.address.street.name, &is_binary/1)
assert is_binary(user.address.street.house)
assert is_float(user.data.age)
assert is_struct(user.created_at, DateTime)
assert is_struct(user.birthday, Date)
end
end

Expand All @@ -59,6 +61,8 @@ defmodule Estructura.Nested.Test do
city: _,
street: [*: Estructura.User.Address.Street, house: _, name: _]
],
birthday: _,
created_at: _,
data: [*: Estructura.User.Data, age: _],
name: _
] = Estructura.Transformer.transform(user)
Expand All @@ -69,6 +73,8 @@ defmodule Estructura.Nested.Test do
address: [
street: [house: _, name: _]
],
birthday: _,
created_at: _,
data: [age: _],
name: _
] = Estructura.Transformer.transform(user, except: [:city], type: false)
Expand All @@ -91,6 +97,8 @@ defmodule Estructura.Nested.Test do
city: user.address.city,
street: %{name: user.address.street.name, house: user.address.street.house}
},
birthday: user.birthday,
created_at: user.created_at,
data: %{age: user.data.age}
}

Expand All @@ -102,6 +110,8 @@ defmodule Estructura.Nested.Test do
ciudad: user.address.city,
street: %{nombre: user.address.street.name, casa: user.address.street.house}
},
birthday: user.birthday,
created_at: user.created_at,
data: %{age: user.data.age}
}

Expand All @@ -122,6 +132,8 @@ defmodule Estructura.Nested.Test do
address_city: user.address.city,
address_street_name: user.address.street.name,
address_street_house: user.address.street.house,
birthday: user.birthday,
created_at: user.created_at,
data_age: user.data.age
}

Expand All @@ -133,7 +145,9 @@ defmodule Estructura.Nested.Test do
:addresscity => user.address.city,
"address_street_nombre" => user.address.street.name,
"address_street_casa" => user.address.street.house,
:data_age => user.data.age
:data_age => user.data.age,
:created_at => user.created_at,
"birthday" => user.birthday
}

assert {:error,
Expand Down
Loading

0 comments on commit d6abb0a

Please sign in to comment.