crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.36k stars 1.62k forks source link

ECR: Render strings, not just files #7399

Open Frohlix opened 5 years ago

Frohlix commented 5 years ago

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.

bcardiff commented 5 years ago

The ECR files are compiled. You can’t 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.

sdogruyol commented 5 years ago

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)

Frohlix commented 5 years ago

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 :)

straight-shoota commented 5 years ago

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.

asterite commented 5 years ago

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.

Blacksmoke16 commented 5 years ago

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.

asterite commented 5 years ago

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

Blacksmoke16 commented 5 years ago

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

HCLarsen commented 2 years ago

I know I'm a bit late to this conversation, but I wanted to add that I did have an application that would have benefitted from this. All the reason that @asterite gave for preferring this over string interpolation were why I didn't go for that, and as @Blacksmoke16 pointed out, a HEREDOC constant would have worked.

I don't think this feature is a necessity, but I do think that it would be useful.