I wrote my first Elixir macro to make writing parsers for my ex_aws_iam
lib less tedious. It’s a thin wrapper around SweetXml. Instead of having to write this:
def parse(xml, "ListUsers") do
SweetXml.xpath(xml, ~x"//ListUsersResponse",
list_users_result: [
~x"//ListUsersResult",
is_truncated: ~x"./IsTruncated/text()"s,
marker: ~x"./Marker/text()"o,
users: [
~x"./Users/member"l,
path: ~x"./Path/text()"s,
user_name: ~x"./UserName/text()"s,
arn: ~x"./Arn/text()"s,
user_id: ~x"./UserId/text()"s,
create_date: ~x"./CreateDate/text()"s
]
],
response_metadata: [~x"//ResponseMetadata", request_id: ~x"./RequestId/text()"s]
)
end
You would write this:
defparser(:list_users,
list_users_result: [
~x"//ListUsersResult",
:is_truncated,
:marker,
users: [
~x"./Users/member"l,
:path,
:user_name,
:arn,
:user_id,
:create_date
]
],
response_metadata: [
~x"//ResponseMetadata",
:request_id
]
)
Below is the macro itself:
defmodule ExAws.Iam.TestMacro do
import SweetXml, only: [sigil_x: 2]
defmacro defparser(action, fields) do
action_name = to_camel(action)
fields = Enum.map(fields, &compile/1)
quote do
def parse(xml, unquote(action_name)) do
SweetXml.xpath(xml, ~x"//#{unquote(xml_path(action_name))}", [
{unquote(xml_node(action_name)), [~x"//#{unquote(xml_path(action_name))}" | unquote(fields)]}
])
end
end
end
defp xml_path(action), do: action <> "Response"
defp xml_node(action) do
action
|> xml_path()
|> Macro.underscore()
|> String.to_atom()
end
defp compile(field) when is_atom(field) do
quote do
{unquote(field), ~x"./#{unquote(to_camel(field))}/text()"s}
end
end
defp compile({key, value}) do
quote do
{unquote(key), unquote(compile(value))}
end
end
defp compile(list) when is_list(list), do: Enum.map(list, &compile/1)
defp compile({:sigil_x, _, _} = field), do: field
defp to_camel(atom) do
atom
|> Atom.to_string()
|> Macro.camelize()
end
end
It would be nice to pass an atom (:users
) instead of a sigil (~x"./Users/member"l
) before the desired fields, but I wouldn’t be able to pass the type modifier, like the l
above to indicate that the node contains a list.
The same issue applies when passing the fields: I have no way of passing the field type.
# ...
users: [
~x"./Users/member"l,
:path,
:user_name,
:arn,
# ...
I can pass the field and its type as a tuple, but this makes things more complicated and then you’d wonder whether the macro adds any value.
Would you consider this a good use case for a macro? Would you write the macro diferently?