Skip to main content

bicmath_core/
error.rs

1//! Stable error codes and structured errors.
2
3use serde::{Deserialize, Serialize};
4
5/// Stable machine-readable error codes. These are part of the wire contract:
6/// callers may branch on them.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ErrorCode {
10    /// Input was not valid JSON or did not match the declared schema shape.
11    MalformedInput,
12    /// The requested function id does not exist.
13    UnknownFunction,
14    /// The requested module id does not exist.
15    UnknownModule,
16    /// The module exists but is disabled by configuration.
17    DisabledModule,
18    /// The input was well-formed but outside the function's mathematical domain.
19    DomainViolation,
20    /// Division or remainder by zero.
21    DivisionByZero,
22    /// Quantity dimensions are incompatible.
23    IncompatibleUnits,
24    /// Two monetary amounts with different currencies were combined without an
25    /// explicit conversion.
26    CurrencyMismatch,
27    /// Not enough observations for the requested estimator.
28    InsufficientObservations,
29    /// Matrix is singular to working precision.
30    SingularMatrix,
31    /// Matrix is numerically ill-conditioned; results may be unreliable.
32    IllConditioned,
33    /// The operation is not supported in the selected numeric mode.
34    UnsupportedNumericMode,
35    /// The result would exceed configured precision.
36    PrecisionLimit,
37    /// An iterative method failed to converge.
38    NonConvergence,
39    /// A configured resource limit was reached.
40    ResourceLimit,
41    /// The caller cancelled the request.
42    Cancelled,
43    /// A batch node failed and a dependent node could not run.
44    BatchDependencyFailed,
45    /// The operation is recognised but not implemented in this release.
46    UnsupportedOperation,
47    /// A referenced batch node, binding, or resource was not found.
48    NotFound,
49    /// Internal invariant violation. Never returned for ordinary user input.
50    Internal,
51}
52
53impl ErrorCode {
54    pub fn as_str(self) -> &'static str {
55        match self {
56            ErrorCode::MalformedInput => "malformed_input",
57            ErrorCode::UnknownFunction => "unknown_function",
58            ErrorCode::UnknownModule => "unknown_module",
59            ErrorCode::DisabledModule => "disabled_module",
60            ErrorCode::DomainViolation => "domain_violation",
61            ErrorCode::DivisionByZero => "division_by_zero",
62            ErrorCode::IncompatibleUnits => "incompatible_units",
63            ErrorCode::CurrencyMismatch => "currency_mismatch",
64            ErrorCode::InsufficientObservations => "insufficient_observations",
65            ErrorCode::SingularMatrix => "singular_matrix",
66            ErrorCode::IllConditioned => "ill_conditioned",
67            ErrorCode::UnsupportedNumericMode => "unsupported_numeric_mode",
68            ErrorCode::PrecisionLimit => "precision_limit",
69            ErrorCode::NonConvergence => "non_convergence",
70            ErrorCode::ResourceLimit => "resource_limit",
71            ErrorCode::Cancelled => "cancelled",
72            ErrorCode::BatchDependencyFailed => "batch_dependency_failed",
73            ErrorCode::UnsupportedOperation => "unsupported_operation",
74            ErrorCode::NotFound => "not_found",
75            ErrorCode::Internal => "internal",
76        }
77    }
78}
79
80impl std::fmt::Display for ErrorCode {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.write_str(self.as_str())
83    }
84}
85
86/// A structured engine error. Errors never carry a fabricated successful result.
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88pub struct EngineError {
89    pub code: ErrorCode,
90    pub message: String,
91    /// JSON-pointer-like path to the offending input, when known.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub path: Option<String>,
94    /// Additional structured detail. Always a JSON object.
95    #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
96    pub details: serde_json::Value,
97}
98
99impl EngineError {
100    pub fn new(code: ErrorCode, message: impl Into<String>) -> EngineError {
101        EngineError {
102            code,
103            message: message.into(),
104            path: None,
105            details: serde_json::Value::Null,
106        }
107    }
108
109    pub fn malformed(message: impl Into<String>) -> EngineError {
110        Self::new(ErrorCode::MalformedInput, message)
111    }
112
113    pub fn domain(message: impl Into<String>) -> EngineError {
114        Self::new(ErrorCode::DomainViolation, message)
115    }
116
117    pub fn division_by_zero(message: impl Into<String>) -> EngineError {
118        Self::new(ErrorCode::DivisionByZero, message)
119    }
120
121    pub fn unsupported(message: impl Into<String>) -> EngineError {
122        Self::new(ErrorCode::UnsupportedOperation, message)
123    }
124
125    pub fn resource(message: impl Into<String>) -> EngineError {
126        Self::new(ErrorCode::ResourceLimit, message)
127    }
128
129    pub fn internal(message: impl Into<String>) -> EngineError {
130        Self::new(ErrorCode::Internal, message)
131    }
132
133    pub fn cancelled() -> EngineError {
134        Self::new(ErrorCode::Cancelled, "calculation was cancelled")
135    }
136
137    pub fn with_path(mut self, path: impl Into<String>) -> EngineError {
138        self.path = Some(path.into());
139        self
140    }
141
142    pub fn with_details(mut self, details: serde_json::Value) -> EngineError {
143        self.details = details;
144        self
145    }
146
147    /// Wrap another error as a batch dependency failure.
148    pub fn batch_dependency(node: &str, source: &EngineError) -> EngineError {
149        EngineError::new(
150            ErrorCode::BatchDependencyFailed,
151            format!("node {node:?} failed: {}", source.message),
152        )
153        .with_details(serde_json::json!({
154            "node": node,
155            "source_code": source.code.as_str(),
156        }))
157    }
158}
159
160impl std::fmt::Display for EngineError {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        write!(f, "{}: {}", self.code, self.message)?;
163        if let Some(path) = &self.path {
164            write!(f, " (at {path})")?;
165        }
166        Ok(())
167    }
168}
169
170impl std::error::Error for EngineError {}