cjbottaro

cjbottaro

How to programmatically call macros?

I want to programmatically generate input objects from introspecting my schema, but I can’t figure out the metaprogramming to do so.

Essentially, I want this desired module def:

defmodule InputObjects do
  use Absinthe.Schema.Notation

  input_object :user_filter do
    field :id, :integer_filter
    field :email, :string_filter
  end
end

But from coming from variables:

defmodule InputObjects do
  use Absinthe.Schema.Notation

  name = :user_filter
  fields = [
    {:id, :integer_filter},
    {:email, :string_filter}
  ]
  
  input_object name do
    Enum.each fields, fn {name, type} ->
      field name, type
    end
  end
end

How to do this? Thanks for the help!

Marked As Solved

Eiji

Eiji

@cjbottaro The problem with absinthe is that they are using macros and work only with raw data (not in variables).

defmodule Example do
  defmacro sample(some_data) do
    {:here, [], [:just, :goes, :ast, :without, :quote]}
  end
end

# instead of:

defmodule Example do
  defmacro sample(some_data) do
    quote bind_quoted: [some_data: some_data], unquote: false do
      here(:just, :goes, :normal, :data, :not, :in, :ast, :format, some_data)
    end
  end
end

This is because absinthe is making some checks on raw data.

Let’s say we have such simple code:

defmodule Example do
  defmacro sample(data) do
    IO.inspect(data)
  end
end

Example.sample(5)
5 # IO.inspect call here
5

# vs

data = 5         
5
Example.sample(data)
{:data, [line: 8], nil} # IO.inspect call here
5

As you can see it’s not possible to work on it without proper quoting. Same goes if you want to use absinthe macros i.e. you need to pass raw data.

However this does not mean that it’s not possible to pass variables - this only means that we need pass raw data to absinthe macros. It should be hint for more experienced developers. Just write your own macro!

Firstly you need to know what AST you need to return:

# inside iex call
quote do
  # code of which ast you want to preview
end

# for example:
quote do
  input_object :user_filter do
    field :id, :integer_filter
    field :email, :string_filter
  end
end

{:input_object, [],
 [
   :user_filter,
   [
     do: {:__block__, [],
      [
        {:field, [], [:id, :integer_filter]},
        {:field, [], [:email, :string_filter]}
      ]}
   ]
 ]}

Let’s split it:

  1. {:field, [], [:id, :integer_filter]}
    As you can see it’s field/2 macro AST

  2. {:__block__, [], […]}
    Block here is list of AST expressions inside function which are not single literals. For example: def sample(…) do 5 end gives us just raw 5 in place of whole :__block__ part, but if we add one more line with same literal they would be arguments in :__block__ AST.

  3. Finally {:input_object, [], [:user_filter, [do: …]]}
    Similarly to 1st point it’s ast for input_object/2 call. Here do … end goes to 2nd argument which is keyword list [do: …]. As in 2nd point we could have: [do: 5] or [do: {:__block__, [], […]}]. For us it’s 2nd case as we will never contain literals there.

From this here goes example code:

defmodule Example do
  defmacro sample do
    name = :user_filter

    fields = [
      [:id, :integer_filter],
      [:email, :string_filter]
    ]

    data = Enum.map(fields, &Example.ast_call(:field, &1))
    block = Example.ast_call(:__block__, data)
    do_block_keyword = [do: block]
    Example.ast_call(:input_object, [:user_filter, do_block_keyword])
    # since you generated AST you do not need `quote do … end` here
  end

  # in same way you can simply create helper functions
  # for specific absinthe calls,
  # so you can minimize your initial data (i.e. no need to pass empty list as 2nd argument in each ast_call)
  # and your code is more readable
  def ast_call(name, args), do: {name, [], args}

  # for example:
  # def field_ast(name, type), do: {:field, [], [name, type]}
end

and here is usage:

defmodule InputObjects do
  use Absinthe.Schema.Notation
  require Example
  Example.sample()
end

Sorry if I made any typo - I wrote everything from memory. :077:

Last Post!

Eiji

Eiji

Firstly AST is not extremely low level if we are talking about Elixir. :077:

Well … I have one really good example from my experience …

My first job … I have worked with Ruby/Rails. I saw really small monkey patching usage from someone who was more experienced than me. I liked how it’s simple (so there was no need to change existing code) and I have stupidly changed file with just few lines of monkey patching into weird file with hundreds of lines with monkey patching.

If something is supported it does not mean that it should be always used. Especially when we are talking about evals. Look … I believe that Joomla! and Wordpress developers are not stupid. Same goes to developers of (at least most popular) extensions for those projects. The problem is that lots of sites was trivially hacked by adding PHP code into image files - just because they used eval.

Sure, one module looks safe, but can you guarantee that this will be still safe after few rotations in dev team? Can you guarantee that it will be still safe after somebody in team decides to move such generator (with eval call) to open source library? Software is changing really rapidly.

If we are going always with easiest solution then we will end with something like Windows in few weeks/months. Windows was created by definitely good developers, but looking how it changes in years we see that something is definitely wrong. For example compare modern (with rewritten code) Plasma 5 features and requirements with what offers Windows’s builtin window manager. I can’t do even as good job as that made in both projects, but still I (being not experienced in OS programming) see huge differences.

Also my solution is not about only “low level”. We have here typical pattern matching which prevents to add data of any type. We can always add simple guard to check if module have Schema part in list too without bigger problem.

Summary:

  1. It’s safe - no matter who will use it and no matter if you made mistake
  2. It’s at least as simple as with eval (in implementation)
  3. It’s definitely faster solution

Therefore I’m not sure what’s wrong with my hint.

Where Next?

Popular in Questions Top

ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49134 226
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 44167 214
New

We're in Beta

About us Mission Statement