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