Skip to main content

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.
34///
35/// # Errors
36/// Returns a structured error if a panic occurs during function execution.
37pub fn shield_panic<T, F>(f: F) -> Result<T, StructuredError>
38where
39    F: FnOnce() -> T + UnwindSafe,
40{
41    catch_unwind(f).map_err(|_| StructuredError::simple("panic", "Unexpected panic in Rust code"))
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use serde_json::json;
48
49    #[test]
50    fn structured_error_constructors_populate_fields() {
51        let details = json!({"field": "name"});
52        let err = StructuredError::new("invalid", "bad input", details.clone());
53        assert_eq!(err.code, "invalid");
54        assert_eq!(err.error, "bad input");
55        assert_eq!(err.details, details);
56
57        let simple = StructuredError::simple("missing", "not found");
58        assert_eq!(simple.code, "missing");
59        assert_eq!(simple.error, "not found");
60        assert!(simple.details.is_object());
61    }
62
63    #[test]
64    fn shield_panic_returns_ok_or_structured_error() {
65        let ok = shield_panic(|| 42);
66        assert_eq!(ok.unwrap(), 42);
67
68        let err = shield_panic(|| panic!("boom")).unwrap_err();
69        assert_eq!(err.code, "panic");
70        assert!(err.error.contains("Unexpected panic"));
71    }
72}