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}
18
19#[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#[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}