dwara_core/
error.rs

1//! Facade-level aggregate error.
2//!
3//! Each domain owns a typed error describing the failures it knows how
4//! to express. This module adds [`Error`]: a single non-exhaustive enum
5//! over those domain errors, so the bin/admin/cli crates (and future API
6//! surfaces) get one match surface at their boundaries instead of eight.
7//!
8//! The domains keep their typed errors — a call site that can recover
9//! from a specific failure should keep matching the domain error (a
10//! quota-aware path matches [`StoreError::QuotaExceeded`], a retry
11//! decision matches [`UpstreamError::ConnectTimeout`]). [`Error`] is for
12//! boundary propagation, where the remaining actions are to log, report,
13//! or terminate: the bin/admin entry points and future API surfaces. The
14//! conversion is lossless — [`From`] wraps the domain error unchanged,
15//! [`Display`](std::fmt::Display) forwards its message, and
16//! [`source`](std::error::Error::source) returns the inner error, so
17//! error chains and `downcast_ref` on that `&dyn Error` keep working.
18//!
19//! # Example
20//!
21//! ```
22//! use dwara_core::error::Error;
23//! use dwara_core::security::tls::TlsError;
24//!
25//! let e: Error = TlsError::NoCertificates.into();
26//! assert!(matches!(e, Error::Tls(_)));
27//! // Display delegates to the domain error's message verbatim:
28//! assert_eq!(e.to_string(), "tls terminate block has no certificate material");
29//! // and source() yields the inner error for chains/downcasting:
30//! assert!(std::error::Error::source(&e).is_some());
31//! ```
32
33use crate::ai::adapter::AiError;
34use crate::dataplane::hardening::InboundBodyError;
35use crate::dataplane::upstream::{UpstreamBodyError, UpstreamError};
36use crate::extensions::ExtensionsError;
37use crate::security::authn::AuthError;
38use crate::security::tls::TlsError;
39use crate::snapshot::CompileError;
40use crate::state::store::StoreError;
41
42/// The facade-level aggregate over the domain errors: one type to
43/// propagate across crate boundaries (bin/admin entry points, future API
44/// surfaces). See the [module docs](self) for when to match a domain
45/// error instead.
46#[derive(Debug)]
47#[non_exhaustive]
48pub enum Error {
49    /// Config compile pipeline failure (validate/compile).
50    Compile(CompileError),
51    /// TLS material load/build/reload failure.
52    Tls(TlsError),
53    /// Durable state store failure.
54    Store(StoreError),
55    /// Upstream request failure (dial, handshake, headers).
56    Upstream(UpstreamError),
57    /// Upstream streamed response-body failure.
58    UpstreamBody(UpstreamBodyError),
59    /// Inbound request-body wrapper failure.
60    InboundBody(InboundBodyError),
61    /// Authentication failure.
62    Auth(AuthError),
63    /// Extension subsystem failure.
64    Extensions(ExtensionsError),
65    /// AI domain failure (request translation or provider error, DW-075).
66    Ai(AiError),
67}
68
69impl std::fmt::Display for Error {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        // Delegates verbatim: the domain errors already carry their
72        // context, and the wrapped error stays reachable via source().
73        match self {
74            Error::Compile(e) => e.fmt(f),
75            Error::Tls(e) => e.fmt(f),
76            Error::Store(e) => e.fmt(f),
77            Error::Upstream(e) => e.fmt(f),
78            Error::UpstreamBody(e) => e.fmt(f),
79            Error::InboundBody(e) => e.fmt(f),
80            Error::Auth(e) => e.fmt(f),
81            Error::Extensions(e) => e.fmt(f),
82            Error::Ai(e) => e.fmt(f),
83        }
84    }
85}
86
87impl std::error::Error for Error {
88    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
89        match self {
90            Error::Compile(e) => Some(e),
91            Error::Tls(e) => Some(e),
92            Error::Store(e) => Some(e),
93            Error::Upstream(e) => Some(e),
94            Error::UpstreamBody(e) => Some(e),
95            Error::InboundBody(e) => Some(e),
96            Error::Auth(e) => Some(e),
97            Error::Extensions(e) => Some(e),
98            Error::Ai(e) => Some(e),
99        }
100    }
101}
102
103impl From<CompileError> for Error {
104    fn from(e: CompileError) -> Self {
105        Error::Compile(e)
106    }
107}
108
109impl From<TlsError> for Error {
110    fn from(e: TlsError) -> Self {
111        Error::Tls(e)
112    }
113}
114
115impl From<StoreError> for Error {
116    fn from(e: StoreError) -> Self {
117        Error::Store(e)
118    }
119}
120
121impl From<UpstreamError> for Error {
122    fn from(e: UpstreamError) -> Self {
123        Error::Upstream(e)
124    }
125}
126
127impl From<UpstreamBodyError> for Error {
128    fn from(e: UpstreamBodyError) -> Self {
129        Error::UpstreamBody(e)
130    }
131}
132
133impl From<InboundBodyError> for Error {
134    fn from(e: InboundBodyError) -> Self {
135        Error::InboundBody(e)
136    }
137}
138
139impl From<AuthError> for Error {
140    fn from(e: AuthError) -> Self {
141        Error::Auth(e)
142    }
143}
144
145impl From<ExtensionsError> for Error {
146    fn from(e: ExtensionsError) -> Self {
147        Error::Extensions(e)
148    }
149}
150
151impl From<AiError> for Error {
152    fn from(e: AiError) -> Self {
153        Error::Ai(e)
154    }
155}
156// retrigger CI after a lost push event (path-filtered workflows
157// need a real file change; empty commits do not qualify).