Struggle on define a variable in macro, use it outside where expanding it

I am struggle on migration one of our small go project to use kemal + avaram.

for avoid duplicate code, i try extract some common code (which in rails, we treat as before_action) into macro,
Maybe i do things wrong, it that is it, please point it out the correct way, anyway, let me show one example:

# define a macro here for used in several blocks
private macro get_user(env)
  open_id = env.params.url["open_id"]
  user = UserQuery.new.open_id(open_id).first    # <= i define a user variable in macro.

  if user.nil?
	env.response.status_code = 400
	next "Error"
  end
  {% debug %}
end

post path.admin_update_user do |env|
   get_user(env)                                   # <= use macro here, the user can be visited in ECR template.
   # ...
end

post path.admin_update_user do |env|
   get_user(env)                                   # <= use macro here too
   

   # i try to use `user` object here, but no luck.
   UpdateUser.update!(user, env.params)
end

As you can see, i get compile time error, like this:

Error: undefined method 'user' for top-level

If you declared 'user' in a suffix if, declare it in a regular if for this to work. 
If the variable was declared in a macro it's not visible outside it)

So, use user outside macro is not supported, right?

What is the correct way to do things like this? as you can see, what i want is just a
before_action like things, which can return early if user is nil.

Thank you.

I think the gist of the problem is the macro isn’t expanded yet when the compiler encounters the update method call.

Doing something like this seems to work:

get_user(env)
{% begin %}
  UpdateUser.update!(user, env.params)
{% end %}

Tho technically speaking using a macro in this context actually results in more code in the program. Could probably just use a method for this.

EDIT: A method might be a bit tricky with how kemal handles error handling. I.e. you need to exit early from the block, which you can’t do from outside of it…

EDIT2: You also dont really need to pass env to the macro, as you’re not using that parameter, nor would it work the way you’re thinking.

Yes, my fault, env is not necessary for this case

this seem like not work.

Macros can’t define local variables.

1 Like

It deff, does. I just tried it:

require "kemal"

class UserQuery
  def open_id(id)
    pp id
    [1, 2, 3]
  end
end

module UpdateUser
  def self.update!(u, params)
    pp u
  end
end

# define a macro here for used in several blocks
private macro get_user(env)
  open_id = env.params.url["open_id"]
  user = UserQuery.new.open_id(open_id).first    # <= i define a user variable in macro.

  if user.nil?
    env.response.status_code = 400
    next "Error"
  end
end

post "/bar/:open_id" do |env|
  get_user(env) # <= use macro here too
  {% begin %}
    # i try to use `user` object here, but no luck.
    UpdateUser.update!(user, env.params)
  {% end %}
end

Kemal.run

Oops, it my fault.

it not works for me because my code is included in a macro, like this:

module AdminUserController
  macro included
    # my code is here.
	macro get_user
      open_id = env.params.url["open_id"]
      user = UserQuery.new.open_id(open_id).first

	  if !user.nil?
		env.response.status_code = 400
		next "Error"
	  end

      user
    end
	
	get ... do
	  \{% begin %}
	    ...
	  \{% end }	
	end
  end
end

i have to escaped it to make it works.

Here’s an advice that will save you a lot of trouble: don’t use macros. You don’t need them. I’m sure of it. Please rewrite your code in a different way. For example get_user could return User or nil, and respond with an error in case of nil. Then you use it like this:

get_user(env) || next "Error"

I recommend not using macros at all in the beginning. Macros are mostly used for library code, not for application code.

5 Likes

Yes! i consider more deeper understood the macro … it not elixir