Skip to content
Β 
Β 

Latest commit

Β 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Temple logo

Temple

A small, fast Rust DSL for shaping data β€” input in, strongly-typed Rust value out.

CI Rust 2021 Version Tests Coverage Decimals License

Install Β· Quick start Β· The language Β· Diagnostics Β· How it works Β· Reliability Β· Design


You give Temple a template and an input value; it gives you back a value deserialized straight into your Rust types. A template fixes the shape of the output, with {{ … }} holes where values are computed:

TemplateInput β†’ Output
let items = input.items
{
  "summary":  "{{ input.customer }} ordered {{ items.len() }} item(s)",
  "lines":    {{ items.map(it -> { "name": it.name, "total": it.qty * it.price }) }},
  "subtotal": {{ items.map(it -> it.qty * it.price).sum() }},
  "discount"?: {{ this.subtotal >= 100 ? round(this.subtotal * 0.1, 2) : null }},
  "receipt":  "Total due: {{ this.subtotal }}"
}
// input
{ "customer": "Ada",
  "items": [ { "name": "widget", "qty": 3, "price": 9.99 } ] }

// output β€” deserialized into your Receipt struct
{ "summary":  "Ada ordered 1 item(s)",
  "lines":    [ { "name": "widget", "total": 29.97 } ],
  "subtotal": 29.97,            // exact Decimal, never f64
  "receipt":  "Total due: 29.97" }
// "discount" dropped β€” `?:` omits null fields

Compile once, render against millions of inputs β€” into your #[derive(Deserialize)] struct, a serde_json::Value, or a dynamic Value.

Note

Pre-1.0, production-shaped. The language, API, and author tooling are complete (milestones 1–8 of 9); 267 tests at ~86% line coverage, benchmarks, a CLI, and CI. The one open milestone is a web editor component. Expect breaking changes only across pre-1.0 versions.


Why Temple

🎯 Exact decimals rust_decimal end to end β€” 129.99 * 0.0825 is exactly 10.724175. No f64 exists anywhere in the value model.
πŸ¦€ Typed output Renders straight into your #[derive(Deserialize)] structs via a custom serde Deserializer β€” or a dynamic Value with render_value.
πŸ›‘οΈ Never panics compile / render / from_bytes return Result on any template or input β€” checked arithmetic, parser depth & size caps, value-depth cap, iterative drop.
⚑ Compile once, render many Parse · validate · dependency-graph analysis happen at write time. The render path never re-parses; sub-¡s simple renders.
πŸ“¦ Versioned blobs to_bytes / from_bytes persist the compiled form as a signature-tagged CBOR blob and reload it without parsing β€” store it in any BYTEA/BLOB column.
🧭 Author tooling Template::format canonicalizes a template; compile / validate report every error at once as rustc-style underlined snippets with did-you-mean hints.
πŸͺΆ Lean core Five small dependencies; serde_json and the CLI are opt-in behind a feature. No unsafe code.

Install

[dependencies]
temple-dsl = { git = "https://github.com/sinha-sahil/temple-dsl", branch = "release" }

crates.io publishing is wired through CI and lands with the next release. The core library pulls in nothing extra; two opt-in features:

Feature Adds
json exact From conversions to/from serde_json::Value β€” JSON flows straight into render, decimals travel as verbatim digit tokens (never f64)
cli the temple binary (implies json)

Quick start

Render into your own types β€” compile once, render many:

use temple_dsl::{Template, Value};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Out { id: i64, name: String }

// Err(Vec<CompileError>) β€” every problem reported at once
let template = Template::compile(r#"{ "id": {{ input.id }}, "name": {{ upper(input.name) }} }"#)?;

let out: Out = template.render(Value::obj([
    ("id",   Value::Int(7)),
    ("name", Value::Str("ada".into())),
]))?;                                   // Out { id: 7, name: "ADA" }

Dynamic output when there's no struct: template.render_value(input) -> Result<Value, RenderError>. (render::<Value> is deliberately a compile error β€” use render_value, it skips the serde round-trip.)

JSON in, JSON out β€” with the json feature there's no manual conversion:

let input: serde_json::Value = serde_json::from_str(request_body)?;
let out = serde_json::Value::from(template.render_value(input)?);
// decimals stay exact in both directions β€” verbatim digits, never f64

Persist the compiled form and skip parsing at render time:

let blob: Vec<u8> = template.to_bytes();        // β†’ store in any BYTEA/BLOB column
let loaded = Template::from_bytes(&blob)?;      // deserialize + re-validate, no parse
let out: Out = loaded.render(input)?;

Every fallible call returns a structured error (CompileError, RenderError, LoadError) β€” Temple never panics and never decides failure handling for you.


The language

A .temple document is an optional let preamble followed by one output literal. Bare {{ expr }} holes yield a typed value; quoted "… {{ expr }} …" holes interpolate into a string. Entries are separated by commas or newlines; trailing commas are always fine; # starts a comment.

Feature Looks like
Paths & access input.cart.subtotal Β· input.items[0] Β· obj["key"] Β· input.coupon?.code ?? "none"
Operators + - * / % Β· == != < <= > >= Β· && || ! Β· cond ? a : b
when guards when { score >= 90: "A", score >= 80: "B", else: "C" }
let + this preamble let, plus this.total reads a sibling key β€” order-free, cycle-checked at compile time
let … in … local bindings anywhere: let rate = … in subtotal * rate (incl. inside when)
Array methods .map .filter .fold .sum .any .all .count .find .contains .index_of .sort .sort_by .reverse .unique .flatten .flat_map .take .drop .slice .join .min .max .avg .first .last .concat .len
String methods .contains .starts_with .ends_with .replace .split .slice .index_of .len
Object methods .keys .values .entries .has .get .merge
Lambdas x -> x * 2 Β· (acc, x) -> acc + x β€” bounded iteration only, never Turing-complete
Constructors array [a, b], object { "k": expr }, computed keys { [expr]: v }, omit-if-null "k"?: expr
Built-in functions abs round floor ceil min max upper lower trim to_string to_number concat type_of is_* json_encode url_encode base64

Methods chain on any expression β€” [3, 1].sort(), f(x).replace(…), (a.concat(b)).len() β€” not just paths. Put together, that covers real work like building an HTTP request with computed headers and conditional fields:

{
  "url":      {{ concat("https://", input.host, "/v1/rules") }},
  "headers":  { "Authorization": {{ concat("Bearer ", input.token) }} },
  "trace_id"?: {{ input.trace_id }},                       # absent when null
  "by_category": {{ input.items.fold({}, (acc, it) ->     # group-by via computed keys
      acc.merge({ [it.cat]: (acc.get(it.cat) ?? 0) + it.qty })) }}
}

See DESIGN.md for the complete spec and samples/ for worked templates β€” every shipped sample is compile-tested in CI.


Diagnostics

compile and validate report all problems in one pass β€” parser recovery plus resolver collection β€” and each error renders as an underlined snippet with line/column and a did-you-mean hint when a name is a near-miss:

error: unknown identifier 'inputt' β€” did you mean `input`?
  --> 2:15
  |
2 |   "total": {{ inputt.cart.subtotal }},
  |               ^^^^^^

error: unknown function 'rouns' β€” did you mean `round`?
  --> 3:15
  |
3 |   "tier":  {{ rouns(this.total) }}
  |               ^^^^^

Programmatic access: every error carries a Span; CompileError::report(src) renders one snippet, CompileError::report_all(src, &errs) renders the batch. Template::format(src) re-emits any parseable template in the canonical layout β€” deterministic and idempotent, ready for format-on-save.


CLI

The feature-gated temple binary renders a template against a JSON input, or prints its canonical formatting:

cargo install --path . --features cli

temple order.temple input.json    # render β†’ JSON on stdout
temple fmt order.temple           # canonical formatting on stdout

Compile errors print as the underlined snippets above; exit codes are script-friendly (0 ok, 1 error, 2 usage).


How it works

A template is compiled once, at save time β€” parsing, validation, and dependency-graph analysis all happen there. The render path loads the compiled artifact and evaluates it without re-parsing, so render latency is bounded by construction, not by template size at request time.

sequenceDiagram
    participant App as Application
    participant Temple
    participant Store as Storage

    Note over App,Store: Write time β€” once, when a template is saved
    App->>Temple: compile(source)
    Temple-->>App: Template
    App->>Store: persist source + to_bytes() blob

    Note over App,Store: Render time β€” on every request
    App->>Store: load compiled blob
    Store-->>App: blob
    App->>Temple: from_bytes(blob)
    Temple-->>App: Template (no parsing)
    App->>Temple: render(input)
    Temple-->>App: Ok(value) or Err(RenderError)
Loading

The blob is a signature-tagged, versioned CBOR encoding of the compiled module; from_bytes re-runs validation on load, so a corrupt or version-stale blob is rejected (recompile from the stored source) β€” never rendered.


Performance

Tree-walking evaluator, measured with cargo bench (Criterion, release):

Benchmark Time
compile_small β€” 3-field template ~0.8 Β΅s
compile_big β€” ~30 fields, 4 levels deep ~8 Β΅s
render_small ~0.5 Β΅s
render_big β€” 30 fields, decimals, struct round-trip ~5.5 Β΅s
compile + render β€” cold path ~1.4 Β΅s

Render cost scales with template size plus the input elements a render visits β€” a large passive input subtree adds nothing. A let … in … binding is an O(1) scope overlay, and lambdas flatten their scope once per collection walk, never per element. Memory: a compiled template holds ~15Γ— its source in RAM (β‰ˆ1.5 KB for a small one); a render allocates ~1 KB transient.


Reliability

The crate is built around a no-panic guarantee: compile, render, and from_bytes return typed errors on any template and any input.

  • Enforced structurally β€” checked arithmetic everywhere, parser depth cap, 1 MiB source cap, render-time value-depth cap, and an iterative Drop so even dropping a pathologically deep value cannot overflow the stack.
  • 268 tests, ~86% line coverage β€” feature-named suites plus a kitchen-sink test that exercises every operator, method, builtin, and structural feature in one template, asserted value-exactly and pushed through the blob and formatter round-trips.
  • Thrash-tested β€” fuzz-style sweeps over malformed sources, deep nesting, overflow inputs, and corrupted blobs (~245k cases at last full run).
  • Round-trip invariants under test β€” format is idempotent and its output always re-parses; to_bytesβ†’from_bytes renders identically to the original.
  • Deterministic β€” no I/O, clocks, or randomness in the language by design: same template + input β‡’ same output, always.

How Temple compares

Temple needs four things at once. Existing crates each miss at least one:

Crate Sub-ms render Exact decimals Typed Rust output Focused surface
minijinja βœ“ βœ— βœ— βœ—
jaq βœ“ βœ— βœ— βœ—
jsonata-core ~ βœ— βœ— βœ—
liquid_json ~ βœ— βœ— ~
Temple βœ“ βœ“ βœ“ βœ“

βœ“ yes Β· ~ partial Β· βœ— no. Evaluated for Temple's specific needs β€” not a general verdict on these crates.


Status & roadmap

The language, API, and author tooling are complete. The single remaining milestone is M9 β€” a web editor component: a browser widget that compiles the same engine to WASM and provides intellisense, inline diagnostics, and format-on-save out of the box. Plan: M9-EDITOR.md.

Full capability matrix β€” milestones 1–8 of 9 done
Capability Status
Object / array / scalar output, value literals βœ…
Bare-hole expressions, path access (input.a.b.c) βœ…
Comments (#), trailing commas, newline-separated entries βœ…
Decimal-correct arithmetic via rust_decimal βœ…
Typed output β€” render::<T> (serde) or render_value (dynamic Value); render::<Value> is a compile error βœ…
Compile once, render many (in-memory Template) βœ…
Result everywhere, no panics β€” checked arithmetic, parser depth/size caps, value-depth cap, iterative drop βœ…
Operators (+ - * / %, comparison, logical, unary -/!, parens) βœ…
Conditionals (when guards, ternary ?:, short-circuit &&/||) βœ…
Safe access ?. and nullish coalesce ?? βœ…
let preamble, this self-reference, compile-time DAG (cycle detection) βœ…
Core methods (map/filter/fold/sum/any/all/len/first/last/concat), lambdas, arr[i] indexing βœ…
Array stdlib: count/find/contains/index_of/sort/sort_by/reverse/unique/flatten/flat_map/take/drop/slice/join/min/max/avg βœ…
String methods: contains/starts_with/ends_with/replace/split/slice/index_of βœ…
Object methods + indexing: keys/values/entries/has/get/merge, obj["k"] βœ…
% operator; to_number, type_of/is_*, concat, json_encode, url_encode, base64 βœ…
Constructors: array […], object { "k": expr }, computed keys { [expr]: v }, omit-if-null "k"?: βœ…
let … in … local bindings; method/index chains on any expression βœ…
String interpolation in quoted holes β€” "Hi {{ input.name }}" (incl. \{ escape) βœ…
Built-in scalar functions (abs … to_string) βœ…
validate(src) author-time check; size/depth caps (early rejection) βœ…
to_bytes / from_bytes β€” versioned compiled blob, reload without reparsing βœ…
Template::format β€” canonical, idempotent pretty-printer βœ…
Diagnostics β€” multi-error reporting with parser recovery, rustc-style underlined snippets, did-you-mean βœ…
Milestone 9 β€” web editor component (intellisense, inline diagnostics, format-on-save) 🟑 planned

Project layout

Path What's there
src/ The library + the feature-gated temple CLI binary
DESIGN.md Full language spec, decisions, open questions
IMPLEMENTATION.md As-built map: file layout, sequence diagram, design choices
M9-EDITOR.md Plan for the milestone-9 web editor component
CAPABILITY-GAPS.md The feature-expansion record β€” what was added and why
FAQ.md How the engine actually works inside
samples/ Canonical .temple templates, one per pattern (compile-tested)
examples/ Runnable Rust demos against the public API
benches/ Criterion benchmark suite
e2e/ Out-of-crate end-to-end (Postgres) harness

Developing

cargo test --all-features     # full suite (267 tests)
cargo bench                   # Criterion benchmarks
cargo llvm-cov --all-features # coverage report

cargo fmt + cargo clippy -D warnings are enforced by a pre-commit hook (installed automatically via cargo-husky) and by CI on the release branch.


Author

Sahil Sinha Β· sahilsinha.dar@gmail.com

License

Dual-licensed under MIT OR Apache-2.0 β€” use it under either at your option.

About

Dynamic templating and runtime executor for rust based app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages