One cool feature of Crystal macros that I forget about is that they don't always have to be defined in a macro method. Where I've seen this be helpful is when you want to define a compile-time error in a method.
One way to think about it is, say you have a method called destroy
and people keep mentioning that they keep forgetting and accidentally trying to call delete
. Sometimes Crystal gives you a recommendation of the method it thinks you wanted but that is not always the case. To be helpful to users, you could use macros to provide a better error.
class MyLibrary
def destroy
# pure destruction
end
def delete
{% raise "Woops! Did you mean to call destroy instead?" %}
end
end
my_library = MyLibrary.new
my_library.delete #=> Helpful compilation error!
It's a nice way to provide help to users who make mistakes from time to time. We use this in Lucky's ORM, Avram, to help users avoid making mistakes that would otherwise produce hard to understand compilation errors, or, even worse, runtime errors 🙀. One example is that in Avram::Model
you define a "belongs to" association by writing belongs_to user : User
but in a migration you can add a column with a foreign key relationship to a different table by writing add_belongs_to user : User
. Out of habit, I have accidentally used belongs_to
instead of add_belongs_to
in a migration and didn't get an understandable error. It took me several minutes to track it down and I felt dumb for making the mistake. Because of that experience, we added a compilation error using macros to catch that and point users towards the solution. Here's the code in Avram that can be found on GitHub here (just a note that this is in a macro def but the same thing can be done in a regular def as well).
macro belongs_to(type_declaration, *args, **named_args)
{% raise <<-ERROR
Unexpected call to `belongs_to` in a migration.
Found in #{type_declaration.filename.id}:#{type_declaration.line_number}:#{type_declaration.column_number}.
Did you mean to use 'add_belongs_to'?
'add_belongs_to #{type_declaration}, ...'
ERROR
%}
end
Caveat
One problem we've had with this recently, though, is that we implemented this in the base method of a class that would be inherited and a module dynamically overrides it to provide the expected functionality. Because we were replacing existing code, some users were calling super in their method expecting to get the base functionality and were instead getting a compilation error. This is not a knock of the macro usage, just a note that it isn't always the perfect choice.
Top comments (0)