0
votes

If we check the example for using Ecto fragments in the official docs, it says that in order to use a macro from another module, we need to import that module first. So, if we have the module with a coalesce/2 macro:

defmodule CustomFunctions do
  defmacro coalesce(left, right) do
    quote do
      fragment("coalesce(?, ?)", unquote(left), unquote(right))
    end
  end
end

We need to import it in another module to use it:

import CustomFunctions

And after that, we could write the queries like this:

where(Post, [p], p.id in coalesce(3,5)

It works great as long as there are no name conflicts. But let's say, that for some reason, I am creating a few modules that will export only one macro called query and obviously that won't take long until name conflicts start to appear. So I am wondering if it's possible to use the macro from above using its full name, like this:

require CustomFunctions
where(Post, [p], p.id in CustomFunctions.coalesce(3,5)

Obviously I tried it but it fails, saying that CustomFunctions.coalesce(3,5) is not a proper query element.

So, is there a simple way to achieve something like that?

1

1 Answers

2
votes

I am not aware of this functionality coming out of the box, but you might do something like:

defmodule(Macro1, do: defmacro(coalesce(foo), do: quote(do: unquote(foo) + 1)))
defmodule(Macro2, do: defmacro(coalesce(foo), do: quote(do: unquote(foo) - 1)))

Those are modules with macros clashing names. We are to import them as macro1_coalesce and macro2_coalesce.

defmodule EctoExtention do
  defmacro __using__(opts) do
    Enum.flat_map(opts, fn what ->
      {mod, funs} =
        case what do
          {mod, :*} -> {mod, Module.concat([mod]).__info__(:macros)}
          {mod, funs} -> {mod, funs}
        end

      prefix =
        mod
        |> to_string()
        |> String.downcase()

      mod = Module.concat([mod]) # or :"Elixir.#{mod}"

      [
        quote(do: require(unquote(mod)))
        | Enum.map(funs, fn {fun, arity} ->
            args = for i <- 0..arity, i > 0, do: Macro.var(:"arg_#{i}", nil)

            quote do
              @doc ~s"""
              Macro #{unquote(fun)} imported from module #{unquote(mod)}
              """
              defmacro unquote(:"#{prefix}_#{fun}")(unquote_splicing(args)),
                do: unquote(mod).unquote(fun)(unquote_splicing(args))
            end
          end)
      ]
    end)
  end
end

Now let’s define the rules to import macros from different modules.

defmodule AllMyMacros do
  use EctoExtention, Macro1: [coalesce: 1], Macro2: :*
end

:* means import all the macros. Let’s test it:

defmodule Test do
  import AllMyMacros
  def info, do: Using.__info__(:macros)
  def test, do: {macro1_coalesce(42), macro2_coalesce(42)}
end


IO.inspect(Test.info(), label: "macros")
#⇒ macros: [macro1_coalesce: 1, macro2_coalesce: 1]

IO.inspect(Test.test(), label: "test")
#⇒ test: {43, 41}