Replace ten near-identical `block_on` helpers (one per SDK module) with a single shared `sdk::bridge::block_on(service: &str, fut)`. The helper takes a service-prefix string and a future whose error implements Display, so each module's `KvError`/`DocsError`/etc. still self-formats. Migrated: - kv, docs, pubsub, users, dead_letters, secrets, files, email - queue.rs has two helpers; the inline-shaped enqueue path and the block_on_u64 (u64→i64) wrapper are left in place — the latter could use the shared helper but the local form is one less call site per finding overlap. Will revisit on a later pass if needed. - http.rs is intentionally NOT migrated: it has per-variant error mapping via `map_http_err` that the generic helper can't express. Net: -218 / +97 lines across the 9 migrated files. Single source of truth for the runtime-handle lookup and error wrapping. AUDIT.md anchor: F-Q-002. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
3.9 KiB
Rust
111 lines
3.9 KiB
Rust
//! 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<T, E, F>(service: &str, fut: F) -> Result<T, Box<EvalAltResult>>
|
|
where
|
|
F: std::future::Future<Output = Result<T, E>>,
|
|
E: std::fmt::Display,
|
|
{
|
|
let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
|
|
EvalAltResult::ErrorRuntime(
|
|
format!("{service}: no tokio runtime available: {e}").into(),
|
|
rhai::Position::NONE,
|
|
)
|
|
.into()
|
|
})?;
|
|
handle.block_on(fut).map_err(|err| -> Box<EvalAltResult> {
|
|
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::<Vec<Dynamic>>()
|
|
.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::<rhai::Array>() {
|
|
return Json::Array(arr.iter().map(dynamic_to_json).collect());
|
|
}
|
|
if let Some(map) = value.clone().try_cast::<Map>() {
|
|
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())
|
|
}
|