/
mcmare
/
RustAPI
Обзор
Документация
Войти
/
mcmare
/
RustAPI
Код
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
v0.1.0
engine/src/script/mod.rs
138 строк
5 KB
mcmare
Add hot reload via file watcher; fix cross-cutting middleware to wrap merged router
18 июл 2026, 16:48
18 июл 2026, 16:48
104d8a6
Код
Авторство
О чём код?
mod convert; mod db_bridge; mod host_fns; pub use host_fns::ScriptResponse; use anyhow::Context; use rhai::Dynamic; use rustapi_core::ApiError; use serde_json::Value; use std::{ collections::HashMap, path::{Path, PathBuf}, sync::Arc, }; use crate::db::DbPool; use convert::json_to_dynamic; use db_bridge::{DbApi, DbContext}; use host_fns::ResponseBuilder; /// Everything a script's `request` object exposes. #[derive(Default)] pub struct ScriptRequest { pub body: Value, pub query: HashMap<String, String>, pub params: HashMap<String, String>, pub headers: HashMap<String, String>, } impl ScriptRequest { fn to_json(&self) -> Value { serde_json::json!({ "body": self.body, "query": self.query, "params": self.params, "headers": self.headers, }) } } pub struct ScriptEngine { engine: Arc<rhai::Engine>, pools: Arc<HashMap<String, DbPool>>, cache: std::sync::Mutex<HashMap<PathBuf, Arc<rhai::AST>>>, } impl ScriptEngine { pub fn new(pools: Arc<HashMap<String, DbPool>>, jwt_secret: String) -> Self { let mut engine = rhai::Engine::new(); // Runaway-script guardrails: a buggy/malicious script fails loudly instead of // hanging the worker thread or exhausting memory. engine.set_max_operations(2_000_000); engine.set_max_string_size(1_000_000); engine.set_max_array_size(100_000); engine.set_max_map_size(10_000); engine.set_max_call_levels(64); host_fns::register(&mut engine, jwt_secret); db_bridge::register(&mut engine); Self { engine: Arc::new(engine), pools, cache: std::sync::Mutex::new(HashMap::new()), } } /// Drops every cached compiled script so the next call recompiles from disk — used by /// hot reload after any config change (cheap: scripts recompile lazily, on demand). pub fn invalidate_all(&self) { self.cache.lock().unwrap().clear(); } /// Drops every cached compiled script so the next call recompiles from disk — /// used by hot reload when a `.rhai` file changes. pub fn invalidate(&self, script_path: &Path) { self.cache.lock().unwrap().remove(script_path); } fn compile(&self, script_path: &Path) -> Result<Arc<rhai::AST>, ApiError> { { let cache = self.cache.lock().unwrap(); if let Some(ast) = cache.get(script_path) { return Ok(ast.clone()); } } let source = std::fs::read_to_string(script_path) .with_context(|| format!("failed to read script {}", script_path.display()))?; let ast = self .engine .compile(&source) .map_err(|e| anyhow::anyhow!("failed to compile {}: {e}", script_path.display()))?; let ast = Arc::new(ast); self.cache.lock().unwrap().insert(script_path.to_path_buf(), ast.clone()); Ok(ast) } /// Runs a script end to end: compiles (or reuses the cached AST), executes it on a /// blocking thread (Rhai is sync; DB host functions bridge back to async via /// `Handle::block_on` from inside that thread), then commits or rolls back every /// transaction the script opened depending on whether it succeeded. pub async fn run(&self, script_path: &Path, request: ScriptRequest) -> Result<ScriptResponse, ApiError> { let ast = self.compile(script_path)?; let engine = self.engine.clone(); let db_ctx = DbContext::new(self.pools.clone()); let db_ctx_finish = db_ctx.clone(); let request_json = request.to_json(); let script_path_owned = script_path.to_path_buf(); let eval_result = tokio::task::spawn_blocking(move || { let mut scope = rhai::Scope::new(); scope.push("request", json_to_dynamic(&request_json)); scope.push("response", ResponseBuilder); scope.push("db", DbApi { ctx: db_ctx }); engine.eval_ast_with_scope::<Dynamic>(&mut scope, &ast) }) .await; let outcome = match eval_result { Ok(Ok(dynamic)) => dynamic.try_cast::<ScriptResponse>().ok_or_else(|| { anyhow::anyhow!( "{}: script must end with response.json(...) or response.error(...)", script_path_owned.display() ) }), Ok(Err(rhai_err)) => Err(anyhow::anyhow!("{}: {rhai_err}", script_path_owned.display())), Err(join_err) => Err(anyhow::anyhow!("{}: script task failed: {join_err}", script_path_owned.display())), }; db_ctx_finish .finish(outcome.is_ok()) .await .context("finalizing script transaction")?; outcome.map_err(ApiError::Internal) } }