1use std::error::Error as StdError;
4use std::fmt;
5
6use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticPathSegment};
7use type_bridge_orm::match_request::{MatchError, MatchErrorCategory};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum ErrorCategory {
13 Connection,
15 Schema,
17 ModelValidation,
19 QueryAuthoring,
21 QueryExecution,
23 Transaction,
25 Remote,
27 Capability,
29 ResourceLimit,
31 NotFound,
33 Database,
35 Other,
37}
38
39impl ErrorCategory {
40 #[must_use]
42 pub const fn as_str(self) -> &'static str {
43 match self {
44 Self::Connection => "connection",
45 Self::Schema => "schema",
46 Self::ModelValidation => "model_validation",
47 Self::QueryAuthoring => "query_authoring",
48 Self::QueryExecution => "query_execution",
49 Self::Transaction => "transaction",
50 Self::Remote => "remote",
51 Self::Capability => "capability",
52 Self::ResourceLimit => "resource_limit",
53 Self::NotFound => "not_found",
54 Self::Database => "database",
55 Self::Other => "other",
56 }
57 }
58}
59
60impl fmt::Display for ErrorCategory {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str(self.as_str())
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum ModelValidationPhase {
69 Input,
71 Hydration,
73}
74
75#[derive(Debug, thiserror::Error)]
77#[non_exhaustive]
78pub enum Error {
79 #[error("Model validation failed during {phase:?}: {message}")]
81 ModelValidation {
82 phase: ModelValidationPhase,
83 code: String,
84 path: Vec<String>,
85 message: String,
86 #[source]
87 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
88 },
89
90 #[error("{category} error [{code}]: {message}")]
93 Classified {
94 category: ErrorCategory,
95 phase: Option<ModelValidationPhase>,
96 code: String,
97 path: Vec<String>,
98 message: String,
99 #[source]
100 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
101 },
102
103 #[error("Schema verification failed: {message}")]
105 SchemaVerification {
106 message: String,
107 #[source]
108 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
109 },
110
111 #[error("Connection error: {message}")]
113 Connection {
114 message: String,
115 #[source]
116 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
117 },
118
119 #[error("Query execution error: {message}")]
121 QueryExecution {
122 message: String,
123 #[source]
124 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
125 },
126
127 #[error("Transaction error: {message}")]
129 Transaction {
130 message: String,
131 #[source]
132 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
133 },
134
135 #[error("Entity not found: {message}")]
137 NotFound {
138 message: String,
139 #[source]
140 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
141 },
142
143 #[error("Database error: {message}")]
145 Database {
146 message: String,
147 #[source]
148 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
149 },
150
151 #[error("Client error: {message}")]
153 Other {
154 message: String,
155 #[source]
156 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
157 },
158}
159
160impl Error {
161 #[allow(dead_code)]
162 pub(crate) fn model_validation(
163 phase: ModelValidationPhase,
164 code: impl Into<String>,
165 path: Vec<String>,
166 message: impl Into<String>,
167 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
168 ) -> Self {
169 Self::ModelValidation {
170 phase,
171 code: code.into(),
172 path,
173 message: message.into(),
174 source,
175 }
176 }
177
178 pub(crate) fn classified(
179 category: ErrorCategory,
180 phase: Option<ModelValidationPhase>,
181 code: impl Into<String>,
182 path: Vec<String>,
183 message: impl Into<String>,
184 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
185 ) -> Self {
186 Self::Classified {
187 category,
188 phase,
189 code: code.into(),
190 path,
191 message: message.into(),
192 source,
193 }
194 }
195
196 #[must_use]
201 pub fn remote(
202 code: impl Into<String>,
203 message: impl Into<String>,
204 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
205 ) -> Self {
206 Self::classified(
207 ErrorCategory::Remote,
208 None,
209 code,
210 Vec::new(),
211 message,
212 source,
213 )
214 }
215
216 pub(crate) fn from_match(error: MatchError, phase: ModelValidationPhase) -> Self {
217 let category = match error.category() {
218 MatchErrorCategory::InvalidPlan => ErrorCategory::QueryAuthoring,
219 MatchErrorCategory::Cardinality | MatchErrorCategory::ResultDecode => {
220 ErrorCategory::ModelValidation
221 }
222 MatchErrorCategory::UnsupportedCapability => ErrorCategory::Capability,
223 MatchErrorCategory::StaleSchema => ErrorCategory::Schema,
224 MatchErrorCategory::ResourceLimit => ErrorCategory::ResourceLimit,
225 MatchErrorCategory::Provider => ErrorCategory::QueryExecution,
226 };
227 let model_phase = (category == ErrorCategory::ModelValidation).then_some(phase);
228 let code = error.code().as_str().to_owned();
229 let path = error
230 .path()
231 .segments()
232 .iter()
233 .map(ToString::to_string)
234 .collect();
235 let message = error.message().to_owned();
236 Self::classified(
237 category,
238 model_phase,
239 code,
240 path,
241 message,
242 Some(Box::new(error)),
243 )
244 }
245
246 pub(crate) fn from_remote_diagnostic(error: Diagnostic) -> Self {
247 let category = match error.category() {
248 DiagnosticCategory::UnsupportedCapability => ErrorCategory::Capability,
249 DiagnosticCategory::ResourceLimit => ErrorCategory::ResourceLimit,
250 DiagnosticCategory::InvalidContract | DiagnosticCategory::Integrity => {
251 ErrorCategory::Remote
252 }
253 };
254 let code = error.code().as_str().to_owned();
255 let path = error
256 .path()
257 .segments()
258 .iter()
259 .map(|segment| match segment {
260 DiagnosticPathSegment::Field(value) => value.clone(),
261 DiagnosticPathSegment::Index(value) => format!("[{value}]"),
262 DiagnosticPathSegment::Identifier(value) => value.clone(),
263 })
264 .collect();
265 let message = error.message().to_owned();
266 Self::classified(category, None, code, path, message, Some(Box::new(error)))
267 }
268
269 #[allow(dead_code)]
270 pub(crate) fn from_orm(err: type_bridge_orm::OrmError) -> Self {
271 match err {
272 type_bridge_orm::OrmError::Match(error) => {
273 Self::from_match(error, ModelValidationPhase::Input)
274 }
275 error @ type_bridge_orm::OrmError::Connection(_) => Self::Connection {
276 message: error.to_string(),
277 source: Some(Box::new(error)),
278 },
279 error @ type_bridge_orm::OrmError::QueryExecution(_) => Self::QueryExecution {
280 message: error.to_string(),
281 source: Some(Box::new(error)),
282 },
283 error @ type_bridge_orm::OrmError::Transaction(_) => Self::Transaction {
284 message: error.to_string(),
285 source: Some(Box::new(error)),
286 },
287 error @ type_bridge_orm::OrmError::NotFound(_) => Self::NotFound {
288 message: error.to_string(),
289 source: Some(Box::new(error)),
290 },
291 error @ type_bridge_orm::OrmError::Hydration { .. } => Self::ModelValidation {
292 phase: ModelValidationPhase::Hydration,
293 code: "invalid_provider_evidence".into(),
294 path: vec![],
295 message: error.to_string(),
296 source: Some(Box::new(error)),
297 },
298 error => Self::Database {
299 message: error.to_string(),
300 source: Some(Box::new(error)),
301 },
302 }
303 }
304
305 pub(crate) fn from_orm_hydration(err: type_bridge_orm::OrmError) -> Self {
306 match err {
307 type_bridge_orm::OrmError::Match(error) => {
308 Self::from_match(error, ModelValidationPhase::Hydration)
309 }
310 error => Self::from_orm(error),
311 }
312 }
313
314 #[must_use]
316 pub const fn category(&self) -> ErrorCategory {
317 match self {
318 Self::ModelValidation { .. } => ErrorCategory::ModelValidation,
319 Self::Classified { category, .. } => *category,
320 Self::SchemaVerification { .. } => ErrorCategory::Schema,
321 Self::Connection { .. } => ErrorCategory::Connection,
322 Self::QueryExecution { .. } => ErrorCategory::QueryExecution,
323 Self::Transaction { .. } => ErrorCategory::Transaction,
324 Self::NotFound { .. } => ErrorCategory::NotFound,
325 Self::Database { .. } => ErrorCategory::Database,
326 Self::Other { .. } => ErrorCategory::Other,
327 }
328 }
329
330 #[must_use]
332 pub fn message(&self) -> &str {
333 match self {
334 Self::ModelValidation { message, .. }
335 | Self::Classified { message, .. }
336 | Self::SchemaVerification { message, .. }
337 | Self::Connection { message, .. }
338 | Self::QueryExecution { message, .. }
339 | Self::Transaction { message, .. }
340 | Self::NotFound { message, .. }
341 | Self::Database { message, .. }
342 | Self::Other { message, .. } => message,
343 }
344 }
345
346 #[must_use]
348 pub fn code(&self) -> Option<&str> {
349 match self {
350 Self::ModelValidation { code, .. } | Self::Classified { code, .. } => Some(code),
351 _ => None,
352 }
353 }
354
355 #[must_use]
357 pub fn path(&self) -> Option<&[String]> {
358 match self {
359 Self::ModelValidation { path, .. } | Self::Classified { path, .. } => Some(path),
360 _ => None,
361 }
362 }
363
364 #[must_use]
366 pub const fn model_validation_phase(&self) -> Option<ModelValidationPhase> {
367 match self {
368 Self::ModelValidation { phase, .. } => Some(*phase),
369 Self::Classified { phase, .. } => *phase,
370 _ => None,
371 }
372 }
373}
374
375pub type Result<T, E = Error> = std::result::Result<T, E>;
377
378#[cfg(test)]
379mod tests {
380 use super::{Error, ErrorCategory};
381
382 #[test]
383 fn public_error_categories_and_remote_constructor_are_stable() {
384 let categories = [
385 (ErrorCategory::Connection, "connection"),
386 (ErrorCategory::Schema, "schema"),
387 (ErrorCategory::ModelValidation, "model_validation"),
388 (ErrorCategory::QueryAuthoring, "query_authoring"),
389 (ErrorCategory::QueryExecution, "query_execution"),
390 (ErrorCategory::Transaction, "transaction"),
391 (ErrorCategory::Remote, "remote"),
392 (ErrorCategory::Capability, "capability"),
393 (ErrorCategory::ResourceLimit, "resource_limit"),
394 (ErrorCategory::NotFound, "not_found"),
395 (ErrorCategory::Database, "database"),
396 (ErrorCategory::Other, "other"),
397 ];
398 for (category, spelling) in categories {
399 assert_eq!(category.as_str(), spelling);
400 assert_eq!(category.to_string(), spelling);
401 }
402
403 let error = Error::remote("remote_transport", "connection reset", None);
404 assert_eq!(error.category(), ErrorCategory::Remote);
405 assert_eq!(error.code(), Some("remote_transport"));
406 assert_eq!(error.path(), Some(&[][..]));
407 assert_eq!(error.message(), "connection reset");
408 }
409}