Skip to main content

varar_core/
error.rs

1//! The error model: the Rust replacement for Java varar-core's typed exception
2//! hierarchy (`CellMismatchException`,
3//! `ReturnShapeException`, `UnexpectedPassException`, author `AssertionError`).
4//! `Result`/panic-catch replace throw; `instanceof` dispatch becomes `match`.
5
6use crate::cell_diff::CellDiff;
7use crate::result::AnchorRange;
8use std::any::Any;
9
10/// A handler-signalled failure (author `Err(...)` or a caught panic) — the
11/// analog of an arbitrary thrown `RuntimeException`/`AssertionError`.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct HandlerError {
14    pub message: String,
15}
16
17impl HandlerError {
18    pub fn new(message: impl Into<String>) -> HandlerError {
19        HandlerError {
20            message: message.into(),
21        }
22    }
23
24    /// Extracts a message from a `catch_unwind` panic payload (`&str`/`String`,
25    /// else a generic fallback).
26    pub fn from_panic(payload: Box<dyn Any + Send>) -> HandlerError {
27        let message = if let Some(s) = payload.downcast_ref::<&str>() {
28            (*s).to_string()
29        } else if let Some(s) = payload.downcast_ref::<String>() {
30            s.clone()
31        } else {
32            "handler panicked".to_string()
33        };
34        HandlerError { message }
35    }
36}
37
38/// A step failure verdict — the closed union replacing the exception hierarchy.
39#[derive(Clone, Debug, PartialEq)]
40pub enum StepError {
41    /// One or more compared cells differ — an inline capture, a table cell, a
42    /// header-bound row's cell, or a doc string (only the failing cells).
43    CellMismatch(Vec<CellDiff>),
44    /// Wrong return type/shape — an author mistake, not a value diff.
45    ReturnShape(String),
46    /// An `error`-fenced example ran without failing.
47    UnexpectedPass,
48    /// An author-signalled failure (`Err`) or a caught panic.
49    Handler(HandlerError),
50}
51
52impl StepError {
53    /// The human-readable message (`getMessage()` parity).
54    pub fn message(&self) -> String {
55        match self {
56            StepError::CellMismatch(cells) => cells
57                .iter()
58                .map(|c| format!("{}: expected {} but was {}", c.column, c.expected, c.actual))
59                .collect::<Vec<_>>()
60                .join("; "),
61            StepError::ReturnShape(msg) => msg.clone(),
62            StepError::UnexpectedPass => "expected the example to fail, but it passed".to_string(),
63            StepError::Handler(e) => e.message.clone(),
64        }
65    }
66
67    /// The failing cells of a [`StepError::CellMismatch`], else `None`
68    /// (`isCellMismatchException` parity).
69    pub fn as_cell_mismatch(&self) -> Option<&[CellDiff]> {
70        match self {
71            StepError::CellMismatch(cells) => Some(cells),
72            _ => None,
73        }
74    }
75}
76
77/// Where a failure points in the `.md` — the structural replacement for Java's
78/// synthetic `StackTraceElement` injection. `line` is the anchor's start line
79/// (all a rendered stack frame can show); `anchor` is its full offset range, so
80/// a renderer can underline the failing step instead of its whole line.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct FailureLocation {
83    pub label: String,
84    pub path: String,
85    pub line: usize,
86    pub anchor: AnchorRange,
87}
88
89/// A caught step failure plus its (optional) source location.
90#[derive(Clone, Debug, PartialEq)]
91pub struct StepFailure {
92    pub error: StepError,
93    pub location: Option<FailureLocation>,
94}
95
96impl StepFailure {
97    /// A failure with no attached location (fallback-line path).
98    pub fn bare(error: StepError) -> StepFailure {
99        StepFailure {
100            error,
101            location: None,
102        }
103    }
104}
105
106/// Mirrors `JSON.stringify`'s quoting of the TS doc-string error message closely
107/// enough for a human-readable message (never parsed back).
108/// Renders `s` the way `JSON.stringify` does in the TypeScript port.
109///
110/// Every port quotes doc-string mismatch messages identically because the text is
111/// matched by substring in an `error` fence — a port that quotes differently fails
112/// an oath its siblings pass. Escaping only `\\`, `"` and `\n` is not enough: doc
113/// strings routinely carry tab-indented code.
114pub(crate) fn quote(s: &str) -> String {
115    let mut out = String::with_capacity(s.len() + 2);
116    out.push('"');
117    for c in s.chars() {
118        match c {
119            '\\' => out.push_str("\\\\"),
120            '"' => out.push_str("\\\""),
121            '\n' => out.push_str("\\n"),
122            '\r' => out.push_str("\\r"),
123            '\t' => out.push_str("\\t"),
124            '\u{08}' => out.push_str("\\b"),
125            '\u{0c}' => out.push_str("\\f"),
126            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
127            c => out.push(c),
128        }
129    }
130    out.push('"');
131    out
132}
133
134/// A registration-time (author-wiring) error — never a step failure.
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub enum RegistryError {
137    /// A duplicate step expression; the message lists both source positions.
138    DuplicateStep(String),
139    /// The cucumber expression failed to compile (e.g. undefined parameter type).
140    Expression(String),
141}