//! JSON ↔ Rhai `Dynamic` value bridge. //! //! Originally inline in `engine.rs`; moved here for v1.1.0 so future //! service modules (KV in v1.1.1, docs in v1.1.2, …) can convert //! values without `engine.rs` being the only owner of the conversions. //! Behaviour is unchanged from the pre-extraction implementation — //! `sdk_contract.rs::json_round_trip_preserves_nested_shapes` pins the //! observable round-trip. use rhai::{Dynamic, EvalAltResult, Map}; use serde_json::Value as Json; use tokio::runtime::Handle as TokioHandle; /// Run an async future inside the synchronous Rhai context. /// /// `LocalExecutorClient` wraps script execution in `spawn_blocking`, so /// the current Tokio runtime is reachable via `Handle::current()`. We /// block on it directly; we are NOT calling this from an async task, /// so blocking is the correct primitive. /// /// Prefix each error string with `service` so a script reading the /// runtime error message learns which SDK surface threw it. /// /// # Errors /// /// Wraps the future's error variant or "no tokio runtime available" in /// `EvalAltResult::ErrorRuntime`, suitable to return from a Rhai- /// registered fn. pub fn block_on(service: &str, fut: F) -> Result> where F: std::future::Future>, E: std::fmt::Display, { let handle = TokioHandle::try_current().map_err(|e| -> Box { EvalAltResult::ErrorRuntime( format!("{service}: no tokio runtime available: {e}").into(), rhai::Position::NONE, ) .into() })?; handle.block_on(fut).map_err(|err| -> Box { EvalAltResult::ErrorRuntime(format!("{service}: {err}").into(), rhai::Position::NONE).into() }) } /// Convert a `serde_json::Value` into a Rhai `Dynamic` suitable for /// pushing into a script's scope. Numbers prefer the narrowest type /// (`i64` over `f64`); anything that can't round-trip falls back to a /// string so the script always sees a defined value. pub fn json_to_dynamic(value: Json) -> Dynamic { match value { Json::Null => Dynamic::UNIT, Json::Bool(b) => b.into(), Json::Number(n) => { if let Some(i) = n.as_i64() { i.into() } else if let Some(f) = n.as_f64() { f.into() } else { n.to_string().into() } } Json::String(s) => s.into(), Json::Array(arr) => arr .into_iter() .map(json_to_dynamic) .collect::>() .into(), Json::Object(obj) => { let mut m = Map::new(); for (k, v) in obj { m.insert(k.into(), json_to_dynamic(v)); } Dynamic::from(m) } } } /// Convert a Rhai `Dynamic` back to a `serde_json::Value`. Custom Rhai /// types (timestamps, user-registered modules) fall back to their /// `Display` form so they appear as strings in JSON output rather than /// failing the response build. pub fn dynamic_to_json(value: &Dynamic) -> Json { if value.is_unit() { return Json::Null; } if let Ok(b) = value.as_bool() { return Json::Bool(b); } if let Ok(i) = value.as_int() { return Json::Number(i.into()); } if let Ok(f) = value.as_float() { return serde_json::Number::from_f64(f).map_or(Json::Null, Json::Number); } if value.is_string() { return Json::String(value.clone().into_string().unwrap_or_default()); } if let Some(arr) = value.clone().try_cast::() { return Json::Array(arr.iter().map(dynamic_to_json).collect()); } if let Some(map) = value.clone().try_cast::() { let mut out = serde_json::Map::new(); for (k, v) in map { out.insert(k.to_string(), dynamic_to_json(&v)); } return Json::Object(out); } Json::String(value.to_string()) }