Types in a decorator and accessing Context struct

Hello,

Probably related with macros (since I am learning them bit a bit), but let’s see if I can get some insight in what is going on.

I installed the decorator library (Elixir function decorators — decorator v1.4.0). Then, using a hello-world Phoenix project, I have a generic print decorator, but I would like to have a specific one for %Plug.Conn{}, and it does not work.

PageController:

  @decorate print_conn()
  def index_specific(%Plug.Conn{} = conn, _params) do
    IO.inspect(conn)
    render(conn, "index.html")
  end

  @decorate print()
  def index_generic(conn, _params) do
    IO.inspect(conn)
    render(conn, "index.html")
  end

The decorators from HwDecorator:

defmodule HwDecorator do
  use Decorator.Define, [print: 0, print_conn: 0]

  # at compile-time it does not know if is Plug.Conn
  # def print(body, %{args: [%Plug.Conn{} = conn, _params]} = context) do
  # so I assume we can work only with arity/2
  # alternative, to mark with print_conn
  def print(body, %{args: [conn, _params]} = context) do
    quote do
      IO.puts("Func Conn begin: #{Atom.to_string(unquote(context.name))}")
      unquote(body)
      IO.puts("Func Conn end: #{Atom.to_string(unquote(context.name))}")
    end
  end

  def print(body, context) do
    x = 3
    quote do
      #  IO.inspect(unquote(context), label: "context")  <---- WHY:2
      IO.puts("Func begin: #{unquote(x)} #{Atom.to_string(unquote(context.name))}")
      unquote(body)
      IO.puts("Func end: #{Atom.to_string(unquote(context.name))}")
    end
  end

  # This does not work:
  #  def print_conn(body, %{args: [%Plug.Conn{} = conn, _params]} = context) do. # WHY:1
  def print_conn(body, %{args: [conn, _params]} = context) do
    quote do
      IO.puts("Func Conn type begin: #{Atom.to_string(unquote(context.name))}")
      unquote(body)
      IO.puts("Func Conn type end: #{Atom.to_string(unquote(context.name))}")
    end
  end
end

The function receives:

  [{:=, [line: 8], [
      {:%, [line: 8], [
        {:__aliases__, [line: 8], [:Plug, :Conn]}, {:%{}, [line: 8], []}
      ]},
      {:conn, [line: 8], nil}
    ]},
    {:_params, [line: 8], nil}
  ]

So, I assume it is not matching since %Plug.Conn{} = conn is not the same as all those tuples from above.

I have 2 questions.

  • WHY:1: What can be done to achieve using types in the decorator functions? Like, for instance, matching Plug.Conn with the first param? I annotate the type in index_specific in case it helps, since I imagine index_generic does not know it is Plug.Conn at compile time (where I suspect the macros/decorators are being executed).
== Compilation error in file lib/hw_phoenix_web/controllers/page_controller.ex ==
** (FunctionClauseError) no function clause matching in HwDecorator.print_conn/2

    The following arguments were given to HwDecorator.print_conn/2:

        # 1
        {:__block__, [], [{{:., [line: 9], [{:__aliases__, [line: 9], [:IO]}, :inspect]}, [line: 9], [{:conn, [line: 9], nil}]}, {:render, [line: 10], [{:conn, [line: 10], nil}, "index.html"]}]}

        # 2
        %Decorator.Decorate.Context{args: [{:=, [line: 8], [{:%, [line: 8], [{:__aliases__, [line: 8], [:Plug, :Conn]}, {:%{}, [line: 8], []}]}, {:conn, [line: 8], nil}]}, {:_params, [line: 8], nil}], arity: 2, module: HwPhoenixWeb.PageController, name: :index}

    lib/hw_phoenix/decorator.ex:28: HwDecorator.print_conn/2
    (decorator 1.4.0) lib/decorator/decorate.ex:164: Decorator.Decorate.apply_decorator/3
    (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (decorator 1.4.0) lib/decorator/decorate.ex:128: Decorator.Decorate.decorate/4
    (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (decorator 1.4.0) expanding macro: Decorator.Decorate.before_compile/1
    lib/hw_phoenix_web/controllers/page_controller.ex:1: HwPhoenixWeb.PageController (module)
  • WHY:2: Why the context variable cannot be used directly there? It is just an struct. I don’t understand why context.args or context.name work, but not context.
== Compilation error in file lib/hw_phoenix_web/controllers/page_controller.ex ==
** (CompileError) lib/hw_phoenix_web/controllers/page_controller.ex: invalid quoted expression: %Decorator.Decorate.Context{args: [{:conn, [line: 14], nil}, {:_params, [line: 14], nil}], arity: 2, module: HwPhoenixWeb.PageController, name: :index_two}

Please make sure your quoted expressions are made of valid AST nodes. If you would like to introduce a value into the AST, such as a four-element tuple or a map, make sure to call Macro.escape/1 before

Any idea?

%Plug.Conn{} = conn is in fact exactly those tuples at compile-time:

iex(1)> quote do
...(1)> %Plug.Conn{} = conn
...(1)> end

{:=, [],
 [
   {:%, [], [{:__aliases__, [alias: false], [:Plug, :Conn]}, {:%{}, [], []}]},
   {:conn, [], Elixir}
 ]}

Check out the very final example from the decorator docs - inside the body of the decorator, context.args has the a list of ASTs from the function’s head so the values can only be used inside of a quote with unquote.