Template Engine (ECR)

Azu CLI uses Crystal's ECR (Embedded Crystal) as its template engine for code generation. ECR provides a powerful, type-safe way to embed Crystal code within templates, enabling dynamic content generation while maintaining compile-time safety.

Overview

ECR (Embedded Crystal) is Crystal's built-in template engine that allows you to embed Crystal code directly within text templates. It provides:

  • Type Safety: Compile-time checking of embedded Crystal code

  • Performance: Templates are compiled to native code, not interpreted

  • Simplicity: Familiar Crystal syntax within templates

  • Flexibility: Full access to Crystal's language features

  • Integration: Seamless integration with Crystal's compilation process

ECR Basics

Template Structure

ECR templates consist of text content with embedded Crystal code:

# Basic ECR template
class <%= @name_camelcase %> < CQL::Model
  table :<%= @name_underscore.pluralize %>

  <% @attributes.each do |attr| %>
  column :<%= attr.name %>, <%= attr.column_type %>
  <% end %>

  timestamps
end

ECR Tags

ECR uses specific tags to embed Crystal code:

<% %>     # Execute Crystal code (no output)
<%= %>    # Execute Crystal code and output result
<%% %>    # Output literal <% %> (escape)

Template Rendering

Templates are rendered using the ECR.render method:

class Azu::Generators::Base
  def render_template(context : Hash(String, String)) : String
    ECR.render(template_path, context)
  end

  def template_path : String
    "src/templates/generators/model/model.cr.ecr"
  end
end

Template Variables

Context Object

Templates receive a context object with variables:

# Setting up template context
def template_context : Hash(String, String)
  {
    "name" => @name,
    "name_camelcase" => @name.camelcase,
    "name_underscore" => @name.underscore,
    "name_pluralize" => @name.pluralize,
    "name_humanize" => @name.humanize,
    "attributes" => @attributes.to_json,
    "description" => @options["description"]? || "",
    "template" => self.class.name.underscore,
    "options" => @options.to_json
  }
end

# Using context in template
class <%= @name_camelcase %> < CQL::Model
  table :<%= @name_underscore.pluralize %>

  <% if @description.presence %>
  # <%= @description %>
  <% end %>
end

Variable Types

Templates can work with various data types:

# String variables
@name : String

# Array variables
@attributes : Array(Attribute)

# Hash variables
@options : Hash(String, String)

# Boolean variables
@skip_tests : Bool

# Custom objects
@attribute : Attribute

Template Organization

Directory Structure

Templates are organized by generator type and purpose:

src/templates/
├── generators/
│   ├── model/
│   │   ├── model.cr.ecr
│   │   └── model_spec.cr.ecr
│   ├── endpoint/
│   │   ├── index_endpoint.cr.ecr
│   │   ├── show_endpoint.cr.ecr
│   │   ├── create_endpoint.cr.ecr
│   │   ├── update_endpoint.cr.ecr
│   │   ├── destroy_endpoint.cr.ecr
│   │   └── endpoint_spec.cr.ecr
│   ├── service/
│   │   ├── service.cr.ecr
│   │   └── service_spec.cr.ecr
│   ├── component/
│   │   ├── component.cr.ecr
│   │   └── component_spec.cr.ecr
│   ├── validator/
│   │   ├── validator.cr.ecr
│   │   └── validator_spec.cr.ecr
│   └── middleware/
│       ├── middleware.cr.ecr
│       └── middleware_spec.cr.ecr
├── project/
│   ├── README.md.ecr
│   ├── shard.yml.ecr
│   ├── src/
│   │   ├── main.cr.ecr
│   │   ├── server.cr.ecr
│   │   └── initializers/
│   │       ├── database.cr.ecr
│   │       └── logger.cr.ecr
│   └── spec/
│       ├── spec_helper.cr.ecr
│       └── main_spec.cr.ecr
└── scaffold/
    ├── src/
    │   ├── contracts/
    │   │   └── resource/
    │   │       ├── create_contract.cr.ecr
    │   │       ├── update_contract.cr.ecr
    │   │       └── index_contract.cr.ecr
    │   ├── endpoints/
    │   │   └── resource/
    │   │       ├── create_endpoint.cr.ecr
    │   │       ├── update_endpoint.cr.ecr
    │   │       └── index_endpoint.cr.ecr
    │   └── pages/
    │       └── resource/
    │           ├── create_page.cr.ecr
    │           ├── update_page.cr.ecr
    │           └── index_page.cr.ecr
    └── public/
        └── templates/
            └── pages/
                └── resource/
                    ├── create_page.jinja.ecr
                    ├── update_page.jinja.ecr
                    └── index_page.jinja.ecr

Template Naming Conventions

Templates follow consistent naming patterns:

{generator_type}_{action}.cr.ecr          # Generator templates
{generator_type}_spec.cr.ecr              # Test templates
{action}_{type}.cr.ecr                    # Action-specific templates
{resource}_{action}.jinja.ecr             # Jinja templates

Template Examples

Model Template

# src/templates/generators/model/model.cr.ecr
class <%= @name_camelcase %> < CQL::Model
  table :<%= @name_underscore.pluralize %>

  <% @attributes.each do |attr| %>
  column :<%= attr.name %>, <%= attr.column_type %>
  <% end %>

  timestamps

  <% @attributes.each do |attr| %>
  <% attr.validation_rules.each do |rule| %>
  validates :<%= attr.name %>, <%= rule %>
  <% end %>
  <% end %>

  <% if @description.presence %>
  # <%= @description %>
  <% end %>
end

Endpoint Template

# src/templates/generators/endpoint/index_endpoint.cr.ecr
class <%= @name_camelcase.pluralize %>::IndexEndpoint < Azu::Endpoint
  def call
    <%= @name_underscore.pluralize %> = <%= @name_camelcase %>.all

    render_page(<%= @name_camelcase.pluralize %>::IndexPage, <%= @name_underscore.pluralize %>: <%= @name_underscore.pluralize %>)
  end
end

Service Template

# src/templates/generators/service/service.cr.ecr
class <%= @name_camelcase %>Service
  def initialize
  end

  <% @attributes.each do |attr| %>
  def create_<%= @name_underscore %>(<%= attr.name %>: <%= attr.column_type %>)
    <%= @name_camelcase %>.create(
      <%= attr.name %>: <%= attr.name %>
    )
  end
  <% end %>

  def find_<%= @name_underscore %>(id: Int64)
    <%= @name_camelcase %>.find(id)
  end

  def update_<%= @name_underscore %>(id: Int64, **params)
    <%= @name_underscore %> = find_<%= @name_underscore %>(id)
    <%= @name_underscore %>.update(**params)
  end

  def delete_<%= @name_underscore %>(id: Int64)
    <%= @name_underscore %> = find_<%= @name_underscore %>(id)
    <%= @name_underscore %>.delete
  end
end

Component Template

# src/templates/generators/component/component.cr.ecr
class <%= @name_camelcase %>Component < Azu::Component
  <% @attributes.each do |attr| %>
  property <%= attr.name %> : <%= attr.column_type %>
  <% end %>

  <% @events.each do |event| %>
  event <%= event %> : String
  <% end %>

  def initialize
    <% @attributes.each do |attr| %>
    @<%= attr.name %> = ""
    <% end %>
  end

  def render
    template("src/templates/components/<%= @name_underscore %>.jinja")
  end

  <% if @websocket %>
  def on_connect
    # WebSocket connection logic
  end

  def on_message(message : String)
    # Handle incoming messages
  end
  <% end %>
end

Jinja Template

{# src/templates/components/counter.jinja.ecr #}
<div class="counter-component">
  <h3>Counter: {{ count }}</h3>
  <button onclick="increment()">Increment</button>
  <button onclick="decrement()">Decrement</button>
</div>

<script>
function increment() {
  window.azu.sendEvent('increment', {});
}

function decrement() {
  window.azu.sendEvent('decrement', {});
}
</script>

Advanced ECR Features

Conditional Logic

<% if @skip_tests %>
# Tests skipped
<% else %>
# Tests included
<% end %>

<% unless @options.empty? %>
# Options: <%= @options.to_json %>
<% end %>

Loops and Iteration

<% @attributes.each do |attr| %>
  column :<%= attr.name %>, <%= attr.column_type %>
<% end %>

<% @actions.each_with_index do |action, index| %>
  <%= action.camelcase %>Endpoint.new
<% end %>

Method Calls

class <%= @name.camelcase %> < CQL::Model
  table :<%= @name.underscore.pluralize %>

  <% @attributes.each do |attr| %>
  column :<%= attr.name %>, <%= attr.column_type %>
  <% end %>

  <% if @attributes.any?(&.has_validation?) %>
  validates :<%= @attributes.select(&.has_validation?).map(&.name).join(", ") %>
  <% end %>
end

Error Handling

<% begin %>
  <% @attributes.each do |attr| %>
  column :<%= attr.name %>, <%= attr.column_type %>
  <% end %>
<% rescue ex %>
  # Error processing attributes: <%= ex.message %>
<% end %>

Template Helpers

String Manipulation

# Built-in string methods
@name.camelcase           # "user" -> "User"
@name.underscore          # "User" -> "user"
@name.pluralize           # "user" -> "users"
@name.humanize            # "user" -> "User"
@name.titleize            # "user" -> "User"
@name.dasherize           # "user_name" -> "user-name"

Custom Helpers

class Azu::Generators::TemplateHelpers
  def self.indent(text : String, spaces : Int32 = 2) : String
    text.lines.map { |line| " " * spaces + line }.join("\n")
  end

  def self.format_attributes(attributes : Array(Attribute)) : String
    attributes.map { |attr| "#{attr.name}: #{attr.column_type}" }.join(", ")
  end

  def self.generate_validations(attributes : Array(Attribute)) : String
    attributes
      .select(&.has_validation?)
      .map { |attr| "validates :#{attr.name}, #{attr.validation_rules.join(", ")}" }
      .join("\n  ")
  end
end

Template Caching

Performance Optimization

class Azu::Generators::TemplateCache
  @@cache = {} of String => String

  def self.get(template_path : String) : String
    @@cache[template_path]? || load_template(template_path)
  end

  private def self.load_template(template_path : String) : String
    content = File.read(template_path)
    @@cache[template_path] = content
    content
  end

  def self.clear
    @@cache.clear
  end

  def self.preload_templates
    Dir.glob("src/templates/**/*.ecr").each do |path|
      get(path)
    end
  end
end

Template Validation

class Azu::Generators::TemplateValidator
  def self.validate_template(template_path : String) : Bool
    content = File.read(template_path)

    # Check for basic ECR syntax
    unless content.includes?("<%") || content.includes?("<%=")
      raise "Template #{template_path} contains no ECR tags"
    end

    # Check for balanced tags
    open_tags = content.scan(/<%[^=]/).size
    close_tags = content.scan(/%>/).size

    if open_tags != close_tags
      raise "Unbalanced ECR tags in #{template_path}"
    end

    true
  rescue ex
    Azu::Logger.error("Template validation failed: #{ex.message}")
    false
  end
end

Template Customization

User Customization

Users can override default templates by creating custom template files:

class Azu::Generators::Base
  def template_path : String
    # Check for custom template first
    custom_path = "templates/generators/#{generator_type}/#{template_name}.cr.ecr"
    return custom_path if File.exists?(custom_path)

    # Fall back to default
    "src/templates/generators/#{generator_type}/#{template_name}.cr.ecr"
  end

  private def generator_type : String
    self.class.name.underscore.split("::").last
  end

  private def template_name : String
    "model"  # Override in subclasses
  end
end

Template Inheritance

Templates can inherit from base templates:

# Base model template
# src/templates/generators/model/base_model.cr.ecr
class <%= @name_camelcase %> < CQL::Model
  table :<%= @name_underscore.pluralize %>

  <% yield %>

  timestamps
end

# Specific model template
# src/templates/generators/model/user_model.cr.ecr
<% ECR.embed "src/templates/generators/model/base_model.cr.ecr" %>
  column :name, String
  column :email, String

  validates :name, presence: true
  validates :email, presence: true, format: /^[^@]+@[^@]+\.[^@]+$/
<% end %>

Best Practices

Template Design

  1. Keep Templates Simple: Avoid complex logic in templates

  2. Use Helper Methods: Move complex logic to helper classes

  3. Consistent Indentation: Maintain readable code structure

  4. Error Handling: Include proper error handling in templates

  5. Documentation: Add comments to explain complex template logic

Performance

  1. Template Caching: Cache frequently used templates

  2. Minimize File I/O: Load templates once and reuse

  3. Efficient Loops: Use appropriate iteration methods

  4. Memory Management: Clean up template cache when needed

Maintainability

  1. Consistent Naming: Use consistent naming conventions

  2. Modular Design: Break complex templates into smaller parts

  3. Version Control: Track template changes in version control

  4. Testing: Test template rendering with various inputs

Last updated