1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, thiserror::Error)]
10pub enum VoceError {
11 #[error("[{code}] Schema error: {message}")]
13 Schema {
14 code: ErrorCode,
15 message: String,
16 suggestion: String,
17 },
18
19 #[error("[{code}] Validation error at {node_path}: {message}")]
21 Validation {
22 code: ErrorCode,
23 message: String,
24 node_path: String,
25 suggestion: String,
26 severity: ErrorSeverity,
27 },
28
29 #[error("[{code}] Compilation error at {node_path}: {message}")]
31 Compilation {
32 code: ErrorCode,
33 message: String,
34 node_path: String,
35 suggestion: String,
36 },
37
38 #[error("[{code}] Deployment error: {message}")]
40 Deployment {
41 code: ErrorCode,
42 message: String,
43 suggestion: String,
44 },
45
46 #[error("[{code}] Pipeline error: {message}")]
48 Pipeline {
49 code: ErrorCode,
50 message: String,
51 suggestion: String,
52 },
53
54 #[error("[{code}] AI bridge error: {message}")]
56 AiBridge {
57 code: ErrorCode,
58 message: String,
59 suggestion: String,
60 },
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ErrorCode(pub String);
66
67impl std::fmt::Display for ErrorCode {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73impl ErrorCode {
74 pub fn schema_parse() -> Self { Self("S001".to_string()) }
76 pub fn schema_version() -> Self { Self("S002".to_string()) }
77 pub fn schema_missing_root() -> Self { Self("S003".to_string()) }
78
79 pub fn compile_node_failed() -> Self { Self("C001".to_string()) }
81 pub fn compile_timeout() -> Self { Self("C002".to_string()) }
82 pub fn compile_unsupported_node() -> Self { Self("C003".to_string()) }
83 pub fn compile_asset_failed() -> Self { Self("C004".to_string()) }
84
85 pub fn deploy_adapter_not_found() -> Self { Self("D001".to_string()) }
87 pub fn deploy_bundle_failed() -> Self { Self("D002".to_string()) }
88 pub fn deploy_upload_failed() -> Self { Self("D003".to_string()) }
89 pub fn deploy_config_invalid() -> Self { Self("D004".to_string()) }
90
91 pub fn pipeline_timeout() -> Self { Self("P001".to_string()) }
93 pub fn pipeline_interrupted() -> Self { Self("P002".to_string()) }
94
95 pub fn ai_api_error() -> Self { Self("A001".to_string()) }
97 pub fn ai_rate_limited() -> Self { Self("A002".to_string()) }
98 pub fn ai_timeout() -> Self { Self("A003".to_string()) }
99 pub fn ai_incomplete_output() -> Self { Self("A004".to_string()) }
100 pub fn ai_key_invalid() -> Self { Self("A005".to_string()) }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum ErrorSeverity {
106 Error,
108 Warning,
110 Info,
112}
113
114impl std::fmt::Display for ErrorSeverity {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 ErrorSeverity::Error => write!(f, "error"),
118 ErrorSeverity::Warning => write!(f, "warning"),
119 ErrorSeverity::Info => write!(f, "info"),
120 }
121 }
122}
123
124#[derive(Debug, Serialize, Deserialize)]
126pub struct ErrorReport {
127 pub code: String,
128 pub severity: String,
129 pub message: String,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub node_path: Option<String>,
132 pub suggestion: String,
133}
134
135impl From<&VoceError> for ErrorReport {
136 fn from(err: &VoceError) -> Self {
137 match err {
138 VoceError::Schema { code, message, suggestion } => ErrorReport {
139 code: code.0.clone(),
140 severity: "error".to_string(),
141 message: message.clone(),
142 node_path: None,
143 suggestion: suggestion.clone(),
144 },
145 VoceError::Validation { code, message, node_path, suggestion, severity } => ErrorReport {
146 code: code.0.clone(),
147 severity: severity.to_string(),
148 message: message.clone(),
149 node_path: Some(node_path.clone()),
150 suggestion: suggestion.clone(),
151 },
152 VoceError::Compilation { code, message, node_path, suggestion } => ErrorReport {
153 code: code.0.clone(),
154 severity: "error".to_string(),
155 message: message.clone(),
156 node_path: Some(node_path.clone()),
157 suggestion: suggestion.clone(),
158 },
159 VoceError::Deployment { code, message, suggestion } => ErrorReport {
160 code: code.0.clone(),
161 severity: "error".to_string(),
162 message: message.clone(),
163 node_path: None,
164 suggestion: suggestion.clone(),
165 },
166 VoceError::Pipeline { code, message, suggestion } => ErrorReport {
167 code: code.0.clone(),
168 severity: "error".to_string(),
169 message: message.clone(),
170 node_path: None,
171 suggestion: suggestion.clone(),
172 },
173 VoceError::AiBridge { code, message, suggestion } => ErrorReport {
174 code: code.0.clone(),
175 severity: "error".to_string(),
176 message: message.clone(),
177 node_path: None,
178 suggestion: suggestion.clone(),
179 },
180 }
181 }
182}
183
184pub mod exit_codes {
186 pub const SUCCESS: i32 = 0;
187 pub const VALIDATION_ERROR: i32 = 1;
188 pub const COMPILATION_ERROR: i32 = 2;
189 pub const DEPLOYMENT_ERROR: i32 = 3;
190 pub const AI_BRIDGE_ERROR: i32 = 4;
191 pub const INTERNAL_ERROR: i32 = 5;
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn error_code_display() {
200 let code = ErrorCode::schema_parse();
201 assert_eq!(code.to_string(), "S001");
202 }
203
204 #[test]
205 fn voce_error_display_includes_code() {
206 let err = VoceError::Schema {
207 code: ErrorCode::schema_parse(),
208 message: "Invalid JSON".to_string(),
209 suggestion: "Check JSON syntax".to_string(),
210 };
211 let msg = format!("{err}");
212 assert!(msg.contains("S001"));
213 assert!(msg.contains("Invalid JSON"));
214 }
215
216 #[test]
217 fn error_report_from_validation_error() {
218 let err = VoceError::Validation {
219 code: ErrorCode("STR001".to_string()),
220 message: "Missing root".to_string(),
221 node_path: "$.root".to_string(),
222 suggestion: "Add a root ViewRoot node".to_string(),
223 severity: ErrorSeverity::Error,
224 };
225 let report = ErrorReport::from(&err);
226 assert_eq!(report.code, "STR001");
227 assert_eq!(report.severity, "error");
228 assert!(report.node_path.is_some());
229 }
230
231 #[test]
232 fn error_report_serializes_to_json() {
233 let report = ErrorReport {
234 code: "C001".to_string(),
235 severity: "error".to_string(),
236 message: "Node failed to compile".to_string(),
237 node_path: Some("root.children[0]".to_string()),
238 suggestion: "Check node structure".to_string(),
239 };
240 let json = serde_json::to_string(&report).unwrap();
241 assert!(json.contains("C001"));
242 assert!(json.contains("root.children[0]"));
243 }
244
245 #[test]
246 fn exit_codes_are_distinct() {
247 let codes = [
248 exit_codes::SUCCESS,
249 exit_codes::VALIDATION_ERROR,
250 exit_codes::COMPILATION_ERROR,
251 exit_codes::DEPLOYMENT_ERROR,
252 exit_codes::AI_BRIDGE_ERROR,
253 exit_codes::INTERNAL_ERROR,
254 ];
255 let unique: std::collections::HashSet<_> = codes.iter().collect();
256 assert_eq!(unique.len(), codes.len());
257 }
258}