#serde #logic #template #resolver #resolution #conditional #config

no-std tmpl-resolver

A lightweight template resolution engine with conditional logic support

5 releases

Uses new Rust 2024

new 0.0.10 Mar 11, 2025
0.0.9 Mar 11, 2025

#333 in Encoding

Download history 274/week @ 2025-03-04

274 downloads per month

Apache-2.0

47KB
840 lines

tmpl_resolver

A lightweight template resolution engine with conditional logic support.

Its syntax is similar to Mozilla Fluent, but it doesn't have as many features as Fluent.

The core logic is implemented using nom and can be used in no_std.

tmpl-resolver.crate Documentation

Apache-2 licensed

Key Concepts

  • Templates: Contain either direct text parts or conditional selectors
  • Selectors: Enable branch logic based on parameter values
  • Variable Resolution: Recursive resolution with context-aware lookup

Features

  • []
    • Minimal configuration for no_std use
  • ["all"]: Enable all features
  • ["std"]
    • Enables standard library
    • Uses ahash::HashMap for faster lookups
  • ["serde"]
    • Adds serialization capabilities
    • Enables template storage/transmission
  • ["bincode"]
    • Efficient binary serialization

Basic

use tmpl_resolver::{TemplateResolver, error::ResolverResult};

fn main() -> ResolverResult<()> {
  let resolver: TemplateResolver = [
      ("h", "Hello"),
      ("greeting", "{h} { $name }! Today is {$day}.")
    ]
    .try_into()?;

  let ctx = [("name", "Alice"), ("day", "Sunday")];

  let result = resolver.get_with_context("greeting", &ctx)?;
  assert_eq!(result, "Hello Alice! Today is Sunday.");
  Ok(())
}

Conditional Logic

use tmpl_resolver::{TemplateResolver, error::ResolverResult};

fn main() -> ResolverResult<()> {
  let selector_msg = [(
    "message",
    r#"
    $status ->
      [success] Operation succeeded!
      [error] Error occurred!
      *[default] Unknown status: {$status}
    "#
  )];

  let resolver: TemplateResolver = selector_msg.try_into()?;

  let success_msg = resolver.get_with_context("message", &[("status", "success")])?;

  assert_eq!(success_msg, "Operation succeeded!");
  Ok(())
}

Escape

  • "{{ a }}" => "a"
  • "{{{a}}}" => "a"
  • "{{{{ a }}}}" => "a"
  • "{{ {a} }}" => "{a}"
  • "{{a}" => ❌ nom Error, code: take_until
  • "{{{ {{a}} }}}" => "{{a}}"
  • "{{{ {{ a }} }}}" => "{{ a }}"
  • "{{{ {{a} }}}" => "{{a}"
use tmpl_resolver::{ResolverResult, TemplateResolver};

fn main() -> ResolverResult<()> {
  let resolver: TemplateResolver = [
    ("h", "Hello { $name }"),
    ("how_are_you", "How Are You"),
    ("greeting", "{h}!{{ how_are_you }}? {{    {$name} }}"),
  ]
  .try_into()?;

  let ctx = [("name", "Alice")];

  let result = resolver.get_with_context("greeting", &ctx)?;
  assert_eq!(result, "Hello Alice!how_are_you? {$name}");
  Ok(())
}

Real World Examples

Add dependencies

cargo add toml tap anyhow
cargo add tmpl-resolver --features=std,serde

Emoji

We can use emoji as variable identifier name.


toml:

"🐱" = "ฅ(°ω°ฅ)"
hello = "Hello {🐱}"
  1. hello references {🐱}
  2. expanding hello
  3. we would get "Hello ฅ(°ω°ฅ)".

rust:

let text = res.get_with_context("hello", &[])?;
assert_eq!(text, "Hello ฅ(°ω°ฅ)");

toml:

hello = "Hello {$🐱}"

$🐱 means that its value is passed in externally.

rust:

let text = res.get_with_context("hello", &[("🐱", "QwQ")])?;
assert_eq!(text, "Hello QwQ");

use tap::Pipe;
use tmpl_resolver::{ResolverResult, TemplateResolver, resolver::AHashRawMap};

fn main() -> ResolverResult<()> {
  let res: TemplateResolver = r##"
      "🐱" = "喵 ฅ(°ω°ฅ)"

      "问候" = """
        $period ->
          [morning] 早安{🐱}
          [night] 晚安{🐱}
          *[other] {$period}好
      """

      "称谓" = """
      $gender ->
        *[male] 先生
        [female] 女士
      """

      greeting = "{ 问候 }!{ $name }{ 称谓 }。"
    "##
    .pipe(toml::from_str::<AHashRawMap>)
    .expect("Failed to deserialize toml")
    .try_into()?;

  let get_text = |ctx| res.get_with_context("greeting", ctx);

  let text = [
    ("period", "morning"),
    ("name", "Young"),
    ("gender", "unknown"),
  ]
  .as_ref()
  .pipe(get_text)?;

  assert_eq!(text, "早安喵 ฅ(°ω°ฅ)!Young先生。");
  assert_eq!(res.get_with_context("🐱", &[])?, "喵 ฅ(°ω°ฅ)");

  Ok(())
}

Simple L10n Message

use anyhow::Result as AnyResult;
use tap::{Pipe, TryConv};
use tmpl_resolver::{TemplateResolver, resolver::AHashRawMap};

const EN_TOML: &str = r#"
  num-to-en = """
    $num ->
      [0] zero
      [1] one
      [2] two
      [3] three
      *[other] {$num}
  """

  unread_msg = "unread message"

  unread-count = """
    $num ->
      [0] No {unread_msg}s.
      [1] You have { num-to-en } {unread_msg}.
      *[other] You have { num-to-en } {unread_msg}s.
  """

  show-unread-messages-count = "{unread-count}"
"#;

const ZH_TOML: &str = r#"
  "阿拉伯数字转汉字" = """
    $num ->
      [0] 〇
      [1] 一
      [2] 二
      [3] 三
      *[其他] {$num}
  """

  "未读msg" = "未读消息"

  "显示未读消息数量" = """
    $num ->
        [0] 没有{ 未读msg }。
        [2] 您有两条{ 未读msg }。
       *[其他] 您有{ 阿拉伯数字转汉字 }条{ 未读msg }。
  """

  show-unread-messages-count = "{显示未读消息数量}"
"#;

fn main() -> AnyResult<()> {
  let get_text = |lang| -> AnyResult<_> {
    match lang {
      "zh" => ZH_TOML,
      _ => EN_TOML,
    }
    .pipe(toml::from_str::<AHashRawMap>)?
    .try_conv::<TemplateResolver>()?
    .pipe(|r| {
      move |num_str| {
        r.get_with_context("show-unread-messages-count", &[("num", num_str)])
      }
    })
    .pipe(Ok)
  };

  let get_en_text = get_text("en")?;
  assert_eq!(get_en_text("0")?, "No unread messages.");
  assert_eq!(get_en_text("1")?, "You have one unread message.");
  assert_eq!(get_en_text("2")?, "You have two unread messages.");
  assert_eq!(get_en_text("100")?, "You have 100 unread messages.");

  let get_zh_text = get_text("zh")?;
  assert_eq!(get_zh_text("0")?, "没有未读消息。");
  assert_eq!(get_zh_text("1")?, "您有一条未读消息。");
  assert_eq!(get_zh_text("2")?, "您有两条未读消息。");
  assert_eq!(get_zh_text("100")?, "您有100条未读消息。");

  Ok(())
}

Dependencies

~1.8–2.7MB
~58K SLoC