Hi,
I’m trying to make a parsing pipeline for one of my params (Phoenix’s payload of endpoint) into a struct.
That involves parsing many dates into DateTime.t().
First, I had this:
def parse_params(%{"date1" => date1} = params) when is_binary(date1) do
date1
|> DateTime.from_iso8601()
|> case do
{:ok, parsed, _} ->
params
|> Map.put("date1", parsed)
|> parse_params()
error ->
error
end
end
# ... same repeating functions for other date fields
def parse_params(params) do
{
:ok,
%MyStruct{
title: params["title"]
number_field: String.to_integer(params["number_field"]),
date1: params["date1"]
}
}
end
The first “overload” will get repeated for each field and that is what I would like to prevent…
So I tried:
~w(date1 date2 date3)
|> Enum.map(fn key ->
quote do
def parse_params(%{unquote(key) => binary_val} = params) when is_binary(binary_val) do
binary_val
|> DateTime.from_iso8601()
|> case do
{:ok, parsed, _} ->
params
|> Map.put(unquote(key), parsed)
|> parse_params()
error ->
error
end
end
end
end)
|> Enum.reduce(
quote do
end,
fn f, acc ->
quote do
unquote(f)
unquote(acc)
end
end
)
|> Code.eval_quoted()
This would produce AST with expected functions but throws an error that says the def can be called only inside a module. Which in fact is called inside a module…
I can understand that there is an isolation of the AST but how would I “merge”/pass the outer context to the AST?
Ofcourse I could make some workaround… And I probably will… But I just wondered if the solution described above makes any sense for even considering…
To reply to the implicit question, why it doesn’t work. I think, your Module is not defined yet, when you try to call def.
Example
defmodule Test do
# a common definition
def(a(b), do: b)
|> IO.inspect(label: "#{__ENV__.line}")
# your approach (simplified)
quote do
def c(d), do: d
end
|> IO.inspect(label: "#{__ENV__.line}")
end
When you compile this, you will see the different outputs:
Yeah, that’s what I have done after rethinking it… But the reason I tried this approach was that I wanted to leverage the function param pattern matching which made it impossible with the map’s key…
And also I posted this just out of curiosity if something like this is somewhat possible…
Assuming this code is inside a defmodule, you don’t need eval_quoted - the goal of a macro is to return AST fragments that become part of the module’s code.
You likely don’t need the reduce at the end either, for similar reasons.
HOWEVER
I don’t think this is a great use of macros. The generated AST doesn’t change significantly based on the value of key. Consider using plain old functions:
defmodule UpdateCaster do
def update_cast({:error, _} = error, _key, _fun), do: error
def update_cast(data, key, fun) do
case wrap_fun(fun).(data[key]) do
{:ok, new_value} ->
Map.put(data, key, new_value)
{:error, _} = error ->
error
end
end
defp wrap_fun(fun) do
fn
s when is_binary(s) -> fun.(s)
anything_else -> anything_else
end
end
end
In use, it would look like:
defmodule SomeController do
def index(conn, params) do
parsed_params =
params
|> UpdateCaster.update_cast("date1", &cast_date/1)
|> UpdateCaster.update_cast("number_field", &cast_number/1)
# parsed_params is either a Map or {:error, something}
end
defp cast_date(input) do
case DateTime.from_iso8601(input) do
{:ok, parsed, _} -> {:ok, parsed}
{:error, _} = error -> error
end
end
defp cast_number(input) do
{:ok, String.to_integer(input)}
end
end
HOWEVER
I’d also recommend considering Ecto.Changeset and its “schemaless changesets” feature; it already does most of the casts you’d be interested in, and will be easier for future readers of your code to follow.
That is what I have not considered at all… I thought that the module would get created “first” but when I think about it now I see why as it could be written as: defmodule(everything_inside_do_block())
Or can it be written as: defmodule(&everything_inside_do_block/1), so the module could get partially created for it to pass some context info to the do block?
But the difference in AST between the 2 approaches is interesting to say the least.
Hmm interesting module… Surely will take a closer look at it… Thanks
I was afraid that the do block could not have “array” of ASTs… Okay, thanks for pointing it out…
Wow, had no idea about the “schemaless” way of Ecto’s changesets… This is really valuable…
This is the only thing (because of its decision to be code-first only) I like about Ecto… Thank you.