Crystal: ECR: Render strings, not just files

Created on 9 Feb 2019  路  8Comments  路  Source: crystal-lang/crystal

I have a project where I would like to read ECR code from a JSON (aka "as a string"), and then use it with Kemal, but the current implementation only supports rendering files. Basically, I want to render an ECR string.
Now, I could write this string out to a temporary file and then render that (however silly that seems), but while reading the code I noticed that the actual process used in ecr/processor.cr is to read the file into a string and then process this string!

def process_file(filename, buffer_name = DefaultBufferName)
    process_string File.read(filename), filename, buffer_name
  end

def process_string(string, filename, buffer_name = DefaultBufferName)
...

So not only would the ability to directly render a string be very useful in my and other cases, it's actually already there, just not accessible via the API.

feature draft

Most helpful comment

I actually think it's not something bad to have. Maybe if the code is simple you can do:

require "ecr"

class Greeter
  def initialize(@names : Array(String))
  end

  ECR.def_to_s_string <<-HTML
    <html>
      <body>
        <ul>
          <% @names.each do |name| %>
            <%= name %>
          <% end %>
        </ul>
      </body>
    </html>
    HTML
end

puts Greeter.new(["John", "Jack"]).to_s_string

Doing it with ECR syntax instead of string interpolation is simpler because one can use <% ... %>.

Same goes with ECR.render_string, it could be useful. That way you could theoretically have all your templates directly inside a same file. The example above generates HTML but ECR can generate anything, and maybe ECR is simpler to read than using String.build or an external file.

That said, I'm not super convinced either that this is super necessary.

All 8 comments

The ECR files are compiled. You can鈥檛 read a string and compile it directly.

Alternatives: 1. use another template interpreted language (check web frameworks for alternatives). 2. Compile the ecr file using the crystal compiler and run the program to get the output.

I recommend 1 since 2 is not safe and will require a crystal compiler on runtime.

As a recommendation for option 1

Crinja might be what you're looking for https://github.com/straight-shoota/crinja (p.s it's created by @straight-shoota one of the core members of Crystal)

I think I made my original post more confusing than it needed to be. I understand that ECR files are compiled and I agree that if one were to do some replacement at runtime, some kind of templating engine would be a good fit. I'm thinking the JSON part was a false lead.
However: What I was actually trying to say is that I think that an equivalent of the "render" macro which takes a string directly, instead of a filename, could be very useful. And in order to show how I mean that, I threw together a bare-bones demonstration at https://github.com/Frohlix/crystal/tree/ecr-string-test
The only problem with that is that I absolutely cannot get it to build, with or without my modifications. I will continue trying, but in the meantime, I will just sum the changes up here:
(I basically added "_string" to everything new, I know some things like "to_s_string" are nonsensical)

crystal/src/ecr/process_string.cr (new)

require "ecr/processor"

string = ARGV[0]
buffer_name = ARGV[1]

begin
  puts ECR.process_string(string, "String", buffer_name)
rescue ex : Errno
  if {Errno::ENOENT, Errno::EISDIR}.includes?(ex.errno)
    STDERR.puts ex.message
    exit 1
  else
    raise ex
  end
end

crystal/src/ecr/macros.cr (added)

  macro def_to_s_string(string)
    def to_s_string(__io__)
      ECR.embed_string {{string}}, "__io__"
    end
  end

  macro embed_string(string, io_name)
    \{{ run("ecr/process_string", {{string}}, {{io_name.id.stringify}}) }}
  end

  macro render_string(string)
    ::String.build do |%io|
      ::ECR.embed_string({{string}}, %io)
    end
  end

And, to demonstrate, demonstration.cr

require "ecr"

class Greeter
    def initialize(@name : String)
    end

    ECR.def_to_s_string "Hello <%= @name %>!"
end

puts Greeter.new("John").to_s_string

othername = "Jack"

puts ECR.render_string("Hello <%= othername %>!")

I hope this helps :)

I don't see a real use case for this. Why would you want to use ECR.render_string("Hello <%= othername %>!") in the first place? A simple "Hello #{othername}" is much simpler.

There might be a point to reading templates from an external source instead of the file system, but when the template is embedded directly into the Crystal code, ECR doesn't make much sense at all.

I actually think it's not something bad to have. Maybe if the code is simple you can do:

require "ecr"

class Greeter
  def initialize(@names : Array(String))
  end

  ECR.def_to_s_string <<-HTML
    <html>
      <body>
        <ul>
          <% @names.each do |name| %>
            <%= name %>
          <% end %>
        </ul>
      </body>
    </html>
    HTML
end

puts Greeter.new(["John", "Jack"]).to_s_string

Doing it with ECR syntax instead of string interpolation is simpler because one can use <% ... %>.

Same goes with ECR.render_string, it could be useful. That way you could theoretically have all your templates directly inside a same file. The example above generates HTML but ECR can generate anything, and maybe ECR is simpler to read than using String.build or an external file.

That said, I'm not super convinced either that this is super necessary.

This would be handy for defining formats for things, main thing I can think of ATM would be log formats. This would allow you to have an ECR template string that should be used to render the object. For example:

require "ecr"

class Greeter
  property template : String = "Hello <%= @name %>!"

  def initialize(@name : String)
  end

  ECR.def_to_s @template
end

g1 = Greeter.new("John")
g1.to_s # => Hello John!

t2 = Gretter.new("Tim")
t2.template = "Wie gehts <%= @name %>!"
g2.to_s # => Wie gehts Tim!

Granted this is a simple example but you get the idea. Using ECR would make more complex formats be much easier to create/maintain than String.build.

@Blacksmoke16 That can't work, the string must be known at compile-time. It seems you are changing the string at runtime.

I'd be okay if you could define the format using a constant and a HEREDOC. Then it would be known.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Papierkorb picture Papierkorb  路  3Comments

relonger picture relonger  路  3Comments

ArthurZ picture ArthurZ  路  3Comments

Sija picture Sija  路  3Comments

asterite picture asterite  路  3Comments