Skip to main content

Templating with ATL

ATL — the Archetect Template Language — is the engine that turns your archetype's template files into a rendered project. If you have used Jinja2, Liquid, or any {{ mustache }}-flavored template language, ATL will feel immediately familiar:

[package]
name = "{{ project_name }}"
description = "{{ description | default(project_name) }}"

This section is a practical guide to writing templates. For the complete, exhaustive syntax and filter listings, see the ATL reference.

Where templates live​

Templates are just ordinary files inside your archetype. There is no special extension requirement and no registration step — any directory in your archetype can be rendered as a template tree by calling directory.render from your archetype.lua:

directory.render("contents", context)

By convention, most archetypes keep their main template tree under contents/, with shared fragments under includes/:

my-archetype/
├── archetype.yaml
├── archetype.lua
├── contents/ # rendered by directory.render("contents", ctx)
│ └── {{ project_name }}/ # directory NAMES are templates too
│ ├── Cargo.toml # file contents are templates
│ └── src/
│ └── main.rs
└── includes/ # partials reachable via {% include %}
└── license-header.atl

Everything is a template​

When Archetect renders a directory tree, both file contents and file/directory names pass through ATL:

  • A directory literally named {{ project_name }} renders to my_tool (or however the user's answer is cased under that key).
  • Every text file's contents are rendered with the same context.
  • Binary files (images, jars, archives...) are detected automatically and copied through untouched — though their names are still rendered. See Organizing Templates for details.

The context is flat​

Templates receive a flat set of variables — you write {{ project_name }}, never context.project_name. Whatever your script puts into the Context is available by name.

Better still, case expansion means a single prompt typically lands in the context under several spellings, each key cased like its value:

KeyValue
project-namemy-tool
project_namemy_tool
projectNamemyTool
ProjectNameMyTool
Project-NameMy-Tool
PROJECT_NAMEMY_TOOL

So instead of applying case filters everywhere, you usually just pick the key whose casing matches the spot you're writing:

pub struct {{ ProjectName }}Config {
// lives in {{ project_name }}/src/config.rs
}

A quick taste​

{# Comments never appear in output #}
[workspace]
members = [
".",
{% for m in workspace_members %}
"{{ m }}",
{% endfor %}
]

{% if use_docker %}
# See docker/README.md for container workflow
{% endif %}

authors = ["{{ author_full }}"]
description = "{{ description | default(project_name) }}"

Three delimiters do all the work:

  • {{ ... }} — expressions: variables, filters, function calls
  • {% ... %} — statements: conditionals, loops, includes
  • {# ... #} — comments, stripped from output

Where to go next​