camel-template 0.28.0

External template component for rust-camel (ADR-0047 Stage 2)
Documentation

camel-template

External template component for rust-camel (MiniJinja, file-based, ADR-0047 Stage 2)

Overview

The Template component renders MiniJinja templates loaded from the filesystem against the body and headers of each inbound exchange. It is producer-only — you place it on the to: side of a route to transform the exchange body into rendered output.

Templates are compiled once at route startup (fail-closed) and cached for zero-filesystem-I/O hot-path rendering. The component supports hot-reload: a control-plane ReloadTemplates command re-acquires the dependency closure, recompiles, and atomically swaps the compiled set without disturbing in-flight renders.

Features

  • File-based: Templates live in standard .html.tmpl files on disk
  • Fail-closed: Compilation errors prevent the route from starting; render errors preserve the original exchange body unchanged
  • Zero-override: The template source and root directory are operator-configured at startup — no exchange header or property can override them
  • Bounded acquisition: Configurable limits on total source bytes, include count/depth, single-template size, and reload wall-clock timeout
  • Bounded render: Per-render limits on context size, output size, fuel, recursion depth, and execution timeout (inherited from the MiniJinja language engine)
  • Atomic hot-reload: Dependency-closure re-acquisition, compilation, and swap — prior set retained on failure
  • openat-based confinement: All file reads go through openat-relative handles; .., symlinks, absolute paths, and cycles are rejected

Installation

Add to your Cargo.toml:

[dependencies]
camel-template = "*"

URI Format

template:file:///<absolute-path-to-template>

The URI has two parts:

Part Description
template Outer scheme identifying this component
file:///<abs-path> Inner scheme with an absolute filesystem path to the entry template

Bare paths (template:/srv/t/page.html) and non-file: inner schemes are rejected at endpoint construction. The path must be absolute and free of .. segments.

Usage

Basic Template Render

use camel_builder::RouteBuilder;
use camel_core::CamelContext;
use camel_processor::LogLevel;
use camel_template::TemplateComponent;
use camel_template::ExternalTemplateLimitsConfig;
use camel_language_api::MinijinjaLimitsConfig;
use camel_api::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut ctx = CamelContext::builder().build().await?;

    ctx.register_component(TemplateComponent::new(
        ExternalTemplateLimitsConfig::default(),
        MinijinjaLimitsConfig::default(),
    ));

    let route = RouteBuilder::from("timer:tick?period=2000")
        .route_id("template-demo")
        .set_body("World")
        .set_header("title", Value::String("Greeting".into()))
        .to("template:file:///srv/templates/page.html.tmpl")
        .log("Rendered template", LogLevel::Info)
        .build()?;

    ctx.add_route_definition(route).await?;
    ctx.start().await?;
    tokio::signal::ctrl_c().await?;
    ctx.stop().await?;
    Ok(())
}

Template file (/srv/templates/page.html.tmpl):

{% autoescape "html" -%}
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
  <h1>{{ title }}</h1>
  <p>Hello, {{ body }}!</p>
</body>
</html>
{%- endautoescape %}

The exchange body becomes the {{ body }} variable in the template context. All exchange headers are also available as top-level variables.

Template with Exchange Context

let route = RouteBuilder::from("timer:order-event")
    .route_id("order-confirmation")
    .set_body("Order #1234 received")
    .set_header("customer_name", Value::String("Alice".into()))
    .set_header("total", Value::String("$42.00".into()))
    .to("template:file:///srv/templates/email.html.tmpl")
    .to("log:info?showBody=true")
    .build()?;

Template (email.html.tmpl):

{% autoescape "html" -%}
<p>Dear {{ customer_name }},</p>
<p>{{ body }}</p>
<p>Total: {{ total }}</p>
{%- endautoescape %}

Configuration via Camel.toml

[components.template.limits]
max-total-source-bytes = 33554432
max-include-count = 128
max-template-size = 2097152
reload-timeout-ms = 10000

[components.template.render-limits]
max-context-size = 65536
max-output-size = 1048576
fuel = 100000

Error Behavior

Scenario Error Behavior
Missing template file on startup CamelError::TemplateReload Route fails to start (fail-closed)
Template compilation error CamelError::TemplateReload Route fails to start; error details logged
Body::Stream at render time CamelError::ProcessorError Body unchanged, error propagated
Strict-undefined variable access CamelError::ProcessorError Body unchanged, error propagated
Output size exceeds limit CamelError::ProcessorError Render halted, body unchanged
Execution timeout CamelError::ProcessorError Render cancelled, body unchanged
Bounded-acquisition exceeded CamelError::TemplateReload Route fails to start (fail-closed)

On every render failure, the exchange body is not mutated — the inbound body reaches the error handler byte-identical to what was submitted.

Empty body caveat

An empty inbound body (Body::Empty) is exposed to the template as null. Under minijinja's default stringification, {{ body }} renders the literal word none — not an empty string, and not an error (strict-undefined does not trip because the body key is present). If a route may receive empty bodies, guard the reference explicitly:

{% if body %}{{ body }}{% endif %}
{# or #}
{{ body | default("") }}

See bd rc-wnqj for a planned shared-engine fix.

Documentation

License

Apache-2.0

Contributing

Contributions are welcome! Please see the main repository for details.