Rust模板引擎gtmpl的使用:轻量高效、支持Go风格模板语法的编译时渲染库

Rust模板引擎gtmpl的使用:轻量高效、支持Go风格模板语法的编译时渲染库

gtmpl-rust为Rust提供了Golang的text/template模板引擎实现,这使得Rust应用能够无缝集成到围绕Kubernetes、Docker等devops工具生态中。

快速开始

在Cargo.toml中添加以下依赖:

[dependencies]
gtmpl = "0.7"

示例代码

基础模板使用

use gtmpl;

fn main() {
    // 使用简单字符串作为模板和上下文
    let output = gtmpl::template("Finally! Some {{ . }} for Rust", "gtmpl");
    assert_eq!(&output.unwrap(), "Finally! Some gtmpl for Rust");
}

添加自定义函数

use gtmpl_value::Function;
use gtmpl::{FuncError, gtmpl_fn, template, Value};

fn main() {
    // 定义自定义加法函数
    gtmpl_fn!(
    fn add(a: u64, b: u64) -> Result<u64, FuncError> {
        Ok(a + b)
    });
    
    // 在模板中调用自定义函数
    let equal = template(r#"{{ call . 1 2 }}"#, Value::Function(Function { f: add }));
    assert_eq!(&equal.unwrap(), "3");
}

传递结构体作为上下文

use gtmpl_derive::Gtmpl;

// 使用Gtmpl派生宏使结构体可用于模板
#[derive(Gtmpl)]
struct Foo {
    bar: u8
}

fn main() {
    let foo = Foo { bar: 42 };
    // 在模板中访问结构体字段
    let output = gtmpl::template("The answer is: {{ .bar }}", foo);
    assert_eq!(&output.unwrap(), "The answer is: 42");
}

调用上下文方法

use gtmpl_derive::Gtmpl;
use gtmpl::{Func, FuncError, Value};

// 定义plus_one方法
fn plus_one(args: &[Value]) -> Result<Value, FuncError> {
    if let Value::Object(ref o) = &args[0] {
        if let Some(Value::Number(ref n)) = o.get("num") {
            if let Some(i) = n.as_i64() {
                return Ok((i +1).into())
            }
        }
    }
    Err(anyhow!("integer required, got: {:?}", args))
}

// 使用Gtmpl派生宏
#[derive(Gtmpl)]
struct AddMe {
    num: u8,
    plus_one: Func  // 包含自定义方法
}

fn main() {
    let add_me = AddMe { num: 42, plus_one };
    // 在模板中调用方法
    let output = gtmpl::template("The answer is: {{ .plus_one }}", add_me);
    assert_eq!(&output.unwrap(), "The answer is: 43");
}

当前限制

这是正在进行的工作,目前不支持以下功能:

  • 复数
  • 以下函数尚未实现:
    • html, js
  • printf还不完全稳定,但应该支持所有合理的输入

增强功能

虽然不打算扩展Golang text/template的语法,但可能会有一些方便的添加:

动态模板

在Cargo.toml中启用gtmpl_dynamic_template

[dependencies.gtmpl]
version = "0.7"
features = ["gtmpl_dynamic_template"]

现在可以为template动作使用动态模板名称。

示例

use gtmpl::{Context, Template};

fn main() {
    let mut template = Template::default();
    // 定义多个模板
    template
        .parse(
            r#"
            {{- define "tmpl1"}} some {{ end -}}
            {{- define "tmpl2"}} some other {{ end -}}
            there is {{- template (.) -}} template
            "#,
        )
        .unwrap();

    // 动态选择模板
    let context = Context::from("tmpl2");

    let output = template.render(&context);
    assert_eq!(output.unwrap(), "there is some other template".to_string());
}

使用以下语法:

{{template (pipeline)}}
    The template with the name evaluated from the pipeline (parenthesized) is
    executed with nil data.

{{template (pipeline) pipeline}}
    The template with the name evaluated from the first pipeline (parenthesized)
    is executed with dot set to the value of the second pipeline.

上下文

使用gtmpl_value的Value作为内部数据类型。gtmpl_derive提供了一个方便的derive宏来为Value生成From实现。

为什么需要这个?

主要动机是使在Rust中编写devops工具更容易,这些工具感觉更原生。Docker和Helm(Kubernetes)使用golang模板,如果围绕它们的工具使用相同的模板,感觉会更原生。

完整示例Demo

下面是一个结合了多个功能的完整示例:

use gtmpl::{template, Value, FuncError, Func};
use gtmpl_derive::Gtmpl;
use gtmpl_value::Function;

// 自定义结构体
#[derive(Gtmpl)]
struct User {
    name: String,
    age: u8,
    // 可以包含自定义方法
    greet: Func,
}

// 自定义方法实现
fn greet_user(args: &[Value]) -> Result<Value, FuncError> {
    if let Value::Object(ref o) = &args[0] {
        if let Some(Value::String(ref name)) = o.get("name") {
            if let Some(Value::Number(ref age)) = o.get("age") {
                if let Some(age) = age.as_u64() {
                    return Ok(Value::String(format!("Hello {}, you are {} years old!", name, age)));
                }
            }
        }
    }
    Err(FuncError::Custom("Invalid arguments".into()))
}

// 自定义函数
gtmpl_fn!(
fn double(x: i32) -> Result<i32, FuncError> {
    Ok(x * 2)
});

fn main() {
    // 创建用户实例
    let user = User {
        name: "Alice".to_string(),
        age: 30,
        greet: Func { f: greet_user },
    };
    
    // 定义模板
    let tmpl = r#"
    User Information:
    Name: {{ .name }}
    Age: {{ .age }}
    Double Age: {{ call .double .age }}
    Greeting: {{ .greet }}
    "#;
    
    // 添加自定义函数到上下文
    let mut context = Value::from(user);
    if let Value::Object(ref mut o) = context {
        o.insert("double".to_string(), Value::Function(Function { f: double }));
    }
    
    // 渲染模板
    let output = template(tmpl, context).unwrap();
    println!("{}", output);
}

这个完整示例展示了:

  1. 使用自定义结构体作为模板上下文
  2. 在结构体中定义方法并在模板中调用
  3. 添加独立的自定义函数并在模板中使用
  4. 复杂的模板渲染场景

输出结果类似:

User Information:
Name: Alice
Age: 30
Double Age: 60
Greeting: Hello Alice, you are 30 years old!

1 回复

Rust模板引擎gtmpl的使用指南

介绍

gtmpl是一个轻量高效的Rust模板引擎,支持Go风格的模板语法,并在编译时完成渲染。它的主要特点包括:

  • 完全兼容Go的text/template语法
  • 编译时模板解析和验证
  • 零运行时开销
  • 类型安全的模板渲染
  • 支持模板继承、部分模板和管道操作

安装

在Cargo.toml中添加依赖:

[dependencies]
gtmpl = "0.7"
gtmpl_derive = "0.7"

基本用法

1. 定义模板

use gtmpl::{Template, Context};
use gtmpl_derive::Gtmpl;

// 定义数据结构并派生Gtmpl trait
#[derive(Gtmpl)]
struct Person {
    name: String,
    age: u32,
}

// 创建模板实例
let template = Template::new("Hello, {{.name}}! You are {{.age}} years old.");

2. 渲染模板

// 创建数据实例
let person = Person {
    name: "Alice".to_string(),
    age: 30,
};

// 创建渲染上下文
let ctx = Context::from_serialize(&person).unwrap();
// 渲染模板
let output = template.render(&ctx).unwrap();
println!("{}", output); // 输出: Hello, Alice! You are 30 years old.

高级功能

条件语句

let template = Template::new(r#"
{{if .age >= 18}}
    {{.name}} is an adult.
{{else}}
    {{.name}} is a minor.
{{end}}
"#);

let person = Person {
    name: "Bob".to_string(),
    age: 16,
};

let ctx = Context::from_serialize(&person).unwrap();
println!("{}", template.render(&ctx).unwrap());
// 输出: Bob is a minor.

循环

// 定义包含集合的数据结构
#[derive(Gtmpl)]
struct Team {
    name: String,
    members: Vec<Person>,
}

let team = Team {
    name: "Rustaceans".to_string(),
    members: vec![
        Person { name: "Alice".to_string(), age: 30 },
        Person { name: "Bob".to_string(), age: 25 },
    ],
};

let template = Template::new(r#"
Team {{.name}} members:
{{range .members}}
- {{.name}} ({{.age}})
{{end}}
"#);

let ctx = Context::from_serialize(&team).unwrap();
println!("{}", template.render(&ctx).unwrap());
/* 输出:
Team Rustaceans members:
- Alice (30)
- Bob (25)
*/

模板函数

use gtmpl_value::Func;

let mut template = Template::new("{{greet .name}}");
// 注册自定义模板函数
template.register_func("greet", |name: String| format!("Hello, {}!", name));

let ctx = Context::from_serialize(&Person {
    name: "Charlie".to_string(),
    age: 40,
}).unwrap();

println!("{}", template.render(&ctx).unwrap());
// 输出: Hello, Charlie!

编译时模板

use gtmpl_derive::template;

// 使用宏在编译时解析模板
#[template(source = "{{.name}} is {{.age}} years old.")]
static PERSON_TEMPLATE: Template;

let ctx = Context::from_serialize(&Person {
    name: "Dave".to_string(),
    age: 50,
}).unwrap();

println!("{}", PERSON_TEMPLATE.render(&ctx).unwrap());
// 输出: Dave is 50 years old.

完整示例

use gtmpl::{Template, Context};
use gtmpl_derive::{Gtmpl, template};
use gtmpl_value::Func;

// 1. 定义数据结构
#[derive(Gtmpl)]
struct Person {
    name: String,
    age: u32,
    hobbies: Vec<String>,
}

// 2. 定义编译时模板
#[template(source = r#"
{{.name}}的个人信息:
年龄: {{.age}}
{{if gt .age 18}}已成年{{else}}未成年{{end}}
爱好:
{{range .hobbies}}
- {{.}}
{{end}}
"#)]
static PROFILE_TEMPLATE: Template;

fn main() {
    // 3. 准备数据
    let person = Person {
        name: "Eve".to_string(),
        age: 22,
        hobbies: vec!["编程".to_string(), "阅读".to_string(), "运动".to_string()],
    };
    
    // 4. 创建运行时模板并注册函数
    let mut runtime_template = Template::new("{{upper .name}}");
    runtime_template.register_func("upper", |s: String| s.to_uppercase());
    
    // 5. 渲染编译时模板
    let ctx = Context::from_serialize(&person).unwrap();
    println!("{}", PROFILE_TEMPLATE.render(&ctx).unwrap());
    
    // 6. 渲染运行时模板
    println!("{}", runtime_template.render(&ctx).unwrap());
}

输出结果:

Eve的个人信息:
年龄: 22
已成年
爱好:
- 编程
- 阅读
- 运动

EVE

性能建议

  1. 对于静态模板,使用#[template]宏在编译时解析
  2. 重用Template实例,它们可以安全地跨线程共享
  3. 对于复杂数据结构,实现gtmpl::Templateable trait以获得最佳性能

gtmpl是Rust生态中一个优秀的模板引擎选择,特别适合需要高性能和Go风格模板语法的场景。

回到顶部