Skip to main content

varar_core/
error.rs

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