//! Runtime helpers — error envelope, request handle, and the per-response //! invalidation / merge resolution the adapters call after a dispatch. use crate::registry::context_members; use crate::traits::FunctionSpec; use serde_json::Value; use std::any::Any; /// A borrow of the request object a hosting framework owns. /// /// `FUNCTIONS` is a non-generic `distributed_slice`, so `FunctionSpec` has to /// be object-safe and no type parameter can reach this handle. The reference /// therefore rides erased, and the crate that names the framework's own type /// is the one that casts back to it. #[derive(Clone)] pub struct RequestHandle<'a> { inner: &'a (dyn Any + Send + Sync), } impl<'a> RequestHandle<'a> { /// Wrap a typed reference. pub fn new(req: &'a T) -> Self { Self { inner: req } } /// Wrap a reference the caller has already erased. pub fn from_dyn(req: &'a (dyn Any + Send + Sync)) -> Self { Self { inner: req } } /// The reference the adapter installed. pub fn installed(&self) -> &'a (dyn Any + Send + Sync) { self.inner } } /// Mizan's standard error envelope — the closed set of failures an adapter /// renders onto the wire. #[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 the function's already-resolved merge entries. /// Each 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 { crate::graph_check::merges_for(fn_spec.name()) .map(|resolved| { let scoped = scoped_params(resolved.context, args); MergeEntry { context: resolved.context.into(), slot: resolved.slot.into(), value: result.clone(), params: if scoped.is_empty() { None } else { Some(scoped) }, } }) .collect() } /// 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() } #[cfg(test)] mod tests { use super::RequestHandle; use std::any::Any; fn installed_addr(handle: &RequestHandle<'_>) -> *const () { handle.installed() as *const (dyn Any + Send + Sync) as *const () } #[test] fn a_handle_installs_the_very_reference_it_was_built_over() { let state = String::from("app-state"); let source = &state as *const String as *const (); assert_eq!(installed_addr(&RequestHandle::new(&state)), source); } #[test] fn an_erased_handle_installs_what_a_typed_one_does() { let state = String::from("app-state"); assert_eq!( installed_addr(&RequestHandle::from_dyn(&state)), installed_addr(&RequestHandle::new(&state)) ); } #[test] fn the_installed_reference_keeps_the_type_it_was_built_over() { let state = String::from("app-state"); let handle = RequestHandle::new(&state); assert!(handle.installed().is::()); assert!(!handle.installed().is::()); } }