Skip to main content

syncbat/
error.rs

1//! Error types for the syncbat runtime shell.
2
3use std::error::Error;
4use std::fmt;
5
6/// Error returned while assembling a [`crate::core::Core`].
7///
8/// `#[non_exhaustive]` so post-1.0 we can add validation variants
9/// (e.g. cross-module descriptor checks, schema-version drift) without
10/// breaking downstream exhaustive matches.
11#[derive(Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum BuildError {
14    /// An operation descriptor was registered more than once.
15    DuplicateOperation {
16        /// Duplicate operation name.
17        name: String,
18    },
19    /// A handler was registered more than once for the same operation name.
20    DuplicateHandler {
21        /// Duplicate handler name.
22        name: String,
23    },
24    /// A handler was registered without a matching operation descriptor.
25    MissingDescriptor {
26        /// Handler name without a descriptor.
27        name: String,
28    },
29    /// An operation descriptor was registered without a matching handler.
30    MissingHandler {
31        /// Operation name without a handler.
32        name: String,
33    },
34    /// A module descriptor failed shape validation.
35    InvalidModule {
36        /// Module name.
37        name: String,
38        /// Validation message.
39        message: String,
40    },
41    /// An operation descriptor failed shape validation.
42    InvalidOperation {
43        /// Operation name.
44        name: String,
45        /// Validation message.
46        message: String,
47    },
48    /// A handler name failed shape validation.
49    InvalidHandler {
50        /// Handler name.
51        name: String,
52        /// Validation message.
53        message: String,
54    },
55    /// The builder was finalized without a receipt sink and without an explicit
56    /// receipts opt-out.
57    ///
58    /// A sinkless core silently drops every runtime receipt, so the build fails
59    /// closed. Wire a sink with
60    /// [`crate::builder::CoreBuilder::receipt_sink`], or state on purpose that
61    /// this core records no receipts with
62    /// [`crate::builder::CoreBuilder::without_receipts`].
63    MissingReceiptSink,
64}
65
66impl BuildError {
67    /// Build a duplicate-operation error.
68    #[must_use]
69    pub fn duplicate_operation(name: impl Into<String>) -> Self {
70        Self::DuplicateOperation { name: name.into() }
71    }
72
73    /// Build a duplicate-handler error.
74    #[must_use]
75    pub fn duplicate_handler(name: impl Into<String>) -> Self {
76        Self::DuplicateHandler { name: name.into() }
77    }
78
79    /// Build a missing-descriptor error.
80    #[must_use]
81    pub fn missing_descriptor(name: impl Into<String>) -> Self {
82        Self::MissingDescriptor { name: name.into() }
83    }
84
85    /// Build a missing-handler error.
86    #[must_use]
87    pub fn missing_handler(name: impl Into<String>) -> Self {
88        Self::MissingHandler { name: name.into() }
89    }
90
91    /// Build an invalid-module error.
92    #[must_use]
93    pub fn invalid_module(name: impl Into<String>, message: impl Into<String>) -> Self {
94        Self::InvalidModule {
95            name: name.into(),
96            message: message.into(),
97        }
98    }
99
100    /// Build an invalid-operation error.
101    #[must_use]
102    pub fn invalid_operation(name: impl Into<String>, message: impl Into<String>) -> Self {
103        Self::InvalidOperation {
104            name: name.into(),
105            message: message.into(),
106        }
107    }
108
109    /// Build an invalid-handler error.
110    #[must_use]
111    pub fn invalid_handler(name: impl Into<String>, message: impl Into<String>) -> Self {
112        Self::InvalidHandler {
113            name: name.into(),
114            message: message.into(),
115        }
116    }
117}
118
119impl fmt::Display for BuildError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            Self::DuplicateOperation { name } => {
123                write!(f, "operation `{name}` is already registered")
124            }
125            Self::DuplicateHandler { name } => {
126                write!(f, "handler for operation `{name}` is already registered")
127            }
128            Self::MissingDescriptor { name } => {
129                write!(f, "handler `{name}` has no matching operation descriptor")
130            }
131            Self::MissingHandler { name } => {
132                write!(f, "operation `{name}` has no registered handler")
133            }
134            Self::InvalidModule { name, message } => {
135                write!(f, "module `{name}` is invalid: {message}")
136            }
137            Self::InvalidOperation { name, message } => {
138                write!(f, "operation `{name}` is invalid: {message}")
139            }
140            Self::InvalidHandler { name, message } => {
141                write!(f, "handler `{name}` is invalid: {message}")
142            }
143            Self::MissingReceiptSink => f.write_str(
144                "core has no receipt sink: a sinkless core silently drops every runtime \
145                 receipt. Wire one with CoreBuilder::receipt_sink, or opt out explicitly \
146                 with CoreBuilder::without_receipts",
147            ),
148        }
149    }
150}
151
152impl Error for BuildError {}
153
154/// Handler failure preserved when receipt recording also fails.
155#[derive(Clone, Debug, Eq, PartialEq)]
156#[non_exhaustive]
157pub struct ReceiptSinkHandlerCause {
158    /// Handler-supplied error class.
159    pub code: String,
160    /// Handler-supplied error message.
161    pub message: String,
162}
163
164impl ReceiptSinkHandlerCause {
165    /// Build a handler cause for a receipt-sink failure.
166    #[must_use]
167    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
168        Self {
169            code: code.into(),
170            message: message.into(),
171        }
172    }
173
174    /// Stable handler error class.
175    #[must_use]
176    pub fn code(&self) -> &str {
177        &self.code
178    }
179
180    /// Handler-supplied error message.
181    #[must_use]
182    pub fn message(&self) -> &str {
183        &self.message
184    }
185}
186
187/// Error returned by synchronous operation dispatch.
188///
189/// `#[non_exhaustive]` so the wire-error vocabulary can grow (e.g.
190/// rate-limit, auth, schema-mismatch variants) without breaking
191/// downstream matches that translate `RuntimeError` into transport-
192/// layer error codes.
193#[derive(Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum RuntimeError {
196    /// The requested operation name is not known to the runtime.
197    UnknownOperation {
198        /// Requested operation name.
199        name: String,
200    },
201    /// The operation descriptor exists, but no handler is available.
202    MissingHandler {
203        /// Requested operation name.
204        name: String,
205    },
206    /// The handler rejected the invocation.
207    Handler {
208        /// Operation name being handled.
209        name: String,
210        /// Handler-supplied error class.
211        code: String,
212        /// Handler-supplied error message.
213        message: String,
214    },
215    /// Runtime policy denied the invocation. Admission guards and observed
216    /// effect-row enforcement both record a `Denied` receipt before returning
217    /// this variant.
218    Denied {
219        /// Operation name that was denied.
220        name: String,
221        /// Guard-supplied denial class.
222        code: String,
223        /// Guard-supplied denial message.
224        message: String,
225    },
226    /// The configured receipt sink rejected a runtime-emitted receipt.
227    ReceiptSink {
228        /// Operation name whose receipt could not be recorded.
229        name: String,
230        /// Sink error message.
231        message: String,
232        /// Handler failure that preceded this sink failure, when present.
233        caused_by_handler: Option<ReceiptSinkHandlerCause>,
234    },
235    /// The configured operation-status sink rejected a runtime-emitted fact.
236    StatusSink {
237        /// Operation name whose status fact could not be recorded.
238        name: String,
239        /// Sink error message.
240        message: String,
241        /// Handler failure that preceded this sink failure, when present.
242        caused_by_handler: Option<ReceiptSinkHandlerCause>,
243    },
244}
245
246impl RuntimeError {
247    /// Build an unknown-operation error.
248    #[must_use]
249    pub fn unknown_operation(name: impl Into<String>) -> Self {
250        Self::UnknownOperation { name: name.into() }
251    }
252
253    /// Build a missing-handler error.
254    #[must_use]
255    pub fn missing_handler(name: impl Into<String>) -> Self {
256        Self::MissingHandler { name: name.into() }
257    }
258
259    /// Build a handler error with an operation name and message.
260    #[must_use]
261    pub fn handler(
262        name: impl Into<String>,
263        code: impl Into<String>,
264        message: impl Into<String>,
265    ) -> Self {
266        Self::Handler {
267            name: name.into(),
268            code: code.into(),
269            message: message.into(),
270        }
271    }
272
273    /// Build an admission-denied error with an operation name, class, and message.
274    #[must_use]
275    pub fn denied(
276        name: impl Into<String>,
277        code: impl Into<String>,
278        message: impl Into<String>,
279    ) -> Self {
280        Self::Denied {
281            name: name.into(),
282            code: code.into(),
283            message: message.into(),
284        }
285    }
286
287    /// Build a receipt-sink error with an operation name and message.
288    #[must_use]
289    pub fn receipt_sink(name: impl Into<String>, message: impl Into<String>) -> Self {
290        Self::ReceiptSink {
291            name: name.into(),
292            message: message.into(),
293            caused_by_handler: None,
294        }
295    }
296
297    /// Build a receipt-sink error after a handler failure.
298    #[must_use]
299    pub fn receipt_sink_after_handler_failure(
300        name: impl Into<String>,
301        message: impl Into<String>,
302        cause: ReceiptSinkHandlerCause,
303    ) -> Self {
304        Self::ReceiptSink {
305            name: name.into(),
306            message: message.into(),
307            caused_by_handler: Some(cause),
308        }
309    }
310
311    /// Build a status-sink error with an operation name and message.
312    #[must_use]
313    pub fn status_sink(name: impl Into<String>, message: impl Into<String>) -> Self {
314        Self::StatusSink {
315            name: name.into(),
316            message: message.into(),
317            caused_by_handler: None,
318        }
319    }
320
321    /// Build a status-sink error after a handler failure.
322    #[must_use]
323    pub fn status_sink_after_handler_failure(
324        name: impl Into<String>,
325        message: impl Into<String>,
326        cause: ReceiptSinkHandlerCause,
327    ) -> Self {
328        Self::StatusSink {
329            name: name.into(),
330            message: message.into(),
331            caused_by_handler: Some(cause),
332        }
333    }
334}
335
336impl fmt::Display for RuntimeError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            Self::UnknownOperation { name } => write!(f, "unknown operation `{name}`"),
340            Self::MissingHandler { name } => {
341                write!(f, "operation `{name}` has no registered handler")
342            }
343            Self::Handler {
344                name,
345                code,
346                message,
347            } => {
348                write!(
349                    f,
350                    "handler for operation `{name}` failed with {code}: {message}"
351                )
352            }
353            Self::Denied {
354                name,
355                code,
356                message,
357            } => {
358                write!(f, "operation `{name}` denied with {code}: {message}")
359            }
360            Self::ReceiptSink {
361                name,
362                message,
363                caused_by_handler,
364            } => {
365                if let Some(cause) = caused_by_handler {
366                    write!(
367                        f,
368                        "receipt sink for operation `{name}` failed after handler error {}: {}: {message}",
369                        cause.code(),
370                        cause.message()
371                    )
372                } else {
373                    write!(f, "receipt sink for operation `{name}` failed: {message}")
374                }
375            }
376            Self::StatusSink {
377                name,
378                message,
379                caused_by_handler,
380            } => {
381                if let Some(cause) = caused_by_handler {
382                    write!(
383                        f,
384                        "status sink for operation `{name}` failed after handler error {}: {}: {message}",
385                        cause.code(),
386                        cause.message()
387                    )
388                } else {
389                    write!(f, "status sink for operation `{name}` failed: {message}")
390                }
391            }
392        }
393    }
394}
395
396impl Error for RuntimeError {}