1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[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#[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#[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}