refactor(executor-core): extract sdk/ module + move json↔dynamic bridge
Hoist the json_to_dynamic / dynamic_to_json helpers out of engine.rs
into a new sdk/bridge.rs so the v1.1.x service modules (KV, docs, …)
can use them without engine.rs being the sole owner. No behavioural
change — the sdk_contract round-trip test pins the observable JSON
fidelity.
Also lands the structural shape that subsequent v1.1.x PRs hook into:
- sdk::register_all(engine, services, cx) — single per-call hook
every stateful service registers through. Body is a no-op for
v1.1.0; SdkCallCx construction inside Engine::execute lands in
the next commit alongside the new ExecRequest fields it reads.
- sdk::cx re-exports picloud_shared::SdkCallCx so SDK callers don't
cross-import shared for one type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use rhai::{Dynamic, Engine as RhaiEngine, EvalAltResult, Map, Module, Scope};
|
|||||||
use serde_json::Value as Json;
|
use serde_json::Value as Json;
|
||||||
|
|
||||||
use crate::sandbox::Limits;
|
use crate::sandbox::Limits;
|
||||||
|
use crate::sdk::bridge::{dynamic_to_json, json_to_dynamic};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ExecError, ExecRequest, ExecResponse, ExecStats, InvocationType, LogEntry, LogLevel,
|
ExecError, ExecRequest, ExecResponse, ExecStats, InvocationType, LogEntry, LogLevel,
|
||||||
};
|
};
|
||||||
@@ -265,69 +266,6 @@ fn parse_structured_response(map: Map) -> Result<(u16, BTreeMap<String, String>,
|
|||||||
Ok((status_code, headers, body))
|
Ok((status_code, headers, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Rhai ↔ serde_json bridges
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// Anything else (timestamps, custom types) — best-effort string form.
|
|
||||||
Json::String(value.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Error mapping
|
// Error mapping
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod context;
|
|||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod sandbox;
|
pub mod sandbox;
|
||||||
|
pub mod sdk;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub use engine::Engine;
|
pub use engine::Engine;
|
||||||
|
|||||||
79
crates/executor-core/src/sdk/bridge.rs
Normal file
79
crates/executor-core/src/sdk/bridge.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
//! 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, Map};
|
||||||
|
use serde_json::Value as Json;
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[must_use]
|
||||||
|
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.
|
||||||
|
#[must_use]
|
||||||
|
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())
|
||||||
|
}
|
||||||
10
crates/executor-core/src/sdk/cx.rs
Normal file
10
crates/executor-core/src/sdk/cx.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! Re-export of `picloud_shared::SdkCallCx`.
|
||||||
|
//!
|
||||||
|
//! The type itself lives in `picloud-shared` because future stateful
|
||||||
|
//! service impls live in `manager-core` (which `executor-core` must
|
||||||
|
//! not depend on) and need to reference the same cx shape. This
|
||||||
|
//! re-export lets executor-side code write
|
||||||
|
//! `use picloud_executor_core::sdk::SdkCallCx;` instead of reaching
|
||||||
|
//! into `picloud_shared` for one type.
|
||||||
|
|
||||||
|
pub use picloud_shared::SdkCallCx;
|
||||||
39
crates/executor-core/src/sdk/mod.rs
Normal file
39
crates/executor-core/src/sdk/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//! SDK plumbing — types and the per-call registration entry point.
|
||||||
|
//!
|
||||||
|
//! `executor-core` is responsible for building the per-invocation Rhai
|
||||||
|
//! engine and wiring stateful services into it. v1.1.0 ships the
|
||||||
|
//! shapes (`Services` bundle, `SdkCallCx`, `register_all` entry point)
|
||||||
|
//! but no actual services — subsequent v1.1.x PRs (KV in v1.1.1,
|
||||||
|
//! docs in v1.1.2, …) extend `register_all` rather than re-threading
|
||||||
|
//! plumbing through `engine.rs`.
|
||||||
|
//!
|
||||||
|
//! Bridge functions (`json_to_dynamic` / `dynamic_to_json`) also live
|
||||||
|
//! here so service modules can convert values without `engine.rs`
|
||||||
|
//! being the only home for the conversion logic.
|
||||||
|
|
||||||
|
pub mod bridge;
|
||||||
|
pub mod cx;
|
||||||
|
|
||||||
|
pub use bridge::{dynamic_to_json, json_to_dynamic};
|
||||||
|
pub use cx::SdkCallCx;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use picloud_shared::Services;
|
||||||
|
use rhai::Engine as RhaiEngine;
|
||||||
|
|
||||||
|
/// Single hook every v1.1.x stateful service registers into. Called
|
||||||
|
/// once per invocation, just after `build_engine` constructs the
|
||||||
|
/// sandboxed Rhai engine and just before script compilation.
|
||||||
|
///
|
||||||
|
/// v1.1.0 ships an intentionally empty body — the call site exists so
|
||||||
|
/// future PRs (KV first) drop their registration logic here rather
|
||||||
|
/// than reaching into `engine.rs::build_engine`. The signature is
|
||||||
|
/// locked: subsequent PRs MUST keep the same parameter shape so that
|
||||||
|
/// hosts don't have to re-thread the plumbing.
|
||||||
|
pub fn register_all(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
|
||||||
|
// Intentionally inert in v1.1.0. The unused-suppression below is a
|
||||||
|
// load-bearing placeholder: future PRs replace this `let _` with
|
||||||
|
// real `register_kv(engine, services, cx.clone())` calls etc.
|
||||||
|
let _ = (engine, services, cx);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user