Skip to main content

type_bridge/
error.rs

1//! Error handling for the public TypeBridge client.
2
3use 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/// Stable public classification for TypeBridge client failures.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum ErrorCategory {
13    /// Connection establishment or connectivity failed.
14    Connection,
15    /// Generated or installed schema authority failed verification.
16    Schema,
17    /// Generated input or provider evidence failed model validation.
18    ModelValidation,
19    /// A typed query was invalid before provider execution.
20    QueryAuthoring,
21    /// The provider failed while executing an accepted query.
22    QueryExecution,
23    /// A transaction lifecycle operation failed.
24    Transaction,
25    /// A remote envelope, reply, transport, or integrity contract failed.
26    Remote,
27    /// The selected provider or remote executor lacks a required capability.
28    Capability,
29    /// A canonical client, provider, or remote resource ceiling was exceeded.
30    ResourceLimit,
31    /// A requested entity or schema element was not found.
32    NotFound,
33    /// An underlying database operation failed outside a narrower category.
34    Database,
35    /// A client invariant failed outside the stable categories above.
36    Other,
37}
38
39impl ErrorCategory {
40    /// Return the stable language-neutral category spelling.
41    #[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/// Stage at which generated-model evidence failed validation.
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum ModelValidationPhase {
69    /// Generated constructor input failed before provider execution.
70    Input,
71    /// Provider row evidence failed while hydrating a generated model.
72    Hydration,
73}
74
75/// Primary error type for the TypeBridge client SDK.
76#[derive(Debug, thiserror::Error)]
77#[non_exhaustive]
78pub enum Error {
79    /// Generated-model evidence did not match the installed schema projection.
80    #[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    /// A structured engine or remote-contract failure mapped into stable
91    /// client-owned categories, codes, and paths.
92    #[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    /// Schema verification or installation failed.
104    #[error("Schema verification failed: {message}")]
105    SchemaVerification {
106        message: String,
107        #[source]
108        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
109    },
110
111    /// Connection to the database failed.
112    #[error("Connection error: {message}")]
113    Connection {
114        message: String,
115        #[source]
116        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
117    },
118
119    /// Database query or operation failed.
120    #[error("Query execution error: {message}")]
121    QueryExecution {
122        message: String,
123        #[source]
124        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
125    },
126
127    /// Database transaction failed.
128    #[error("Transaction error: {message}")]
129    Transaction {
130        message: String,
131        #[source]
132        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
133    },
134
135    /// Requested schema element or database entity was not found.
136    #[error("Entity not found: {message}")]
137    NotFound {
138        message: String,
139        #[source]
140        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
141    },
142
143    /// Underlying database error.
144    #[error("Database error: {message}")]
145    Database {
146        message: String,
147        #[source]
148        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
149    },
150
151    /// Client request or execution error.
152    #[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    /// Construct one application-owned remote transport failure.
197    ///
198    /// Transport implementations should use a stable lowercase snake-case
199    /// code so callers can handle the failure without parsing its message.
200    #[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    /// Return the stable public failure category.
315    #[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    /// Return the error message string.
331    #[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    /// Return the stable machine-readable failure code, when available.
347    #[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    /// Return the owned structured diagnostic path, when available.
356    #[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    /// Return the model-validation phase, when applicable.
365    #[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
375/// Convenience Result type for the TypeBridge client.
376pub 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}