Skip to main content

ldp_protocol/types/
error.rs

1//! LDP typed failure codes.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Category of failure.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum FailureCategory {
10    Identity,
11    Capability,
12    Policy,
13    Runtime,
14    Quality,
15    Session,
16    Transport,
17}
18
19/// Severity level of a failure.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ErrorSeverity {
23    Warning,
24    Error,
25    Fatal,
26}
27
28/// Structured LDP error with category, severity, and retry information.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct LdpError {
31    pub code: String,
32    pub category: FailureCategory,
33    pub message: String,
34    pub severity: ErrorSeverity,
35    pub retryable: bool,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub partial_output: Option<Value>,
38}
39
40impl LdpError {
41    pub fn identity(code: impl Into<String>, message: impl Into<String>) -> Self {
42        Self {
43            code: code.into(),
44            category: FailureCategory::Identity,
45            message: message.into(),
46            severity: ErrorSeverity::Error,
47            retryable: false,
48            partial_output: None,
49        }
50    }
51
52    pub fn capability(code: impl Into<String>, message: impl Into<String>) -> Self {
53        Self {
54            code: code.into(),
55            category: FailureCategory::Capability,
56            message: message.into(),
57            severity: ErrorSeverity::Error,
58            retryable: false,
59            partial_output: None,
60        }
61    }
62
63    pub fn policy(code: impl Into<String>, message: impl Into<String>) -> Self {
64        Self {
65            code: code.into(),
66            category: FailureCategory::Policy,
67            message: message.into(),
68            severity: ErrorSeverity::Fatal,
69            retryable: false,
70            partial_output: None,
71        }
72    }
73
74    pub fn runtime(code: impl Into<String>, message: impl Into<String>) -> Self {
75        Self {
76            code: code.into(),
77            category: FailureCategory::Runtime,
78            message: message.into(),
79            severity: ErrorSeverity::Error,
80            retryable: true,
81            partial_output: None,
82        }
83    }
84
85    pub fn quality(code: impl Into<String>, message: impl Into<String>) -> Self {
86        Self {
87            code: code.into(),
88            category: FailureCategory::Quality,
89            message: message.into(),
90            severity: ErrorSeverity::Warning,
91            retryable: false,
92            partial_output: None,
93        }
94    }
95
96    pub fn session(code: impl Into<String>, message: impl Into<String>) -> Self {
97        Self {
98            code: code.into(),
99            category: FailureCategory::Session,
100            message: message.into(),
101            severity: ErrorSeverity::Error,
102            retryable: true,
103            partial_output: None,
104        }
105    }
106
107    pub fn transport(code: impl Into<String>, message: impl Into<String>) -> Self {
108        Self {
109            code: code.into(),
110            category: FailureCategory::Transport,
111            message: message.into(),
112            severity: ErrorSeverity::Warning,
113            retryable: true,
114            partial_output: None,
115        }
116    }
117
118    pub fn with_partial_output(mut self, output: Value) -> Self {
119        self.partial_output = Some(output);
120        self
121    }
122}
123
124impl std::fmt::Display for LdpError {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "[{:?}] {}: {}", self.category, self.code, self.message)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn identity_failure() {
136        let err = LdpError::identity("IDENTITY_MISMATCH", "Trust domain mismatch");
137        assert_eq!(err.category, FailureCategory::Identity);
138        assert!(!err.retryable);
139    }
140
141    #[test]
142    fn runtime_failure_retryable() {
143        let err = LdpError::runtime("TIMEOUT", "Request timed out");
144        assert!(err.retryable);
145    }
146
147    #[test]
148    fn error_with_partial_output() {
149        let partial = serde_json::json!({"partial": "data"});
150        let err = LdpError::runtime("TIMEOUT", "Timed out").with_partial_output(partial.clone());
151        assert_eq!(err.partial_output, Some(partial));
152    }
153
154    #[test]
155    fn serialization_roundtrip() {
156        let err = LdpError::capability("SKILL_NOT_FOUND", "No such skill");
157        let json = serde_json::to_value(&err).unwrap();
158        let restored: LdpError = serde_json::from_value(json).unwrap();
159        assert_eq!(restored.code, "SKILL_NOT_FOUND");
160    }
161
162    #[test]
163    fn policy_is_fatal() {
164        let err = LdpError::policy("TRUST_VIOLATION", "Not allowed");
165        assert_eq!(err.severity, ErrorSeverity::Fatal);
166        assert!(!err.retryable);
167    }
168
169    #[test]
170    fn quality_constructor_exists() {
171        let err = LdpError::quality("BELOW_THRESHOLD", "Score too low");
172        assert_eq!(err.category, FailureCategory::Quality);
173    }
174}