1use std::collections::BTreeMap;
4use std::error::Error as StdError;
5use std::fmt;
6
7use type_bridge_contract::diagnostic::{
8 Diagnostic, DiagnosticCategory, DiagnosticDetailValue, DiagnosticPathSegment,
9};
10use type_bridge_orm::match_request::{MatchError, MatchErrorCategory};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[non_exhaustive]
15pub enum ErrorCategory {
16 Connection,
18 Schema,
20 ModelValidation,
22 QueryAuthoring,
24 QueryExecution,
26 Transaction,
28 Remote,
30 Capability,
32 ResourceLimit,
34 NotFound,
36 Lifecycle,
38 Database,
40 Other,
42}
43
44impl ErrorCategory {
45 #[must_use]
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::Connection => "connection",
50 Self::Schema => "schema",
51 Self::ModelValidation => "model_validation",
52 Self::QueryAuthoring => "query_authoring",
53 Self::QueryExecution => "query_execution",
54 Self::Transaction => "transaction",
55 Self::Remote => "remote",
56 Self::Capability => "capability",
57 Self::ResourceLimit => "resource_limit",
58 Self::NotFound => "not_found",
59 Self::Lifecycle => "lifecycle",
60 Self::Database => "database",
61 Self::Other => "other",
62 }
63 }
64}
65
66impl fmt::Display for ErrorCategory {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter.write_str(self.as_str())
69 }
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum ModelValidationPhase {
75 Input,
77 Hydration,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83#[non_exhaustive]
84pub enum ErrorDetail {
85 Text(String),
87 Long(i64),
89 Boolean(bool),
91 TextList(Vec<String>),
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97#[non_exhaustive]
98pub enum ErrorPathSegment {
99 Field(String),
101 Index(u64),
103 Identifier(String),
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct ErrorDiagnostic {
111 path: Vec<ErrorPathSegment>,
112 details: BTreeMap<String, ErrorDetail>,
113}
114
115impl ErrorDiagnostic {
116 #[must_use]
118 pub fn path(&self) -> &[ErrorPathSegment] {
119 &self.path
120 }
121
122 #[must_use]
124 pub fn details(&self) -> &BTreeMap<String, ErrorDetail> {
125 &self.details
126 }
127}
128
129#[derive(Debug, thiserror::Error)]
131#[non_exhaustive]
132pub enum Error {
133 #[error("Model validation failed during {phase:?}: {message}")]
135 ModelValidation {
136 phase: ModelValidationPhase,
138 code: String,
140 path: Vec<String>,
142 message: String,
144 #[source]
146 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
147 },
148
149 #[error("{category} error [{code}]: {message}")]
152 Classified {
153 category: ErrorCategory,
155 phase: Option<ModelValidationPhase>,
157 code: String,
159 path: Vec<String>,
161 diagnostic: Option<Box<ErrorDiagnostic>>,
163 message: String,
165 #[source]
167 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
168 },
169
170 #[error("Schema verification failed: {message}")]
172 SchemaVerification {
173 message: String,
175 #[source]
177 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
178 },
179
180 #[error("Connection error: {message}")]
182 Connection {
183 message: String,
185 #[source]
187 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
188 },
189
190 #[error("Query execution error: {message}")]
192 QueryExecution {
193 message: String,
195 #[source]
197 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
198 },
199
200 #[error("Transaction error: {message}")]
202 Transaction {
203 message: String,
205 #[source]
207 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
208 },
209
210 #[error("Entity not found: {message}")]
212 NotFound {
213 message: String,
215 #[source]
217 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
218 },
219
220 #[error("Database error: {message}")]
222 Database {
223 message: String,
225 #[source]
227 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
228 },
229
230 #[error("Client error: {message}")]
232 Other {
233 message: String,
235 #[source]
237 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
238 },
239}
240
241impl Error {
242 #[allow(dead_code)]
243 pub(crate) fn model_validation(
244 phase: ModelValidationPhase,
245 code: impl Into<String>,
246 path: Vec<String>,
247 message: impl Into<String>,
248 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
249 ) -> Self {
250 Self::ModelValidation {
251 phase,
252 code: code.into(),
253 path,
254 message: message.into(),
255 source,
256 }
257 }
258
259 pub(crate) fn classified(
260 category: ErrorCategory,
261 phase: Option<ModelValidationPhase>,
262 code: impl Into<String>,
263 path: Vec<String>,
264 message: impl Into<String>,
265 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
266 ) -> Self {
267 Self::classified_with_diagnostic(category, phase, code, path, None, message, source)
268 }
269
270 fn classified_with_diagnostic(
271 category: ErrorCategory,
272 phase: Option<ModelValidationPhase>,
273 code: impl Into<String>,
274 path: Vec<String>,
275 diagnostic: Option<ErrorDiagnostic>,
276 message: impl Into<String>,
277 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
278 ) -> Self {
279 Self::Classified {
280 category,
281 phase,
282 code: code.into(),
283 path,
284 diagnostic: diagnostic.map(Box::new),
285 message: message.into(),
286 source,
287 }
288 }
289
290 #[must_use]
295 pub fn remote(
296 code: impl Into<String>,
297 message: impl Into<String>,
298 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
299 ) -> Self {
300 Self::classified(
301 ErrorCategory::Remote,
302 None,
303 code,
304 Vec::new(),
305 message,
306 source,
307 )
308 }
309
310 pub(crate) fn from_match(error: MatchError, phase: ModelValidationPhase) -> Self {
311 let category = match error.category() {
312 MatchErrorCategory::InvalidPlan => ErrorCategory::QueryAuthoring,
313 MatchErrorCategory::Cardinality | MatchErrorCategory::ResultDecode => {
314 ErrorCategory::ModelValidation
315 }
316 MatchErrorCategory::UnsupportedCapability => ErrorCategory::Capability,
317 MatchErrorCategory::StaleSchema => ErrorCategory::Schema,
318 MatchErrorCategory::ResourceLimit => ErrorCategory::ResourceLimit,
319 MatchErrorCategory::Provider => ErrorCategory::QueryExecution,
320 };
321 let model_phase = (category == ErrorCategory::ModelValidation).then_some(phase);
322 let code = error.code().as_str().to_owned();
323 let path = error
324 .path()
325 .segments()
326 .iter()
327 .map(ToString::to_string)
328 .collect();
329 let message = error.message().to_owned();
330 Self::classified(
331 category,
332 model_phase,
333 code,
334 path,
335 message,
336 Some(Box::new(error)),
337 )
338 }
339
340 pub(crate) fn from_remote_diagnostic(error: Diagnostic) -> Self {
341 let category = match error.category() {
342 DiagnosticCategory::UnsupportedCapability => ErrorCategory::Capability,
343 DiagnosticCategory::ResourceLimit => ErrorCategory::ResourceLimit,
344 DiagnosticCategory::InvalidContract | DiagnosticCategory::Integrity => {
345 ErrorCategory::Remote
346 }
347 };
348 let code = error.code().as_str().to_owned();
349 let path = error
350 .path()
351 .segments()
352 .iter()
353 .map(|segment| match segment {
354 DiagnosticPathSegment::Field(value) => value.clone(),
355 DiagnosticPathSegment::Index(value) => format!("[{value}]"),
356 DiagnosticPathSegment::Identifier(value) => value.clone(),
357 })
358 .collect();
359 let diagnostic_path = error
360 .path()
361 .segments()
362 .iter()
363 .map(|segment| match segment {
364 DiagnosticPathSegment::Field(value) => ErrorPathSegment::Field(value.clone()),
365 DiagnosticPathSegment::Index(value) => ErrorPathSegment::Index(*value),
366 DiagnosticPathSegment::Identifier(value) => {
367 ErrorPathSegment::Identifier(value.clone())
368 }
369 })
370 .collect();
371 let details = error
372 .details()
373 .iter()
374 .map(|(key, value)| {
375 let value = match value {
376 DiagnosticDetailValue::Text(value) => ErrorDetail::Text(value.clone()),
377 DiagnosticDetailValue::Long(value) => ErrorDetail::Long(*value),
378 DiagnosticDetailValue::Boolean(value) => ErrorDetail::Boolean(*value),
379 DiagnosticDetailValue::TextList(value) => ErrorDetail::TextList(value.clone()),
380 };
381 (key.clone(), value)
382 })
383 .collect();
384 let message = error.message().to_owned();
385 Self::classified_with_diagnostic(
386 category,
387 None,
388 code,
389 path,
390 Some(ErrorDiagnostic {
391 path: diagnostic_path,
392 details,
393 }),
394 message,
395 Some(Box::new(error)),
396 )
397 }
398
399 pub(crate) fn from_hook(error: crate::hooks::HookError) -> Self {
400 let code = match error {
401 crate::hooks::HookError::Rejected { .. } => "lifecycle_hook_rejected",
402 crate::hooks::HookError::Internal { .. } => "lifecycle_hook_failed",
403 };
404 Self::classified(
405 ErrorCategory::Lifecycle,
406 None,
407 code,
408 Vec::new(),
409 error.to_string(),
410 Some(Box::new(error)),
411 )
412 }
413
414 #[allow(dead_code)]
415 pub(crate) fn from_orm(err: type_bridge_orm::OrmError) -> Self {
416 match err {
417 type_bridge_orm::OrmError::Match(error) => {
418 Self::from_match(error, ModelValidationPhase::Input)
419 }
420 error @ type_bridge_orm::OrmError::Connection(_) => Self::Connection {
421 message: error.to_string(),
422 source: Some(Box::new(error)),
423 },
424 error @ type_bridge_orm::OrmError::QueryExecution(_) => Self::QueryExecution {
425 message: error.to_string(),
426 source: Some(Box::new(error)),
427 },
428 error @ type_bridge_orm::OrmError::Transaction(_) => Self::Transaction {
429 message: error.to_string(),
430 source: Some(Box::new(error)),
431 },
432 error @ type_bridge_orm::OrmError::NotFound(_) => Self::NotFound {
433 message: error.to_string(),
434 source: Some(Box::new(error)),
435 },
436 error @ type_bridge_orm::OrmError::Hydration { .. } => Self::ModelValidation {
437 phase: ModelValidationPhase::Hydration,
438 code: "invalid_provider_evidence".into(),
439 path: vec![],
440 message: error.to_string(),
441 source: Some(Box::new(error)),
442 },
443 error => Self::Database {
444 message: error.to_string(),
445 source: Some(Box::new(error)),
446 },
447 }
448 }
449
450 pub(crate) fn from_orm_hydration(err: type_bridge_orm::OrmError) -> Self {
451 match err {
452 type_bridge_orm::OrmError::Match(error) => {
453 Self::from_match(error, ModelValidationPhase::Hydration)
454 }
455 error => Self::from_orm(error),
456 }
457 }
458
459 #[must_use]
461 pub const fn category(&self) -> ErrorCategory {
462 match self {
463 Self::ModelValidation { .. } => ErrorCategory::ModelValidation,
464 Self::Classified { category, .. } => *category,
465 Self::SchemaVerification { .. } => ErrorCategory::Schema,
466 Self::Connection { .. } => ErrorCategory::Connection,
467 Self::QueryExecution { .. } => ErrorCategory::QueryExecution,
468 Self::Transaction { .. } => ErrorCategory::Transaction,
469 Self::NotFound { .. } => ErrorCategory::NotFound,
470 Self::Database { .. } => ErrorCategory::Database,
471 Self::Other { .. } => ErrorCategory::Other,
472 }
473 }
474
475 #[must_use]
477 pub fn message(&self) -> &str {
478 match self {
479 Self::ModelValidation { message, .. }
480 | Self::Classified { message, .. }
481 | Self::SchemaVerification { message, .. }
482 | Self::Connection { message, .. }
483 | Self::QueryExecution { message, .. }
484 | Self::Transaction { message, .. }
485 | Self::NotFound { message, .. }
486 | Self::Database { message, .. }
487 | Self::Other { message, .. } => message,
488 }
489 }
490
491 #[must_use]
493 pub fn code(&self) -> Option<&str> {
494 match self {
495 Self::ModelValidation { code, .. } | Self::Classified { code, .. } => Some(code),
496 _ => None,
497 }
498 }
499
500 #[must_use]
502 pub fn path(&self) -> Option<&[String]> {
503 match self {
504 Self::ModelValidation { path, .. } | Self::Classified { path, .. } => Some(path),
505 _ => None,
506 }
507 }
508
509 #[must_use]
514 pub fn diagnostic_path(&self) -> Option<&[ErrorPathSegment]> {
515 match self {
516 Self::Classified { diagnostic, .. } => diagnostic.as_deref().map(ErrorDiagnostic::path),
517 _ => None,
518 }
519 }
520
521 #[must_use]
523 pub fn details(&self) -> Option<&BTreeMap<String, ErrorDetail>> {
524 match self {
525 Self::Classified { diagnostic, .. } => {
526 diagnostic.as_deref().map(ErrorDiagnostic::details)
527 }
528 _ => None,
529 }
530 }
531
532 #[must_use]
534 pub const fn model_validation_phase(&self) -> Option<ModelValidationPhase> {
535 match self {
536 Self::ModelValidation { phase, .. } => Some(*phase),
537 Self::Classified { phase, .. } => *phase,
538 _ => None,
539 }
540 }
541}
542
543pub type Result<T, E = Error> = std::result::Result<T, E>;
545
546#[cfg(test)]
547mod tests {
548 use super::{Error, ErrorCategory};
549
550 #[test]
551 fn public_error_categories_and_remote_constructor_are_stable() {
552 let categories = [
553 (ErrorCategory::Connection, "connection"),
554 (ErrorCategory::Schema, "schema"),
555 (ErrorCategory::ModelValidation, "model_validation"),
556 (ErrorCategory::QueryAuthoring, "query_authoring"),
557 (ErrorCategory::QueryExecution, "query_execution"),
558 (ErrorCategory::Transaction, "transaction"),
559 (ErrorCategory::Remote, "remote"),
560 (ErrorCategory::Capability, "capability"),
561 (ErrorCategory::ResourceLimit, "resource_limit"),
562 (ErrorCategory::NotFound, "not_found"),
563 (ErrorCategory::Lifecycle, "lifecycle"),
564 (ErrorCategory::Database, "database"),
565 (ErrorCategory::Other, "other"),
566 ];
567 for (category, spelling) in categories {
568 assert_eq!(category.as_str(), spelling);
569 assert_eq!(category.to_string(), spelling);
570 }
571
572 let error = Error::remote("remote_transport", "connection reset", None);
573 assert_eq!(error.category(), ErrorCategory::Remote);
574 assert_eq!(error.code(), Some("remote_transport"));
575 assert_eq!(error.path(), Some(&[][..]));
576 assert_eq!(error.message(), "connection reset");
577 }
578}