//! Runtime helpers — error envelope, request handle, invalidation/merge //! resolution. Ports `compute_invalidation` / `compute_merges` / //! `_resolve_merge_slot` / `_scoped_params` from //! `backends/mizan-fastapi/src/mizan_fastapi/executor.py:189-263`. use crate::registry::context_members; use crate::traits::FunctionSpec; use serde_json::Value; use std::any::Any; /// Type-erased handle to the framework's request object. The HTTP adapter /// stuffs its native `Request` here; user code casts back via the adapter's /// helper types. #[derive(Clone)] pub struct RequestHandle<'a> { pub inner: &'a (dyn Any + Send + Sync), } impl<'a> RequestHandle<'a> { /// Wrap a typed reference. The most common path — handlers downcast back /// to `T` via `downcast::()`. pub fn new(req: &'a T) -> Self { Self { inner: req } } /// Wrap an already-erased `dyn Any` reference. Used by HTTP adapters /// that thread an `Arc` app state in. pub fn from_dyn(req: &'a (dyn Any + Send + Sync)) -> Self { Self { inner: req } } pub fn downcast(&self) -> Option<&'a T> { self.inner.downcast_ref::() } } /// Mizan's standard error envelope. Mirrors FastAPI's MizanError enum. #[derive(Debug, Clone)] pub enum MizanError { NotFound(String), BadRequest(String), ValidationFailed { message: String, details: Value, }, Unauthorized(String), Forbidden(String), NotImplementedYet(String), InternalError(String), } impl MizanError { pub fn code(&self) -> &'static str { match self { MizanError::NotFound(_) => "NOT_FOUND", MizanError::BadRequest(_) => "BAD_REQUEST", MizanError::ValidationFailed { .. } => "VALIDATION_FAILED", MizanError::Unauthorized(_) => "UNAUTHORIZED", MizanError::Forbidden(_) => "FORBIDDEN", MizanError::NotImplementedYet(_) => "NOT_IMPLEMENTED", MizanError::InternalError(_) => "INTERNAL_ERROR", } } pub fn message(&self) -> &str { match self { MizanError::NotFound(m) | MizanError::BadRequest(m) | MizanError::Unauthorized(m) | MizanError::Forbidden(m) | MizanError::NotImplementedYet(m) | MizanError::InternalError(m) => m, MizanError::ValidationFailed { message, .. } => message, } } pub fn http_status(&self) -> u16 { match self { MizanError::NotFound(_) => 404, MizanError::BadRequest(_) => 400, MizanError::ValidationFailed { .. } => 422, MizanError::Unauthorized(_) => 401, MizanError::Forbidden(_) => 403, MizanError::NotImplementedYet(_) => 501, MizanError::InternalError(_) => 500, } } /// JSON envelope shape consumers see on the wire. pub fn to_json(&self) -> Value { let mut body = serde_json::Map::new(); body.insert("code".into(), Value::String(self.code().into())); body.insert("message".into(), Value::String(self.message().into())); if let MizanError::ValidationFailed { details, .. } = self { body.insert("details".into(), details.clone()); } Value::Object({ let mut env = serde_json::Map::new(); env.insert("error".into(), Value::Object(body)); env }) } } /// One entry in the response's `invalidate` array. #[derive(Debug, Clone)] pub enum InvalidationTarget { /// A whole context is invalidated. Context(String), /// A context, scoped to specific param values. ScopedContext { context: String, params: serde_json::Map, }, /// A specific function output is invalidated. Function(String), } impl InvalidationTarget { pub fn to_json(&self) -> Value { match self { InvalidationTarget::Context(name) => Value::String(name.clone()), InvalidationTarget::ScopedContext { context, params } => { let mut m = serde_json::Map::new(); m.insert("context".into(), Value::String(context.clone())); m.insert("params".into(), Value::Object(params.clone())); Value::Object(m) } InvalidationTarget::Function(name) => { let mut m = serde_json::Map::new(); m.insert("function".into(), Value::String(name.clone())); Value::Object(m) } } } } /// One entry in the response's `merge` array. Server-resolved slot — the /// kernel writes the value into `bundle[slot]` directly. #[derive(Debug, Clone)] pub struct MergeEntry { pub context: String, pub slot: String, pub value: Value, pub params: Option>, } impl MergeEntry { pub fn to_json(&self) -> Value { let mut m = serde_json::Map::new(); m.insert("context".into(), Value::String(self.context.clone())); m.insert("slot".into(), Value::String(self.slot.clone())); m.insert("value".into(), self.value.clone()); if let Some(params) = &self.params { m.insert("params".into(), Value::Object(params.clone())); } Value::Object(m) } } /// Build the `invalidate` list from a function's `affects` metadata, /// auto-scoping when arg names match context params. pub fn compute_invalidation( fn_spec: &dyn FunctionSpec, args: &serde_json::Map, ) -> Vec { fn_spec .affects() .iter() .map(|target| match target { crate::ir::AffectTarget::Context(name) => { let scoped = scoped_params(name, args); if scoped.is_empty() { InvalidationTarget::Context((*name).into()) } else { InvalidationTarget::ScopedContext { context: (*name).into(), params: scoped, } } } crate::ir::AffectTarget::Function { name, .. } => { InvalidationTarget::Function((*name).into()) } }) .collect() } /// Build the `merge` list from a function's `merge` metadata. Each entry /// names the slot inside the context bundle the return value lands in. pub fn compute_merges( fn_spec: &dyn FunctionSpec, args: &serde_json::Map, result: &Value, ) -> Vec { let targets = fn_spec.merge(); if targets.is_empty() { return Vec::new(); } let mutation_output = fn_spec.output_type(); let mut out = Vec::new(); for ctx_name in targets { let slot = match resolve_merge_slot(ctx_name, mutation_output) { Some(s) => s, None => continue, }; let scoped = scoped_params(ctx_name, args); out.push(MergeEntry { context: (*ctx_name).into(), slot, value: result.clone(), params: if scoped.is_empty() { None } else { Some(scoped) }, }); } out } /// Find the unique function-name slot whose Output type matches the /// mutation's Output type. Matches Python's `types_match_for_merge` — /// structural shape comparison, not name comparison. Returns None on no /// match or ambiguous match. fn resolve_merge_slot(context_name: &str, mutation_output: &str) -> Option { let mutation_shape = crate::graph_check::resolve_type_shape(mutation_output)?; let mut matches: Vec<&'static str> = Vec::new(); for fn_spec in context_members(context_name) { if let Some(candidate_shape) = crate::graph_check::resolve_type_shape(fn_spec.output_type()) { if crate::graph_check::types_match(&candidate_shape, &mutation_shape) { matches.push(fn_spec.name()); } } } if matches.len() == 1 { Some(matches[0].into()) } else { None } } /// Match input args against the context's declared Input field names. fn scoped_params( context_name: &str, args: &serde_json::Map, ) -> serde_json::Map { let mut declared: std::collections::HashSet<&'static str> = std::collections::HashSet::new(); for fn_spec in context_members(context_name) { for p in fn_spec.input_params() { declared.insert(p.name); } } args.iter() .filter(|(k, _)| declared.contains(k.as_str())) .map(|(k, v)| (k.clone(), v.clone())) .collect() }