5个版本 (3个重大更改)

0.7.0 2024年8月9日
0.3.0 2024年7月2日
0.2.1 2024年2月29日
0.2.0 2024年2月27日
0.1.0 2024年2月24日

42模板引擎

Download history 163/week @ 2024-07-01 97/week @ 2024-08-05

97 每月下载量

MIT/Apache

33KB
335

Rhai引擎对Loco的集成

GitHub last commit Stars License crates.io crates.io API Docs

这个crate为Rhai添加了对Loco的支持。

为什么包含脚本引擎

尽管基于Loco的系统通常为了最大性能而编译,但有时用户需求是动态的,需要适应,最好是不需要重新编译。

脚本在以下情况下非常有用

  • 在不同地点安装的复杂自定义配置或自定义业务逻辑,无需重新编译。在另一种编程语言中,可以使用DLL或动态链接库。

  • 快速适应不断变化的环境(例如,处理新的数据格式、输入更改或新的用户错误等),而不需要硬编码规则(这些规则可能很快又需要更改)。

  • 通过快速迭代测试新功能或业务逻辑(无需重新编译)。一旦稳定,最终版本可以转换为本地Rust代码以提高性能。

  • 在脚本中开发Tera过滤器,以便快速迭代。有用的过滤器可以转换为Rust本地过滤器。这通常可以通过Tera宏来实现,但Rhai脚本语言比Tera表达式更强大和更具表现力,允许实现更复杂的逻辑。

用法

Cargo.toml中导入rhai-loco

[dependencies]
rhai-loco = "0.3.0"

配置

可以使用initializers中的Loco config部分来设置Rhai引擎的选项。

# Initializers configuration
initializers:
  # Scripting engine configuration
  scripting:
    # Directory holding scripts
    scripts_path: assets/scripts
    # Directory holding Tera filter scripts
    filters_path: assets/scripts/tera/filters

启用脚本化的Tera过滤器

修改ViewEngineInitializer下的src/initializers/view_engine.rs

┌─────────────────────────────────┐
│ src/initializers/view_engine.rs │
└─────────────────────────────────┘

// Within this method...
async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
    let mut tera_engine = engines::TeraView::build()?;

            :
            :

    ///////////////////////////////////////////////////////////////////////////////////
    // Add the following to enable scripted Tera filters

    // Get scripting engine configuration object
    let config = ctx.config.initializers.as_ref()
        .and_then(|m| m.get(rhai_loco::ScriptingEngineInitializer::NAME))
        .cloned().unwrap_or_default();

    let config: rhai_loco::ScriptingEngineInitializerConfig = serde_json::from_value(config)?;

    if config.filters_path.is_dir() {
        // This code is duplicated from the original code
        // to expose the i18n `t` function to Rhai scripts
        let i18n = if Path::new(I18N_DIR).is_dir() {
            let arc = ArcLoader::builder(I18N_DIR, unic_langid::langid!("de-DE"))
                .shared_resources(Some(&[I18N_SHARED.into()]))
                .customize(|bundle| bundle.set_use_isolating(false))
                .build()
                .map_err(|e| Error::string(&e.to_string()))?;
            Some(FluentLoader::new(arc))
        } else {
            None
        };
        rhai_loco::RhaiScript::register_tera_filters(
            &mut tera_engine,
            config.filters_path,
            |_engine| {},   // custom configuration of the Rhai Engine, if any
            i18n,
        )?;
        info!("Filter scripts loaded");
    }

    // End addition
    ///////////////////////////////////////////////////////////////////////////////////

    Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
}

每个Rhai脚本文件(扩展名.rhai)可以包含多个过滤器。子目录将被忽略。

除非被标记为private,Rhai脚本文件中的每个函数都构成一个过滤器。函数的名称就是过滤器的名称。

函数签名

每个过滤器函数必须恰好接受一个参数,该参数是一个包含过滤器调用中所有变量的对象映射。

此外,过滤器调用中的变量也可以作为独立的变量访问。

原始数据值映射到this

示例

对于一个过滤器调用

┌───────────────┐
│ Tera template │
└───────────────┘

{{ "hello" | super_duper(a = "world", b = 42, c = true) }}

可以在Rhai脚本文件中如下定义过滤器函数super_duper

┌─────────────┐
│ Rhai script │
└─────────────┘

// This private function is ignored
private fn do_something(x) {
    ...
}

// This function has the wrong number of parameters and is ignored
fn do_other_things(x, y, z) {
    ...
}

// Filter 'super_duper'
fn super_duper(vars) {
    // 'this' maps to "hello"
    // 'vars' contains 'a', 'b' and 'c'
    // The stand-alone variables 'a', 'b' and 'c' can also be accessed

    let name = if vars.b > 0 {  // access 'b' under 'vars'
        ...
    } else if c {               // access 'c'
        ...
    } else !a.is_empty() {      // access 'a'
        ...
    } else {
        ...
    }

    // 'this' can be modified
    this[0].to_upper();

    // Return new value
    `${this}, ${name}!`
}

脚本过滤器作为转换/格式化工具

脚本过滤器在即兴转换/格式化方面非常灵活,因为它们允许快速迭代和更改,而无需重新编译。

┌────────────────────┐
│ Rhai filter script │
└────────────────────┘

/// Say we have in-house status codes that we need to convert into text
/// for display with i18n support...
fn status(vars) {
    switch this {
        case "P" => t("Pending", lang),
        case "A" => t("Active", lang),
        case "C" => t("Cancelled", lang),
        case "X" => t("Deleted", lang),
    }
}

/// Use script to inject HTML also!
/// The input value is used to select from the list of options
fn all_status(vars) {`
    <option value="P" ${if this == "P" { "selected" }}>t("Pending", lang)</option>
    <option value="A" ${if this == "A" { "selected" }}>t("Active", lang)</option>
    <option value="C" ${if this == "C" { "selected" }}>t("Cancelled", lang)</option>
    <option value="X" ${if this == "X" { "selected" }}>t("Deleted", lang)</option>
`}

/// Say we have CSS classes that we need to add based on certain data values
fn count_css(vars) {
    if this.count > 1 {
        "error more-than-one"
    } else if this.count == 0 {
        "error missing-value"
    } else {
        "success"
    }
}
┌───────────────┐
│ Tera template │
└───────────────┘

<!-- use script to determine the CSS class -->
<div id="record" class="{{ value | count_css }}">
    <!-- use script to map the status display -->
    <span>{{ value.status | status(lang="de-DE") }} : {{ value.count }}</span>
</div>

<!-- use script to inject HTML directly -->
<select>
    <option value="">t("All", "de-DE")</option>
    <!-- avoid escaping as text via the `safe` filter -->
    {{ "A" | all_status(lang="de-DE") | safe }}
</select>

上述内容等同于以下Tera模板。

从技术上讲,您可以在脚本中或Tera模板本身中维护此类即兴行为,但在脚本中这样做可以提供重用和更干净的模板。

┌───────────────┐
│ Tera template │
└───────────────┘

<div id="record" class="{% if value.count > 1 %}
                            error more-than-one
                        {% elif value.count == 0 %}
                            error missing-value
                        {% else %}
                            success
                        {% endif %}">

    <span>
        {% if value.status == "P" %}
            t(key = "Pending", lang = "de-DE")
        {% elif value.status == "A" %}
            t(key = "Active", lang = "de-DE")
        {% elif value.status == "C" %}
            t(key = "Cancelled", lang = "de-DE")
        {% elif value.status == "D" %}
            t(key = "Deleted", lang = "de-DE")
        {% endif %}
        : {{ value.count }}
    </span>
</div>

在Loco请求中运行Rhai脚本

首先通过ScriptingEngineInitializer将脚本引擎注入到Loco中。

┌────────────┐
│ src/app.rs │
└────────────┘

async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
    Ok(vec![
        // Add the scripting engine initializer
        Box::new(rhai_loco::ScriptingEngineInitializer),
        Box::new(initializers::view_engine::ViewEngineInitializer),
    ])
}

然后可以使用ScriptingEngine在请求中提取脚本引擎。

例如,以下示例向登录认证过程添加了自定义脚本支持

┌─────────────────────────┐
│ src/controllers/auth.rs │
└─────────────────────────┘

// Import the scripting engine types
use rhai_loco::{RhaiScript, ScriptingEngine};

pub async fn login(
    State(ctx): State<AppContext>,
    // Extract the scripting engine
    ScriptingEngine(script): ScriptingEngine<RhaiScript>,
    Json(mut params): Json<LoginParams>,
) -> Result<Json<LoginResponse>> {
    // Use `run_script_if_exists` to run a function `login` from a script
    // `on_login.rhai` if it exists under `assets/scripts/`.
    //
    // Use `run_script` if the script is required to exist or an error is returned.
    let result = script
        .run_script_if_exists("on_login", &mut params, "login", ())
        //                    ^ script file            ^ function name
        //                                ^ data mapped to `this` in script
        //                                                      ^^ function arguments
        .or_else(|err| script.convert_runtime_error(err, |msg| unauthorized(&msg)))?;
        //                                               ^^^^^^^^^^^^^^^^^^^^^^^^
        //                      turn any runtime error into an unauthorized response

                :
                :
}

如果存在,这将调用脚本文件on_login.rhai中的名为login的函数

┌──────────────────────────────┐
│ assets/scripts/on_login.rhai │
└──────────────────────────────┘

// Function for custom login logic
fn login() {
    // Can import other Rhai modules!
    import "super/secure/vault" as vault;

    debug(`Trying to login with user = ${this.user} and password = ${this.password}`);

    let security_context = vault.extensive_checking(this.user, this.password);

    if security_context.passed {
        // Data values can be changed!
        this.user = security_context.masked_user;
        this.password = security_context.masked_password;
        return security_context.id;
    } else {
        vault::black_list(this.user);
        throw `The user ${this.user} has been black-listed!`;
    }
}

自定义引擎设置

为了自定义Rhai脚本引擎,例如添加自定义函数或自定义类型支持,可以通过ScriptingEngineInitializerWithSetup轻松地在Rhai引擎上执行自定义设置。

┌────────────┐
│ src/app.rs │
└────────────┘

async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
    Ok(vec![
        // Add the scripting engine initializer
        Box::new(rhai_loco::ScriptingEngineInitializerWithSetup::new_with_setup(|engine| {
                        :
            // ... do custom setup of Rhai engine here ...
                        :
        })),
        Box::new(initializers::view_engine::ViewEngineInitializer),
    ])
}

依赖项

~45–78MB
~1.5M SLoC