Skip to main content

formualizer_common/
error.rs

1//! Excel-style error representation that is both ergonomic **now**
2//! *and* flexible enough to grow new, data-rich variants later.
3//!
4//! - **`ExcelErrorKind`** : the canonical set of Excel error codes  
5//! - **`ErrorContext`**   : lightweight, sheet-agnostic location info  
6//! - **`ExcelErrorExtra`**: per-kind “extension slot” (e.g. `Spill`)  
7//! - **`ExcelError`**     : one struct that glues the three together
8//!
9//! When a future error needs its own payload, just add another variant
10//! to `ExcelErrorExtra`; existing code does not break.
11
12use std::{error::Error, fmt};
13
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17use crate::LiteralValue;
18
19/// All recognised Excel error codes.
20///
21/// **Note:** names are CamelCase (idiomatic Rust) while `Display`
22/// renders them exactly as Excel shows them (`#DIV/0!`, …).
23#[non_exhaustive]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
26pub enum ExcelErrorKind {
27    Null,
28    Ref,
29    Name,
30    Value,
31    Div,
32    Na,
33    Num,
34    Error,
35    NImpl,
36    Spill,
37    Calc,
38    Circ,
39    Cancelled,
40}
41
42impl fmt::Display for ExcelErrorKind {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(match self {
45            Self::Null => "#NULL!",
46            Self::Ref => "#REF!",
47            Self::Name => "#NAME?",
48            Self::Value => "#VALUE!",
49            Self::Div => "#DIV/0!",
50            Self::Na => "#N/A",
51            Self::Num => "#NUM!",
52            Self::Error => "#ERROR!",
53            Self::NImpl => "#N/IMPL!",
54            Self::Spill => "#SPILL!",
55            Self::Calc => "#CALC!",
56            Self::Circ => "#CIRC!",
57            Self::Cancelled => "#CANCELLED!",
58        })
59    }
60}
61
62impl ExcelErrorKind {
63    pub fn try_parse(s: &str) -> Option<Self> {
64        match s.trim().to_ascii_lowercase().as_str() {
65            "#null!" => Some(Self::Null),
66            "#ref!" => Some(Self::Ref),
67            "#name?" => Some(Self::Name),
68            "#value!" => Some(Self::Value),
69            "#div/0!" => Some(Self::Div),
70            "#n/a" => Some(Self::Na),
71            "#num!" => Some(Self::Num),
72            "#error!" => Some(Self::Error),
73            "#n/impl!" => Some(Self::NImpl),
74            "#spill!" => Some(Self::Spill),
75            "#calc!" => Some(Self::Calc),
76            "#circ!" => Some(Self::Circ),
77            "#cancelled!" => Some(Self::Cancelled),
78            _ => None,
79        }
80    }
81
82    pub fn parse(s: &str) -> Self {
83        Self::try_parse(s).unwrap_or(Self::Error)
84    }
85}
86
87/// Generic, lightweight metadata that *any* error may carry.
88///
89/// Keep this minimal—anything only one error kind needs belongs in
90/// `ExcelErrorExtra`.
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
93pub struct ErrorContext {
94    pub row: Option<u32>,
95    pub col: Option<u32>,
96    // Origin location where the error first occurred (if different from row/col)
97    pub origin_row: Option<u32>,
98    pub origin_col: Option<u32>,
99    pub origin_sheet: Option<String>,
100}
101
102/// Stable reason for a resource exhaustion diagnostic.
103#[non_exhaustive]
104#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
105#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
106pub enum ResourceExhaustionReason {
107    Admission,
108    RetainedMemory,
109    ScratchMemory,
110    WorkUnits,
111    Deadline,
112    GraphVertices,
113    GraphEdges,
114    MaterializationCells,
115    ArithmeticOverflow,
116}
117
118impl ResourceExhaustionReason {
119    pub const fn as_str(self) -> &'static str {
120        match self {
121            Self::Admission => "admission",
122            Self::RetainedMemory => "retained_memory",
123            Self::ScratchMemory => "scratch_memory",
124            Self::WorkUnits => "work_units",
125            Self::Deadline => "deadline",
126            Self::GraphVertices => "graph_vertices",
127            Self::GraphEdges => "graph_edges",
128            Self::MaterializationCells => "materialization_cells",
129            Self::ArithmeticOverflow => "arithmetic_overflow",
130        }
131    }
132}
133
134/// Binding-safe typed resource exhaustion payload.
135#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
137pub struct ResourceExhaustionDetail {
138    pub reason: ResourceExhaustionReason,
139    pub limit: u64,
140    pub observed: u64,
141    pub request_id: Option<u64>,
142}
143
144/// Stable category for a target-preparation plan rejected at its final boundary.
145#[non_exhaustive]
146#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
147#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
148pub enum PreparationStaleReason {
149    Graph,
150    Authority,
151    Staged,
152    Symbols,
153    Semantic,
154    Provider,
155}
156
157impl PreparationStaleReason {
158    pub const fn as_str(self) -> &'static str {
159        match self {
160            Self::Graph => "graph",
161            Self::Authority => "authority",
162            Self::Staged => "staged",
163            Self::Symbols => "symbols",
164            Self::Semantic => "semantic",
165            Self::Provider => "provider",
166        }
167    }
168}
169
170/// Stable category for a reusable recalculation plan rejected before execution.
171#[non_exhaustive]
172#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
173#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
174pub enum PlanStaleReason {
175    Engine,
176    Provider,
177    Semantic,
178    Budget,
179    Staged,
180    Symbols,
181    Authority,
182    SpanGeneration,
183    Graph,
184}
185
186impl PlanStaleReason {
187    pub const fn as_str(self) -> &'static str {
188        match self {
189            Self::Engine => "engine",
190            Self::Provider => "provider",
191            Self::Semantic => "semantic",
192            Self::Budget => "budget",
193            Self::Staged => "staged",
194            Self::Symbols => "symbols",
195            Self::Authority => "authority",
196            Self::SpanGeneration => "span_generation",
197            Self::Graph => "graph",
198        }
199    }
200}
201
202/// Kind-specific payloads (“extension slot”).
203///
204/// Only variants that need extra data get it—rest stay at `None`.
205#[non_exhaustive]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
208pub enum ExcelErrorExtra {
209    /// No additional payload (the vast majority of errors).
210    #[default]
211    None,
212
213    /// `#SPILL!` – information about the intended spill size.
214    Spill {
215        expected_rows: u32,
216        expected_cols: u32,
217    },
218
219    /// Typed resource exhaustion. The canonical Excel error kind remains
220    /// unchanged so existing bindings and formula error handling stay compatible.
221    Resource {
222        detail: Box<ResourceExhaustionDetail>,
223    },
224
225    PreparationStale {
226        reason: PreparationStaleReason,
227    },
228
229    PlanStale {
230        reason: PlanStaleReason,
231    },
232    // --- Add future custom payloads below -------------------------------
233    // AnotherKind { … },
234}
235
236/// The single struct your API passes around.
237///
238/// It combines:
239/// * **kind**   – the mandatory Excel error code
240/// * **message**– optional human explanation
241/// * **context**– generic location†
242/// * **extra**  – optional, kind-specific data
243///
244/// † If you *never* need row/col you can build the value with
245///   `ExcelError::from(kind)`, which sets `context = None`.
246#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
247#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248pub struct ExcelError {
249    pub kind: ExcelErrorKind,
250    pub message: Option<String>,
251    pub context: Option<ErrorContext>,
252    pub extra: ExcelErrorExtra,
253}
254
255/* ───────────────────── Constructors & helpers ─────────────────────── */
256
257impl From<ExcelErrorKind> for ExcelError {
258    fn from(kind: ExcelErrorKind) -> Self {
259        Self {
260            kind,
261            message: None,
262            context: None,
263            extra: ExcelErrorExtra::None,
264        }
265    }
266}
267
268impl ExcelError {
269    /// Basic constructor (no message, no location, no extra).
270    pub fn new(kind: ExcelErrorKind) -> Self {
271        kind.into()
272    }
273
274    /// Attach a human-readable explanation.
275    pub fn with_message<S: Into<String>>(mut self, msg: S) -> Self {
276        self.message = Some(msg.into());
277        self
278    }
279
280    /// Attach generic row/column coordinates.
281    pub fn with_location(mut self, row: u32, col: u32) -> Self {
282        self.context = Some(ErrorContext {
283            row: Some(row),
284            col: Some(col),
285            origin_row: None,
286            origin_col: None,
287            origin_sheet: None,
288        });
289        self
290    }
291
292    /// Attach origin location where the error first occurred.
293    pub fn with_origin(mut self, sheet: Option<String>, row: u32, col: u32) -> Self {
294        if let Some(ref mut ctx) = self.context {
295            ctx.origin_sheet = sheet;
296            ctx.origin_row = Some(row);
297            ctx.origin_col = Some(col);
298        } else {
299            self.context = Some(ErrorContext {
300                row: None,
301                col: None,
302                origin_row: Some(row),
303                origin_col: Some(col),
304                origin_sheet: sheet,
305            });
306        }
307        self
308    }
309
310    /// Attach kind-specific extra data.
311    pub fn with_extra(mut self, extra: ExcelErrorExtra) -> Self {
312        self.extra = extra;
313        self
314    }
315
316    pub fn from_error_string(s: &str) -> Self {
317        match ExcelErrorKind::try_parse(s) {
318            Some(kind) => Self::new(kind),
319            None => {
320                Self::new(ExcelErrorKind::Error).with_message(format!("Unknown error code: {s}"))
321            }
322        }
323    }
324
325    pub fn new_value() -> Self {
326        Self::new(ExcelErrorKind::Value)
327    }
328
329    pub fn new_name() -> Self {
330        Self::new(ExcelErrorKind::Name)
331    }
332
333    pub fn new_div() -> Self {
334        Self::new(ExcelErrorKind::Div)
335    }
336
337    pub fn new_ref() -> Self {
338        Self::new(ExcelErrorKind::Ref)
339    }
340
341    pub fn new_circ() -> Self {
342        Self::new(ExcelErrorKind::Circ)
343    }
344
345    pub fn new_num() -> Self {
346        Self::new(ExcelErrorKind::Num)
347    }
348
349    pub fn new_na() -> Self {
350        Self::new(ExcelErrorKind::Na)
351    }
352}
353
354/* ───────────────────────── Display / Error ────────────────────────── */
355
356impl fmt::Display for ExcelError {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        // Start with the canonical code:
359        write!(f, "{}", self.kind)?;
360
361        // Optional human message.
362        if let Some(ref msg) = self.message {
363            write!(f, ": {msg}")?;
364        }
365
366        // Optional row/col context.
367        if let Some(ref ctx) = self.context {
368            if let (Some(r), Some(c)) = (ctx.row, ctx.col) {
369                write!(f, " (row {r}, col {c})")?;
370            }
371
372            // Show origin if different from the evaluation location
373            if let (Some(or), Some(oc)) = (ctx.origin_row, ctx.origin_col) {
374                if ctx.row != Some(or) || ctx.col != Some(oc) {
375                    if let Some(ref sheet) = ctx.origin_sheet {
376                        write!(f, " [origin: {sheet}!R{or}C{oc}]")?;
377                    } else {
378                        write!(f, " [origin: R{or}C{oc}]")?;
379                    }
380                }
381            }
382        }
383
384        // Optional kind-specific payload - keep it terse for logs.
385        match &self.extra {
386            ExcelErrorExtra::None => {}
387            ExcelErrorExtra::Spill {
388                expected_rows,
389                expected_cols,
390            } => {
391                write!(f, " [spill {expected_rows}×{expected_cols}]")?;
392            }
393            ExcelErrorExtra::Resource { detail } => {
394                write!(
395                    f,
396                    " [resource {} {}/{}]",
397                    detail.reason.as_str(),
398                    detail.observed,
399                    detail.limit
400                )?;
401            }
402            ExcelErrorExtra::PreparationStale { reason } => {
403                write!(f, " [preparation stale {}]", reason.as_str())?;
404            }
405            ExcelErrorExtra::PlanStale { reason } => {
406                write!(f, " [plan stale {}]", reason.as_str())?;
407            }
408        }
409
410        Ok(())
411    }
412}
413
414impl Error for ExcelError {}
415impl From<ExcelError> for String {
416    fn from(error: ExcelError) -> Self {
417        format!("{error}")
418    }
419}
420impl From<ExcelError> for LiteralValue {
421    fn from(error: ExcelError) -> Self {
422        LiteralValue::Error(error)
423    }
424}
425
426impl PartialEq<str> for ExcelErrorKind {
427    fn eq(&self, other: &str) -> bool {
428        format!("{self}") == other
429    }
430}
431
432impl PartialEq<&str> for ExcelError {
433    fn eq(&self, other: &&str) -> bool {
434        self.kind.to_string() == *other
435    }
436}
437
438impl PartialEq<str> for ExcelError {
439    fn eq(&self, other: &str) -> bool {
440        self.kind.to_string() == other
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn parse_known_error_kind() {
450        assert_eq!(ExcelErrorKind::parse("#DIV/0!"), ExcelErrorKind::Div);
451        assert_eq!(ExcelErrorKind::parse("#n/a"), ExcelErrorKind::Na);
452    }
453
454    #[test]
455    fn parse_unknown_error_kind_falls_back() {
456        assert_eq!(ExcelErrorKind::parse("#BOGUS!"), ExcelErrorKind::Error);
457        let err = ExcelError::from_error_string("#BOGUS!");
458        assert_eq!(err.kind, ExcelErrorKind::Error);
459        assert!(err.message.unwrap_or_default().contains("#BOGUS!"));
460    }
461
462    #[test]
463    fn preparation_stale_reason_has_stable_snake_case_names() {
464        assert_eq!(PreparationStaleReason::Graph.as_str(), "graph");
465        assert_eq!(PreparationStaleReason::Authority.as_str(), "authority");
466        assert_eq!(PreparationStaleReason::Staged.as_str(), "staged");
467        assert_eq!(PreparationStaleReason::Symbols.as_str(), "symbols");
468        assert_eq!(PreparationStaleReason::Semantic.as_str(), "semantic");
469        assert_eq!(PreparationStaleReason::Provider.as_str(), "provider");
470    }
471
472    #[test]
473    fn plan_stale_reason_has_stable_snake_case_names() {
474        assert_eq!(PlanStaleReason::Engine.as_str(), "engine");
475        assert_eq!(PlanStaleReason::Provider.as_str(), "provider");
476        assert_eq!(PlanStaleReason::Semantic.as_str(), "semantic");
477        assert_eq!(PlanStaleReason::Budget.as_str(), "budget");
478        assert_eq!(PlanStaleReason::Staged.as_str(), "staged");
479        assert_eq!(PlanStaleReason::Symbols.as_str(), "symbols");
480        assert_eq!(PlanStaleReason::Authority.as_str(), "authority");
481        assert_eq!(PlanStaleReason::SpanGeneration.as_str(), "span_generation");
482        assert_eq!(PlanStaleReason::Graph.as_str(), "graph");
483    }
484}