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}