The scope of ecr within crystal: or how do I pass in variables and Objects into ECR templates?

As a beginner in the Crystal language I still struggle to get my head around some of the concepts in it, and develop a feel for coding in Crystal.

When I come across difficult problems, which I solve or start to understand, I blog about them, so others can benefit – as lack of documentation is severely felt (by me) sometimes.

Onwards!

Here is the documentation for ECR (in v.0.27, the currently last version of Crystal):

https://crystal-lang.org/api/0.27.0/ECR.html

You can take a look at the source code of ECR, the embedded crystal templates, here:

https://github.com/crystal-lang/crystal/tree/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr

ECR is a compile-time template language. You can’t use ECR to process templates at run-time!

Macros to rule the world

When you use the ECR templates in Crystal ( also if you use them by way of using Kemal ), you are using macros.

Crystal has a macro language which allows you to abstract and reuse code; As a first step of compiling, the macros are evaluated and the resulting code is “pasted in” into where the macro call has been.

Then Crystal goes on to compile the code.

How does this work for ECR?

Let’s take, for instance, the def_to_s(filename) macro in the code from the example:

require "ecr"

class Greeting
  def initialize(@name : String)
  end

  ECR.def_to_s "greeting.ecr"
end

# greeting.ecr
Greeting, <%= @name %>!

Greeting.new("John").to_s #=> Greeting, John!

Crystal will call the macro def_to_s at compile time with “greeting.ecr” being passed into it.
The macro is defined here:
https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/macros.cr

macro def_to_s(filename)

  def to_s(__io__)

     ECR.embed {{filename}}, “__io__”

  end

end

your class will be rewritten like this in the first step:

require "ecr"

class Greeting
  def initialize(@name : String)
  end

  def to_s(__io__)
    ECR.embed "greeting.ecr", "__io__"
end

Do you see what just happened? a to_s method was added to your class, which itself contains a macro. Let’s look at what this macro does:

macro embed(filename, io_name)

    \{{ run(“ecr/process”, {{filename}}, {{io_name.id.stringify}}) }}

end

This is the core call of ECR. What it does, is it compiles (? / executes) a different application ecr/process and passes filename and io_name to it as parameters.

The return is the result of that application’s output.

What does the backslash mean?

“It is possible to define a macro which generates one or more macro definitions. You must escape macro expressions of the inner macro by preceding them with a backslash character \ to prevent them from being evaluated by the outer macro.”

It is essentially a nested macro!

ecr/process is defined here:

https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/process.cr

it is essentially a wrapper around ECR.process_file (remember, this is not a macro anymore – this is an application the output of which will be eventually pasted into your Crystal code!)

ecr/processor is defined here:

https://github.com/crystal-lang/crystal/blob/c9d1eef8fde5c7a03a029d64c8483ed7b4f2fe86/src/ecr/processor.cr

It processes the file, and creates a string which it gives back.

Here’s a small excerpt:

str << buffer_name

str << ” << “

string.inspect(str)

str << ‘\n’

buffer_name is what we passed to Crystal above (__io__  – identified by it’s id in io_name.id.stringify).

Output and control values are also processed. Additionally, debug output is pasted in for you (using # as a comment):

append_loc(str, filename, line_number, column_number)

Basically, the code you put into the ECR files is pasted in your code directly – at the place you have specified. It is all done by macros and running a special ECR parser.

The ECR code is not aware of your variables – if you get scope failures, undefined variable failures, etc, it is not due to you not “having passed it into ECR”, but due to not being in the right scope.

Try what you were trying to do with the ECR code directly in your main code.

Demonstration

Here is a demonstration how to “use” a class within your ECR code in Kemal. The class nests an additional ECR snippet to be rendered with the classes’ context.

the file debug.cr:

require “kemal”

require “./../macros.cr”

module Debug
   include Macros

      class DClass
           @test : String
           def initialize()
               @test = “Test String”
           end
           ECR.def_to_s “src/views/snippets/debug_snippet.ecr”
       end

  get “/debug” do |env|

    loggedin = false

    mrender “debug”
   end

end

the file debug.ecr:

<% content_for “main” do%>
<H1>Debug information</H1>

<%= loggedin %>
<H1>Incorporating decorated snippet</H1>

<%= DClass.new() %>

<% end %>

the relevant code for the macro mrender, defined in macros.cr:

macro mrender(filename)
   render “src/views/#{{{filename}}}.ecr”, “src/views/layout.ecr”
end   

It uses Kemal’s macro render which allows you to specify a layout for your view. (You don’t need this in your code necessarily, it is just given for completeness here)

the file src/views/snippets/debug_snippet.ecr:

Debug snippet
<B><%= @test %></B>

The output:

image

Putting it all together:

  1. The user calls /debug in their webbrowser
  2. Kemal ‘s macro get will match for the route “/debug”. It is defined in my debug.cr
    1. The local variable loggedin will be set to false
    2. debug.ecr will be processed by the ECR macros, and pasted in “debug.cr” (the AST representation), as if it was directly entered by you
      1. the local variable loggedin will be evaluated as false (at run-time)
      2. we call DClass.new(), and ask it for it’s string representation – which is defined by the def to_s method.
        1. we can call DClass.new(), because it is defined in the same module, as we are executing in. Again, simply think as your ECR code being pasted right there, below the class definition.
        2. we ask it for it’s string representation because we use the <%=  %> syntax

OK, let’s take a look at what happens in the DClass.new call:

      class DClass
          @test : String
           def initialize()
               @test = “Test String”
           end
           ECR.def_to_s “src/views/snippets/debug_snippet.ecr”
       end

on initialization, a string @test is set. This is an instance variable of the DClass instance we just created. (You can see that it is an instance variable because it has one “@” in front of it. Two “@@” would be a class variable)

This instance variable is used / displayed in debug_snippet.ecr

We discussed how ECR.def_to_s works before.

Effectively, after passing through the macro stage, this class would look something like this:

      class DClass
          @test : String
           def initialize()
               @test = “Test String”
           end
           def to_s(__io__)

                __io__ << “Debug snippet\n”

                __io__ << “<B>”

                __io__ << @test

                __io__ << “</B>”

           end
       end

Using this technique you can define classes to render snippets of ECR code, instead of setting up and passing every variable name manually.

I hope this article helps you, and that you will find it instead of growing frustrated – I wish I had something like this to guide me in getting started Smile

References

Kemal reference:

Refer to this page for more about macros:

NB: AST Nodes: abstract syntax tree nodes

This pointed me in the right direction, kudos!!