upon

Crates.ioupon
lib.rsupon
version0.8.1
sourcesrc
created_at2022-03-26 11:48:00.412559
updated_at2024-02-23 10:34:50.68176
descriptionA simple, powerful template engine with minimal dependencies and configurable delimiters
homepage
repositoryhttps://github.com/rossmacarthur/upon
max_upload_size
id556681
size324,471
Ross MacArthur (rossmacarthur)

documentation

README

upon

Crates.io Version Docs.rs Latest Build Status

A simple, powerful template engine with minimal dependencies and configurable delimiters.

Table of Contents

Overview

Syntax

  • Expressions: {{ user.name }}
  • Conditionals: {% if user.enabled %} ... {% endif %}
  • Loops: {% for user in users %} ... {% endfor %}
  • Nested templates: {% include "nested" %}
  • Configurable delimiters: <? user.name ?>, (( if user.enabled ))
  • Arbitrary user defined filters: {{ user.name | replace: "\t", " " }}

Engine

  • Clear and well documented API
  • Customizable value formatters: {{ user.name | escape_html }}
  • Render to a String or any std::io::Write implementor
  • Render using any serde serializable values
  • Convenient macro for quick rendering: upon::value!{ name: "John", age: 42 }
  • Pretty error messages when displayed using {:#}
  • Format agnostic (does not escape values for HTML by default)
  • Minimal dependencies and decent runtime performance

Why another template engine?

It’s true there are already a lot of template engines for Rust!

I created upon because I required a template engine that had runtime compiled templates, configurable syntax delimiters and minimal dependencies. I also didn’t need support for arbitrary expressions in the template syntax but occasionally I needed something more flexible than outputting simple values (hence filters). Performance was also a concern for me, template engines like Handlebars and Tera have a lot of features but can be up to five to seven times slower to render than engines like TinyTemplate.

Basically I wanted something like TinyTemplate with support for configurable delimiters and user defined filter functions. The syntax is inspired by template engines like Liquid and Jinja.

MSRV

Currently the minimum supported version for upon is Rust 1.65. Disabling the filters feature reduces it to Rust 1.60. The MSRV will only ever be increased in a breaking release.

Getting started

First, add the crate to your Cargo manifest.

cargo add upon

Now construct an Engine. The engine stores the syntax config, filter functions, formatters, and compiled templates. Generally, you only need to construct one engine during the lifetime of a program.

let engine = upon::Engine::new();

Next, add_template(..) is used to compile and store a template in the engine.

engine.add_template("hello", "Hello {{ user.name }}!")?;

Finally, the template is rendered by fetching it using template(..), calling render(..) and rendering to a string.

let result = engine
    .template("hello")
    .render(upon::value!{ user: { name: "John Smith" }})
    .to_string()?;
assert_eq!(result, "Hello John Smith!");

Further reading

  • The syntax module documentation outlines the template syntax.
  • The filters module documentation describes filters and how they work.
  • The fmt module documentation contains information on value formatters.
  • In addition to the examples in the current document, the examples/ directory in the repository constains some more concrete code examples.

Features

The following crate features are available.

  • filters (enabled by default) — Enables support for filters in templates (see Engine::add_filter). This does not affect value formatters (see Engine::add_formatter). Disabling this will improve compile times.

  • serde (enabled by default) — Enables all serde support and pulls in the serde crate as a dependency. If disabled then you can use render_from(..) to render templates and construct the context using Value’s From impls.

  • unicode (enabled by default) — Enables unicode support and pulls in the unicode-ident and unicode-width crates. If disabled then unicode identifiers will no longer be allowed in templates and .chars().count() will be used in error formatting.

To disable all features or to use a subset you need to set default-features = false in your Cargo manifest and then enable the features that you would like. For example to use serde but disable filters and unicode you would do the following.

[dependencies]
upon = { version = "...", default-features = false, features = ["serde"] }

Examples

Nested templates

You can include other templates by name using {% include .. %}.

let mut engine = upon::Engine::new();
engine.add_template("hello", "Hello {{ user.name }}!")?;
engine.add_template("goodbye", "Goodbye {{ user.name }}!")?;
engine.add_template("nested", "{% include \"hello\" %}\n{% include \"goodbye\" %}")?;

let result = engine.template("nested")
    .render(upon::value!{ user: { name: "John Smith" }})
    .to_string()?;
assert_eq!(result, "Hello John Smith!\nGoodbye John Smith!");

Render to writer

Instead of rendering to a string it is possible to render the template to any std::io::Write implementor using to_writer(..).

use std::io;

let mut engine = upon::Engine::new();
engine.add_template("hello", "Hello {{ user.name }}!")?;

let mut stdout = io::BufWriter::new(io::stdout());
engine
    .template("hello")
    .render(upon::value!{ user: { name: "John Smith" }})
    .to_writer(&mut stdout)?;
// Prints: Hello John Smith!

Borrowed templates with short lifetimes

If the lifetime of the template source is shorter than the engine lifetime or you don’t need to store the compiled template then you can also use the compile(..) function to return the template directly.

let template = engine.compile("Hello {{ user.name }}!")?;
let result = template
    .render(&engine, upon::value!{ user: { name: "John Smith" }})
    .to_string()?;
assert_eq!(result, "Hello John Smith!");

Custom template store and function

The compile(..) function can also be used in conjunction with a custom template store which can allow for more advanced use cases. For example: relative template paths or controlling template access.

let mut store = std::collections::HashMap::<&str, upon::Template>::new();
store.insert("hello", engine.compile("Hello {{ user.name }}!")?);
store.insert("goodbye", engine.compile("Goodbye {{ user.name }}!")?);
store.insert("nested", engine.compile("{% include \"hello\" %}\n{% include \"goodbye\" %}")?);

let result = store.get("nested")
    .unwrap()
    .render(&engine, upon::value!{ user: { name: "John Smith" }})
    .with_template_fn(|name| {
        store
            .get(name)
            .ok_or_else(|| String::from("template not found"))
    })
    .to_string()?;
assert_eq!(result, "Hello John Smith!\nGoodbye John Smith!");

Benchmarks

upon was benchmarked against several popular template rendering engines in the Rust ecosystem. Obviously, each of these engines has a completely different feature set so the benchmark just compares the performance of some of the features that they share.

Violin plot of compile results Violin plot of render results Violin plot of render with filters results

Benchmarking was done using criterion on a quiet cloud machine.

Host

  • Vultr.com
  • 4 CPU
  • 8192 MB RAM
  • Ubuntu 22.04
  • Rust 1.71.0

License

Licensed under either of

at your option.

Commit count: 148

cargo fmt