spikard_core/
errors.rs

1//! Shared structured error types and panic shielding utilities.
2//!
3//! Bindings should convert all fatal paths into this shape to keep cross-language
4//! error payloads consistent and avoid panics crossing FFI boundaries.
5
6use serde::Serialize;
7use serde_json::Value;
8use std::panic::{UnwindSafe, catch_unwind};
9
10/// Canonical error payload: { error, code, details }.
11#[derive(Debug, Clone, Serialize)]
12pub struct StructuredError {
13    pub error: String,
14    pub code: String,
15    #[serde(default)]
16    pub details: Value,
17}
18
19impl StructuredError {
20    pub fn new(code: impl Into<String>, error: impl Into<String>, details: Value) -> Self {
21        Self {
22            code: code.into(),
23            error: error.into(),
24            details,
25        }
26    }
27
28    pub fn simple(code: impl Into<String>, error: impl Into<String>) -> Self {
29        Self::new(code, error, Value::Object(serde_json::Map::new()))
30    }
31}
32
33/// Catch panics and convert to a structured error so they don't cross FFI boundaries.
34pub fn shield_panic<T, F>(f: F) -> Result<T, StructuredError>
35where
36    F: FnOnce() -> T + UnwindSafe,
37{
38    catch_unwind(f).map_err(|_| StructuredError::simple("panic", "Unexpected panic in Rust code"))
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use serde_json::json;
45
46    #[test]
47    fn structured_error_constructors_populate_fields() {
48        let details = json!({"field": "name"});
49        let err = StructuredError::new("invalid", "bad input", details.clone());
50        assert_eq!(err.code, "invalid");
51        assert_eq!(err.error, "bad input");
52        assert_eq!(err.details, details);
53
54        let simple = StructuredError::simple("missing", "not found");
55        assert_eq!(simple.code, "missing");
56        assert_eq!(simple.error, "not found");
57        assert!(simple.details.is_object());
58    }
59
60    #[test]
61    fn shield_panic_returns_ok_or_structured_error() {
62        let ok = shield_panic(|| 42);
63        assert_eq!(ok.unwrap(), 42);
64
65        let err = shield_panic(|| panic!("boom")).unwrap_err();
66        assert_eq!(err.code, "panic");
67        assert!(err.error.contains("Unexpected panic"));
68    }
69}