1use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ErrorCode {
27 InvalidPdf,
29 CorruptPdf,
31 PasswordProtected,
33 PageLimitExceeded,
35 FileTooLarge,
37 OcrRequired,
39 UnsupportedPdfFeature,
41 ParseTimeout,
43 MemoryLimitExceeded,
45 InternalError,
47}
48
49impl ErrorCode {
50 pub const ALL: [ErrorCode; 10] = [
52 ErrorCode::InvalidPdf,
53 ErrorCode::CorruptPdf,
54 ErrorCode::PasswordProtected,
55 ErrorCode::PageLimitExceeded,
56 ErrorCode::FileTooLarge,
57 ErrorCode::OcrRequired,
58 ErrorCode::UnsupportedPdfFeature,
59 ErrorCode::ParseTimeout,
60 ErrorCode::MemoryLimitExceeded,
61 ErrorCode::InternalError,
62 ];
63
64 pub fn as_str(self) -> &'static str {
66 match self {
67 ErrorCode::InvalidPdf => "invalid_pdf",
68 ErrorCode::CorruptPdf => "corrupt_pdf",
69 ErrorCode::PasswordProtected => "password_protected",
70 ErrorCode::PageLimitExceeded => "page_limit_exceeded",
71 ErrorCode::FileTooLarge => "file_too_large",
72 ErrorCode::OcrRequired => "ocr_required",
73 ErrorCode::UnsupportedPdfFeature => "unsupported_pdf_feature",
74 ErrorCode::ParseTimeout => "parse_timeout",
75 ErrorCode::MemoryLimitExceeded => "memory_limit_exceeded",
76 ErrorCode::InternalError => "internal_error",
77 }
78 }
79
80 pub fn exit_code(self) -> i32 {
82 match self {
83 ErrorCode::InvalidPdf => 3,
84 ErrorCode::CorruptPdf => 4,
85 ErrorCode::PasswordProtected => 5,
86 ErrorCode::PageLimitExceeded => 6,
87 ErrorCode::FileTooLarge => 7,
88 ErrorCode::OcrRequired => 8,
89 ErrorCode::UnsupportedPdfFeature => 9,
90 ErrorCode::ParseTimeout => 10,
91 ErrorCode::MemoryLimitExceeded => 11,
92 ErrorCode::InternalError => 12,
93 }
94 }
95}
96
97impl core::fmt::Display for ErrorCode {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 f.write_str(self.as_str())
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
106#[error("{code}: {message}")]
107pub struct EthosError {
108 pub code: ErrorCode,
110 pub message: String,
112}
113
114impl EthosError {
115 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
117 EthosError {
118 code,
119 message: message.into(),
120 }
121 }
122
123 pub fn internal(message: impl Into<String>) -> Self {
125 EthosError::new(ErrorCode::InternalError, message)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn exit_codes_are_dense_and_disjoint() {
135 let mut seen = std::collections::BTreeSet::new();
136 for code in ErrorCode::ALL {
137 assert!(seen.insert(code.exit_code()), "duplicate exit code");
138 }
139 assert_eq!(*seen.first().unwrap(), 3);
140 assert_eq!(*seen.last().unwrap(), 12);
141 assert_eq!(seen.len(), 10);
142 }
143
144 #[test]
145 fn wire_format_round_trips() {
146 for code in ErrorCode::ALL {
147 let json = serde_json::to_string(&code).unwrap();
148 assert_eq!(json, format!("\"{}\"", code.as_str()));
149 assert_eq!(serde_json::from_str::<ErrorCode>(&json).unwrap(), code);
150 }
151 }
152}