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}