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`, `DocStringMismatchException`,
3//! `ReturnShapeException`, `UnexpectedPassException`, author `AssertionError`).
4//! `Result`/panic-catch replace throw; `instanceof` dispatch becomes `match`.
5
6use crate::cell_diff::CellDiff;
7use crate::doc_string_diff::DocStringDiff;
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    /// Table / header-bound row mismatch (only the failing cells).
42    CellMismatch(Vec<CellDiff>),
43    /// Doc-string body mismatch.
44    DocStringMismatch(DocStringDiff),
45    /// Wrong return type/shape — an author mistake, not a value diff.
46    ReturnShape(String),
47    /// An `error`-fenced example ran without failing.
48    UnexpectedPass,
49    /// An author-signalled failure (`Err`) or a caught panic.
50    Handler(HandlerError),
51}
52
53impl StepError {
54    /// The human-readable message (`getMessage()` parity).
55    pub fn message(&self) -> String {
56        match self {
57            StepError::CellMismatch(cells) => cells
58                .iter()
59                .map(|c| format!("{}: expected {} but was {}", c.column, c.expected, c.actual))
60                .collect::<Vec<_>>()
61                .join("; "),
62            StepError::DocStringMismatch(diff) => {
63                format!(
64                    "doc string: expected {} but was {}",
65                    quote(&diff.expected),
66                    quote(&diff.actual)
67                )
68            }
69            StepError::ReturnShape(msg) => msg.clone(),
70            StepError::UnexpectedPass => "expected the example to fail, but it passed".to_string(),
71            StepError::Handler(e) => e.message.clone(),
72        }
73    }
74
75    /// The failing cells of a [`StepError::CellMismatch`], else `None`
76    /// (`isCellMismatchException` parity).
77    pub fn as_cell_mismatch(&self) -> Option<&[CellDiff]> {
78        match self {
79            StepError::CellMismatch(cells) => Some(cells),
80            _ => None,
81        }
82    }
83
84    /// The diff of a [`StepError::DocStringMismatch`], else `None`.
85    pub fn as_doc_string_mismatch(&self) -> Option<&DocStringDiff> {
86        match self {
87            StepError::DocStringMismatch(diff) => Some(diff),
88            _ => None,
89        }
90    }
91}
92
93/// Where a failure points in the `.md` — the structural replacement for Java's
94/// synthetic `StackTraceElement` injection.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct FailureLocation {
97    pub label: String,
98    pub path: String,
99    pub line: usize,
100}
101
102/// A caught step failure plus its (optional) source location.
103#[derive(Clone, Debug, PartialEq)]
104pub struct StepFailure {
105    pub error: StepError,
106    pub location: Option<FailureLocation>,
107}
108
109impl StepFailure {
110    /// A failure with no attached location (fallback-line path).
111    pub fn bare(error: StepError) -> StepFailure {
112        StepFailure {
113            error,
114            location: None,
115        }
116    }
117}
118
119/// Mirrors `JSON.stringify`'s quoting of the TS doc-string error message closely
120/// enough for a human-readable message (never parsed back).
121/// Renders `s` the way `JSON.stringify` does in the TypeScript port.
122///
123/// Every port quotes doc-string mismatch messages identically because the text is
124/// matched by substring in an `error` fence — a port that quotes differently fails
125/// a spec its siblings pass. Escaping only `\\`, `"` and `\n` is not enough: doc
126/// strings routinely carry tab-indented code.
127fn quote(s: &str) -> String {
128    let mut out = String::with_capacity(s.len() + 2);
129    out.push('"');
130    for c in s.chars() {
131        match c {
132            '\\' => out.push_str("\\\\"),
133            '"' => out.push_str("\\\""),
134            '\n' => out.push_str("\\n"),
135            '\r' => out.push_str("\\r"),
136            '\t' => out.push_str("\\t"),
137            '\u{08}' => out.push_str("\\b"),
138            '\u{0c}' => out.push_str("\\f"),
139            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
140            c => out.push(c),
141        }
142    }
143    out.push('"');
144    out
145}
146
147/// A registration-time (author-wiring) error — never a step failure.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub enum RegistryError {
150    /// A duplicate step expression; the message lists both source positions.
151    DuplicateStep(String),
152    /// The cucumber expression failed to compile (e.g. undefined parameter type).
153    Expression(String),
154}