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).