Crate rhai_loco

Crate rhai_loco 

Source
Expand description

§Rhai Engine Integration for Loco

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

This crate adds Rhai script support to Loco.

§Why Include a Scripting Engine

Although a system based upon Loco is usually compiled for maximum performance, there are times where user requirements are dynamic and need to be adapted to, preferably without recompilation.

Scripts are tremendously useful in the following cases:

  • Complex custom configuration or custom business logic per installation at different sites without recompilation. In a different programming language, DLL’s or dynamically-linked libraries may be used.

  • Rapidly adapt to changing environments (e.g. handle new data formats, input changes, or novel user errors etc.) without hard-coding the rules (which may soon change again).

  • Trial testing new features or business logic with fast iteration (without recompilation). The final version, once stable, can be converted into native Rust code for performance.

  • Develop Tera filters in script so they can be iterated quickly. Useful ones can then be converted into Rust native filters. This can normally be achieved via Tera macros, but the Rhai scripting language is more powerful and expressive than Tera expressions, allowing more complex logic to be implemented.

§Usage

Import rhai-loco inside Cargo.toml:

[dependencies]
rhai-loco = "0.15.0"

§Configuration

The Loco config section of initializers can be used to set options for the Rhai engine.

# 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

§Enable Scripted Tera Filters

Modify the ViewEngineInitializer under src/initializers/view_engine.rs:

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

///////////////////////////////////////////////////////////////////////////////////
// Within this method...
// Modify as follows to enable scripted Tera filters.
async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
    /////////////////////////////////////////////////////////////////
    // Add code to get scripting engine configuration
    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)?;
    let filters_path = config.filters_path.is_dir().then_some(config.filters_path);
    // End modification
    /////////////////////////////////////////////////////////////////

    let tera_engine = if std::path::Path::new(I18N_DIR).exists() {
        let arc = std::sync::Arc::new(
            ArcLoader::builder(&I18N_DIR, unic_langid::langid!("en-US"))
                .shared_resources(Some(&[I18N_SHARED.into()]))
                .customize(|bundle| bundle.set_use_isolating(false))
                .build()
                .map_err(|e| Error::string(&e.to_string()))?,
        );
        info!("locales loaded");

        engines::TeraView::build()?.post_process(move |tera| {
            ///////////////////////////////////////////////////////////////
            // Add Rhai scripted filters registration when not using i18n
            if let Some(ref path) = filters_path {
                rhai_loco::RhaiScript::register_tera_filters(tera, &path,
                    |_engine| {},   // custom configuration of the Rhai Engine, if any
                    FluentLoader::new(arc.clone()),
                )?;
                info!("Filter scripts loaded");
            }
            // End modification
            ///////////////////////////////////////////////////////////////
            tera.register_function("t", FluentLoader::new(arc.clone()));
            Ok(())
        })?
    /////////////////////////////////////////////////
    // Add Rhai scripted filters registration
    } else if let Some(path) = filters_path {
        engines::TeraView::build()?.post_process(move |tera| {
            rhai_loco::RhaiScript::register_tera_filters(tera, &path,
                |_engine| {},   // custom configuration of the Rhai Engine, if any
                FluentLoader::new(arc.clone()),
            )?;
            info!("Filter scripts loaded");
            Ok(())
        })?
    // End modification
    /////////////////////////////////////////////////
    } else {
        engines::TeraView::build()?
    };

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

Each Rhai script file (extension .rhai) can contain multiple filters. Sub-directories are ignored.

Each function inside the Rhai script file constitutes one filter, unless marked as private. The name of the function is the name of the filter.

§Function Signature

Each filter function must take exactly one parameter, which is an object-map containing all the variables in the filter call.

In addition, variables in the filter call can also be accessed as stand-alone variables.

The original data value is mapped to this.

§Example

For a filter call:

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

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

The filter function super_duper can be defined as follows in a Rhai script file:

┌─────────────┐
│ 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}!`
}

§Scripted filters as conversion/formatting tool

Scripted filters can be very flexible for ad-hoc conversion/formatting purposes because they enable rapid iterations and changes without recompiling.

┌────────────────────┐
│ 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>

The above is equivalent to the following Tera template.

Technically speaking, you either maintain such ad-hoc behavior in script or inside the Tera template itself, but doing so in script allows for reuse and a cleaner template.

┌───────────────┐
│ 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>

§Run a Rhai script in Loco Request

The scripting engine is first injected into Loco via the ScriptingEngineInitializer:

┌────────────┐
│ 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),
    ])
}

The scripting engine can then be extracted in requests using ScriptingEngine.

For example, the following adds custom scripting support to the login authentication process:

┌─────────────────────────┐
│ 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

                :
                :
}

This calls a function named login within the script file on_login.rhai if it exists:

┌──────────────────────────────┐
│ 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!`;
    }
}

§Custom Engine Setup

In order to customize the Rhai scripting engine, for example to add custom functions or custom types support, it is easy to perform custom setup on the Rhai engine via ScriptingEngineInitializerWithSetup:

┌────────────┐
│ 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),
    ])
}

Re-exports§

pub use tera;

Modules§

config
Configuration for Rhai.
module_resolvers
Module containing all built-in module resolvers.
packages
Module containing all built-in packages available to Rhai, plus facilities to define custom packages.
plugin
Module defining macros for developing plugins.
serde
(serde) Serialization and deserialization support for serde. Exported under the serde feature only.

Macros§

combine_with_exported_module
Macro to combine a plugin module into an existing module.
def_package
Macro that makes it easy to define a package (which is basically a shared module) and register functions into it.
exported_module
Macro to generate a Rhai Module from a plugin module defined via #[export_module].
register_exported_fn
Macro to register a plugin function (defined via #[export_fn]) into an Engine.
set_exported_fn
Macro to register a plugin function into a Rhai Module.
set_exported_global_fn
Macro to register a plugin function into a Rhai Module and expose it globally.

Structs§

AST
Compiled AST (abstract syntax tree) of a Rhai script.
CallFnOptions
Options for calling a script-defined function via Engine::call_fn_with_options.
Dynamic
Dynamic type containing any value.
Engine
Rhai main scripting engine.
EvalContext
Context of a script evaluation process.
Expression
An expression sub-tree in an AST.
FnPtr
A general function pointer, which may carry additional (i.e. curried) argument values to be passed onto a function during a call.
FuncRegistration
Type for fine-tuned module function registration.
ImmutableString
The system immutable string type.
Instant
A measurement of a monotonically nondecreasing clock. Opaque and useful only with Duration.
Locked
A reader-writer lock
Module
A module which may contain variables, sub-modules, external Rust functions, and/or script-defined functions.
NativeCallContext
Context of a native Rust function call.
ParseError
Error when parsing a script.
Position
A location (line number + character position) in the input script.
RhaiScript
A scripting engine based on Rhai.
Scope
Type containing information about the current scope. Useful for keeping state between Engine evaluation runs.
ScriptFnMetadata
A type containing the metadata of a script-defined function.
ScriptingEngine
Type that wraps a scripting engine for use in Axum handlers.
ScriptingEngineInitializerConfig
ScriptingEngineInitializerWithSetup
Loco initializer for the Rhai scripting engine with custom setup.
Shared
A thread-safe reference-counting pointer. ‘Arc’ stands for ‘Atomically Reference Counted’.
TypeBuilder
Builder to build the API of a custom type for use with an Engine.
VarDefInfo
Information on a variable declaration.

Enums§

EvalAltResult
Evaluation result.
FnAccess
A type representing the access mode of a function.
FnNamespace
A type representing the namespace of a function.
LexError
Error encountered when tokenizing the script text.
OptimizationLevel
Level of optimization performed.
ParseErrorType
Error encountered when parsing a script.

Constants§

FILTER_SCRIPTS_DIR
Directory containing Rhai scripts for Tera filters.
FUNC_TO_DEBUG
Standard debug-print function.
FUNC_TO_STRING
Standard pretty-print function.
OP_CONTAINS
Standard containment testing function.
OP_EQUALS
Standard equality comparison operator.
ROOT
Target namespace path for logging.
SCRIPTS_DIR
Directory containing Rhai scripts.

Statics§

ENGINE
Global Rhai Engine instance for scripts evaluation.
FILTERS_ENGINE
Global Rhai Engine instance for filter scripts evaluation.
RHAI_SCRIPT
Global RhaiScript instance for scripts evaluation.

Traits§

CustomType
Trait to build the API of a custom type for use with an Engine (i.e. register the type and its getters, setters, methods, etc.).
Func
Trait to create a Rust closure from a script.
FuncArgs
Trait that parses arguments to a function call.
ModuleResolver
Trait that encapsulates a module resolution service.
RhaiNativeFunc
Trait to register custom Rust functions.

Functions§

eval
Evaluate a string as a script, returning the result value or an error.
eval_file
Evaluate a script file, returning the result value or an error.
format_map_as_json
Return the JSON representation of an object map.
from_dynamic
Deserialize a Dynamic value into a Rust type that implements serde::Deserialize.
run
Evaluate a string as a script.
run_file
Evaluate a file.
to_dynamic
Serialize a Rust type that implements serde::Serialize into a Dynamic.

Type Aliases§

Array
Variable-sized array of Dynamic values.
Blob
Variable-sized array of u8 values (byte array).
FLOAT
The system floating-point type. It is defined as f64.
INT
The system integer type. It is defined as i64.
Map
A dictionary of Dynamic values with string keys.
RhaiResult
Type alias for Result<T, Box<EvalAltResult>>.
ScriptingEngineInitializer
Loco initializer for the Rhai scripting engine.

Attribute Macros§

export_fn
Attribute, when put on a Rust function, turns it into a plugin function.
export_module
Attribute, when put on a Rust module, turns it into a plugin module.
expose_under_internals
Macro to automatically expose a Rust function, type-def or use statement as pub when under the internals feature.

Derive Macros§

CustomType
Macro to implement the [CustomType][rhai::CustomType] trait.