Skip to content

Commit

Permalink
[289509] Fix: implicit preprocessors can be defined as anonymous func…
Browse files Browse the repository at this point in the history
…tions
  • Loading branch information
Double-oxygeN committed Jun 26, 2024
1 parent 080fc60 commit f08bc95
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 48 deletions.
31 changes: 21 additions & 10 deletions lib/type/base_param_struct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule Antikythera.BaseParamStruct do
@typep accept_case_t() :: :snake | :lower_camel | :upper_camel | :capital
@typep default_value_opt_t() :: Croma.Result.t(term(), :no_default_value)
@typep field_with_attr_t() ::
{atom(), [atom()], module(), preprocessor_t(), default_value_opt_t()}
{atom(), [atom()], module(), nil | preprocessor_t(), default_value_opt_t()}

@doc false
defun compute_default_value(mod :: v[module()]) :: default_value_opt_t() do
Expand Down Expand Up @@ -72,13 +72,16 @@ defmodule Antikythera.BaseParamStruct do
defun preprocess_params(
struct_mod :: v[module()],
fields_with_attrs :: list(field_with_attr_t()),
params :: params_t()
params :: params_t(),
default_pp :: (module() -> preprocessor_t())
) :: Croma.Result.t(%{required(atom()) => term()}, validate_error_t()) do
Enum.map(fields_with_attrs, fn {field_name, accepted_field_names, mod, preprocessor,
default_value_opt} ->
case get_param(params, field_name, accepted_field_names, mod, default_value_opt) do
{:ok, param} ->
preprocess_param(param, field_name, mod, preprocessor)
pp = if is_nil(preprocessor), do: default_pp.(mod), else: preprocessor

preprocess_param(param, field_name, mod, pp)
|> Croma.Result.map(&{field_name, &1})

{:default, default_value} ->
Expand Down Expand Up @@ -283,14 +286,23 @@ defmodule Antikythera.BaseParamStruct do
{:error, {:invalid_value, [struct_mod]}}
end

defmodule Preprocessor do
@moduledoc false

def default(_mod), do: {:error, :no_default_preprocessor}
end

defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
if opts[:accept_case] not in [nil, :snake, :lower_camel, :upper_camel, :capital] do
raise ":accept_case option must be one of :snake, :lower_camel, :upper_camel, or :capital"
end

default_pp =
Keyword.get(opts, :default_preprocessor, fn _mod -> {:error, :no_default_preprocessor} end)
@default_pp Keyword.get(
opts,
:default_preprocessor,
&Antikythera.BaseParamStruct.Preprocessor.default/1
)

fields_with_attrs =
Keyword.fetch!(opts, :fields)
Expand All @@ -306,12 +318,10 @@ defmodule Antikythera.BaseParamStruct do

{field_name, {mod, [default: default_value]}}
when is_atom(field_name) and is_atom(mod) ->
{field_name, mod, Croma.Result.get(default_pp.(mod), &Function.identity/1),
{:ok, default_value}}
{field_name, mod, nil, {:ok, default_value}}

{field_name, mod} when is_atom(field_name) and is_atom(mod) ->
{field_name, mod, Croma.Result.get(default_pp.(mod), &Function.identity/1),
Antikythera.BaseParamStruct.compute_default_value(mod)}
{field_name, mod, nil, Antikythera.BaseParamStruct.compute_default_value(mod)}
end)
|> Enum.map(fn {field_name, mod, preprocessor, default_value_opt} ->
{field_name,
Expand Down Expand Up @@ -347,7 +357,8 @@ defmodule Antikythera.BaseParamStruct do
Antikythera.BaseParamStruct.preprocess_params(
__MODULE__,
@base_param_struct_fields_with_attrs,
params
params,
fn mod -> @default_pp.(mod) |> Croma.Result.get(&Function.identity/1) end
)
|> Croma.Result.bind(&new/1)

Expand Down
44 changes: 6 additions & 38 deletions lib/type/param_string_struct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ defmodule Antikythera.ParamStringStruct do

@doc false
defun default(mod :: v[module()]) :: Croma.Result.t(t()) do
# The default preprocessors are defined as a capture form, which can be used in module attributes.
case Module.split(mod) do
# Preprocessors for Croma built-in types
["Croma", "Boolean"] ->
Expand Down Expand Up @@ -169,7 +168,12 @@ defmodule Antikythera.ParamStringStruct do

original_mod
|> default()
|> Croma.Result.map(&generate_nilable_preprocessor(&1, original_mod))
|> Croma.Result.map(fn pp ->
fn
nil -> nil
s -> pp.(s)
end
end)

_ ->
{:error, :no_default_preprocessor}
Expand Down Expand Up @@ -214,42 +218,6 @@ defmodule Antikythera.ParamStringStruct do
{:ok, dt, _tz_offset} = DateTime.from_iso8601(s)
dt
end

@doc false
defun generate_nilable_preprocessor(original_pp :: t(), original_mod :: v[module()]) :: t() do
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
nilable_pp_mod = Module.concat([__MODULE__, Nilable, original_mod])

nilable_pp_body =
quote do
@spec parse(nil | String.t()) :: nil | unquote(original_mod).t()
def parse(nil), do: nil
def parse(s), do: unquote(original_pp).(s)
end

:ok = ensure_module_defined(nilable_pp_mod, nilable_pp_body, Macro.Env.location(__ENV__))

&nilable_pp_mod.parse/1
end

defunp ensure_module_defined(
mod :: v[module()],
body :: term(),
location :: Macro.Env.t() | keyword()
) :: :ok do
if :code.which(mod) == :non_existing do
case Agent.start(fn -> nil end, name: mod) do
{:ok, _pid} ->
Module.create(mod, body, location)
:ok

{:error, _already_started} ->
:ok
end
else
:ok
end
end
end

defmacro __using__(opts) do
Expand Down

0 comments on commit f08bc95

Please sign in to comment.