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::{Deserialize, Serialize};
7use serde_json::Value;
8use std::panic::{UnwindSafe, catch_unwind};
9
10/// Canonical error payload: { error, code, details }.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct StructuredError {
13    /// Human-readable error message describing what went wrong.
14    pub error: String,
15    /// Machine-readable error code identifying the error category.
16    pub code: String,
17    /// Additional structured context for the error (empty object when absent).
18    #[serde(default)]
19    pub details: Value,
20}
21
22impl StructuredError {
23    pub fn new(code: impl Into<String>, error: impl Into<String>, details: Value) -> Self {
24        Self {
25            code: code.into(),
26            error: error.into(),
27            details,
28        }
29    }
30
31    pub fn simple(code: impl Into<String>, error: impl Into<String>) -> Self {
32        Self::new(code, error, Value::Object(serde_json::Map::new()))
33    }
34}
35
36/// Catch panics and convert to a structured error so they don't cross FFI boundaries.
37///
38/// # Errors
39/// Returns a structured error if a panic occurs during function execution.
40pub fn shield_panic<T, F>(f: F) -> Result<T, StructuredError>
41where
42    F: FnOnce() -> T + UnwindSafe,
43{
44    catch_unwind(f).map_err(|_| StructuredError::simple("panic", "Unexpected panic in Rust code"))
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use serde_json::json;
51
52    #[test]
53    fn structured_error_constructors_populate_fields() {
54        let details = json!({"field": "name"});
55        let err = StructuredError::new("invalid", "bad input", details.clone());
56        assert_eq!(err.code, "invalid");
57        assert_eq!(err.error, "bad input");
58        assert_eq!(err.details, details);
59
60        let simple = StructuredError::simple("missing", "not found");
61        assert_eq!(simple.code, "missing");
62        assert_eq!(simple.error, "not found");
63        assert!(simple.details.is_object());
64    }
65
66    #[test]
67    fn shield_panic_returns_ok_or_structured_error() {
68        let ok = shield_panic(|| 42);
69        assert_eq!(ok.unwrap(), 42);
70
71        let err = shield_panic(|| panic!("boom")).unwrap_err();
72        assert_eq!(err.code, "panic");
73        assert!(err.error.contains("Unexpected panic"));
74    }
75}