{"id":29066903,"url":"https://github.com/rhaiscript/rhai-loco","last_synced_at":"2025-06-27T10:08:54.316Z","repository":{"id":224176132,"uuid":"762643276","full_name":"rhaiscript/rhai-loco","owner":"rhaiscript","description":"Rhai integration for Loco.","archived":false,"fork":false,"pushed_at":"2025-06-02T04:13:15.000Z","size":69,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-02T12:47:24.307Z","etag":null,"topics":["loco","rhai","scripting-engine","scripting-language"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rhaiscript.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-24T09:31:48.000Z","updated_at":"2025-06-02T04:13:18.000Z","dependencies_parsed_at":"2024-02-29T02:41:19.569Z","dependency_job_id":"ab5f7792-c54e-47d8-8f9a-8c54cbd60def","html_url":"https://github.com/rhaiscript/rhai-loco","commit_stats":null,"previous_names":["rhaiscript/rhai-loco"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/rhaiscript/rhai-loco","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rhaiscript%2Frhai-loco","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rhaiscript%2Frhai-loco/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rhaiscript%2Frhai-loco/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rhaiscript%2Frhai-loco/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rhaiscript","download_url":"https://codeload.github.com/rhaiscript/rhai-loco/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rhaiscript%2Frhai-loco/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262235784,"owners_count":23279567,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["loco","rhai","scripting-engine","scripting-language"],"created_at":"2025-06-27T10:08:53.243Z","updated_at":"2025-06-27T10:08:54.298Z","avatar_url":"https://github.com/rhaiscript.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"Rhai Engine Integration for Loco\n================================\n\n![GitHub last commit](https://img.shields.io/github/last-commit/rhaiscript/rhai-loco?logo=github)\n[![Stars](https://img.shields.io/github/stars/rhaiscript/rhai-loco?style=flat\u0026logo=github)](https://github.com/rhaiscript/rhai-loco)\n[![License](https://img.shields.io/crates/l/rhai-loco)](https://github.com/license/rhaiscript/rhai-loco)\n[![crates.io](https://img.shields.io/crates/v/rhai-loco?logo=rust)](https://crates.io/crates/rhai-loco/)\n[![crates.io](https://img.shields.io/crates/d/rhai-loco?logo=rust)](https://crates.io/crates/rhai-loco/)\n[![API Docs](https://docs.rs/rhai-loco/badge.svg?logo=docs-rs)](https://docs.rs/rhai-loco/)\n\nThis crate adds [Rhai](https://rhai.rs) script support to [Loco](https://loco.rs).\n\n\nWhy Include a Scripting Engine\n------------------------------\n\nAlthough a system based upon [Loco](https://loco.rs) is usually compiled for maximum performance, there are times where user requirements are dynamic and need to be adapted to, preferably without recompilation.\n\nScripts are tremendously useful in the following cases:\n\n* 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.\n\n* 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).\n\n* 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.\n\n* Develop [Tera](https://crates.io/crates/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](https://crates.io/crates/tera) macros, but the Rhai scripting language is more powerful and expressive than [Tera](https://crates.io/crates/tera) expressions, allowing more complex logic to be implemented.\n\n\nUsage\n-----\n\nImport `rhai-loco` inside `Cargo.toml`:\n\n```toml\n[dependencies]\nrhai-loco = \"0.15.0\"\n```\n\n\nConfiguration\n-------------\n\nThe Loco `config` section of `initializers` can be used to set options for the Rhai engine.\n\n```yaml\n# Initializers configuration\ninitializers:\n  # Scripting engine configuration\n  scripting:\n    # Directory holding scripts\n    scripts_path: assets/scripts\n    # Directory holding Tera filter scripts\n    filters_path: assets/scripts/tera/filters\n```\n\n\nEnable Scripted Tera Filters\n----------------------------\n\nModify the `ViewEngineInitializer` under `src/initializers/view_engine.rs`:\n\n```rust\n┌─────────────────────────────────┐\n│ src/initializers/view_engine.rs │\n└─────────────────────────────────┘\n\n// Within this method...\nasync fn after_routes(\u0026self, router: AxumRouter, ctx: \u0026AppContext) -\u003e Result\u003cAxumRouter\u003e {\n    let mut tera_engine = engines::TeraView::build()?;\n\n            :\n            :\n\n    ///////////////////////////////////////////////////////////////////////////////////\n    // Add the following to enable scripted Tera filters\n\n    // Get scripting engine configuration object\n    let config = ctx.config.initializers.as_ref()\n        .and_then(|m| m.get(rhai_loco::ScriptingEngineInitializer::NAME))\n        .cloned().unwrap_or_default();\n\n    let config: rhai_loco::ScriptingEngineInitializerConfig = serde_json::from_value(config)?;\n\n    if config.filters_path.is_dir() {\n        // This code is duplicated from the original code\n        // to expose the i18n `t` function to Rhai scripts\n        let i18n = if Path::new(I18N_DIR).is_dir() {\n            let arc = ArcLoader::builder(I18N_DIR, unic_langid::langid!(\"de-DE\"))\n                .shared_resources(Some(\u0026[I18N_SHARED.into()]))\n                .customize(|bundle| bundle.set_use_isolating(false))\n                .build()\n                .map_err(|e| Error::string(\u0026e.to_string()))?;\n            Some(FluentLoader::new(arc))\n        } else {\n            None\n        };\n        rhai_loco::RhaiScript::register_tera_filters(\n            \u0026mut tera_engine,\n            config.filters_path,\n            |_engine| {},   // custom configuration of the Rhai Engine, if any\n            i18n,\n        )?;\n        info!(\"Filter scripts loaded\");\n    }\n\n    // End addition\n    ///////////////////////////////////////////////////////////////////////////////////\n\n    Ok(router.layer(Extension(ViewEngine::from(tera_engine))))\n}\n```\n\nEach Rhai script file (extension `.rhai`) can contain multiple filters. Sub-directories are ignored.\n\nEach function inside the Rhai script file constitutes one filter, unless marked as `private`.\nThe name of the function is the name of the filter.\n\n\n### Function Signature\n\nEach filter function must take exactly _one_ parameter, which is an object-map containing all the\nvariables in the filter call.\n\nIn addition, variables in the filter call can also be accessed as stand-alone variables.\n\nThe original data value is mapped to `this`.\n\n\n### Example\n\nFor a filter call:\n\n```tera\n┌───────────────┐\n│ Tera template │\n└───────────────┘\n\n{{ \"hello\" | super_duper(a = \"world\", b = 42, c = true) }}\n```\n\nThe filter function `super_duper` can be defined as follows in a Rhai script file:\n\n```js\n┌─────────────┐\n│ Rhai script │\n└─────────────┘\n\n// This private function is ignored\nprivate fn do_something(x) {\n    ...\n}\n\n// This function has the wrong number of parameters and is ignored\nfn do_other_things(x, y, z) {\n    ...\n}\n\n// Filter 'super_duper'\nfn super_duper(vars) {\n    // 'this' maps to \"hello\"\n    // 'vars' contains 'a', 'b' and 'c'\n    // The stand-alone variables 'a', 'b' and 'c' can also be accessed\n\n    let name = if vars.b \u003e 0 {  // access 'b' under 'vars'\n        ...\n    } else if c {               // access 'c'\n        ...\n    } else !a.is_empty() {      // access 'a'\n        ...\n    } else {\n        ...\n    }\n\n    // 'this' can be modified\n    this[0].to_upper();\n\n    // Return new value\n    `${this}, ${name}!`\n}\n```\n\n### Scripted filters as conversion/formatting tool\n\nScripted filters can be very flexible for ad-hoc conversion/formatting purposes because they enable\nrapid iterations and changes without recompiling.\n\n```rust\n┌────────────────────┐\n│ Rhai filter script │\n└────────────────────┘\n\n/// Say we have in-house status codes that we need to convert into text\n/// for display with i18n support...\nfn status(vars) {\n    switch this {\n        case \"P\" =\u003e t(\"Pending\", lang),\n        case \"A\" =\u003e t(\"Active\", lang),\n        case \"C\" =\u003e t(\"Cancelled\", lang),\n        case \"X\" =\u003e t(\"Deleted\", lang),\n    }\n}\n\n/// Use script to inject HTML also!\n/// The input value is used to select from the list of options\nfn all_status(vars) {`\n    \u003coption value=\"P\" ${if this == \"P\" { \"selected\" }}\u003et(\"Pending\", lang)\u003c/option\u003e\n    \u003coption value=\"A\" ${if this == \"A\" { \"selected\" }}\u003et(\"Active\", lang)\u003c/option\u003e\n    \u003coption value=\"C\" ${if this == \"C\" { \"selected\" }}\u003et(\"Cancelled\", lang)\u003c/option\u003e\n    \u003coption value=\"X\" ${if this == \"X\" { \"selected\" }}\u003et(\"Deleted\", lang)\u003c/option\u003e\n`}\n\n/// Say we have CSS classes that we need to add based on certain data values\nfn count_css(vars) {\n    if this.count \u003e 1 {\n        \"error more-than-one\"\n    } else if this.count == 0 {\n        \"error missing-value\"\n    } else {\n        \"success\"\n    }\n}\n```\n\n```html\n┌───────────────┐\n│ Tera template │\n└───────────────┘\n\n\u003c!-- use script to determine the CSS class --\u003e\n\u003cdiv id=\"record\" class=\"{{ value | count_css }}\"\u003e\n    \u003c!-- use script to map the status display --\u003e\n    \u003cspan\u003e{{ value.status | status(lang=\"de-DE\") }} : {{ value.count }}\u003c/span\u003e\n\u003c/div\u003e\n\n\u003c!-- use script to inject HTML directly --\u003e\n\u003cselect\u003e\n    \u003coption value=\"\"\u003et(\"All\", \"de-DE\")\u003c/option\u003e\n    \u003c!-- avoid escaping as text via the `safe` filter --\u003e\n    {{ \"A\" | all_status(lang=\"de-DE\") | safe }}\n\u003c/select\u003e\n```\n\nThe above is equivalent to the following Tera template.\n\nTechnically speaking, you either maintain such ad-hoc behavior in script or inside the Tera template\nitself, but doing so in script allows for reuse and a cleaner template.\n\n```html\n┌───────────────┐\n│ Tera template │\n└───────────────┘\n\n\u003cdiv id=\"record\" class=\"{% if value.count \u003e 1 %}\n                            error more-than-one\n                        {% elif value.count == 0 %}\n                            error missing-value\n                        {% else %}\n                            success\n                        {% endif %}\"\u003e\n\n    \u003cspan\u003e\n        {% if value.status == \"P\" %}\n            t(key = \"Pending\", lang = \"de-DE\")\n        {% elif value.status == \"A\" %}\n            t(key = \"Active\", lang = \"de-DE\")\n        {% elif value.status == \"C\" %}\n            t(key = \"Cancelled\", lang = \"de-DE\")\n        {% elif value.status == \"D\" %}\n            t(key = \"Deleted\", lang = \"de-DE\")\n        {% endif %}\n        : {{ value.count }}\n    \u003c/span\u003e\n\u003c/div\u003e\n```\n\n\nRun a Rhai script in Loco Request\n---------------------------------\n\nThe scripting engine is first injected into Loco via the `ScriptingEngineInitializer`:\n\n```rust\n┌────────────┐\n│ src/app.rs │\n└────────────┘\n\nasync fn initializers(_ctx: \u0026AppContext) -\u003e Result\u003cVec\u003cBox\u003cdyn Initializer\u003e\u003e\u003e {\n    Ok(vec![\n        // Add the scripting engine initializer\n        Box::new(rhai_loco::ScriptingEngineInitializer),\n        Box::new(initializers::view_engine::ViewEngineInitializer),\n    ])\n}\n```\n\nThe scripting engine can then be extracted in requests using `ScriptingEngine`.\n\nFor example, the following adds custom scripting support to the login authentication process:\n\n```rust\n┌─────────────────────────┐\n│ src/controllers/auth.rs │\n└─────────────────────────┘\n\n// Import the scripting engine types\nuse rhai_loco::{RhaiScript, ScriptingEngine};\n\npub async fn login(\n    State(ctx): State\u003cAppContext\u003e,\n    // Extract the scripting engine\n    ScriptingEngine(script): ScriptingEngine\u003cRhaiScript\u003e,\n    Json(mut params): Json\u003cLoginParams\u003e,\n) -\u003e Result\u003cJson\u003cLoginResponse\u003e\u003e {\n    // Use `run_script_if_exists` to run a function `login` from a script\n    // `on_login.rhai` if it exists under `assets/scripts/`.\n    //\n    // Use `run_script` if the script is required to exist or an error is returned.\n    let result = script\n        .run_script_if_exists(\"on_login\", \u0026mut params, \"login\", ())\n        //                    ^ script file            ^ function name\n        //                                ^ data mapped to `this` in script\n        //                                                      ^^ function arguments\n        .or_else(|err| script.convert_runtime_error(err, |msg| unauthorized(\u0026msg)))?;\n        //                                               ^^^^^^^^^^^^^^^^^^^^^^^^\n        //                      turn any runtime error into an unauthorized response\n\n                :\n                :\n}\n```\n\nThis calls a function named `login` within the script file `on_login.rhai` if it exists:\n\n```rust\n┌──────────────────────────────┐\n│ assets/scripts/on_login.rhai │\n└──────────────────────────────┘\n\n// Function for custom login logic\nfn login() {\n    // Can import other Rhai modules!\n    import \"super/secure/vault\" as vault;\n\n    debug(`Trying to login with user = ${this.user} and password = ${this.password}`);\n\n    let security_context = vault.extensive_checking(this.user, this.password);\n\n    if security_context.passed {\n        // Data values can be changed!\n        this.user = security_context.masked_user;\n        this.password = security_context.masked_password;\n        return security_context.id;\n    } else {\n        vault::black_list(this.user);\n        throw `The user ${this.user} has been black-listed!`;\n    }\n}\n```\n\n\nCustom Engine Setup\n-------------------\n\nIn order to customize the Rhai scripting engine, for example to add custom functions or custom types\nsupport, it is easy to perform custom setup on the Rhai engine via `ScriptingEngineInitializerWithSetup`:\n\n```rust\n┌────────────┐\n│ src/app.rs │\n└────────────┘\n\nasync fn initializers(_ctx: \u0026AppContext) -\u003e Result\u003cVec\u003cBox\u003cdyn Initializer\u003e\u003e\u003e {\n    Ok(vec![\n        // Add the scripting engine initializer\n        Box::new(rhai_loco::ScriptingEngineInitializerWithSetup::new_with_setup(|engine| {\n                        :\n            // ... do custom setup of Rhai engine here ...\n                        :\n        })),\n        Box::new(initializers::view_engine::ViewEngineInitializer),\n    ])\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frhaiscript%2Frhai-loco","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frhaiscript%2Frhai-loco","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frhaiscript%2Frhai-loco/lists"}