Skip to main content

varar_core/
result.rs

1//! Immutable run-result records — port of `result.ts` / `Result.java`. The
2//! persisted `.varar/<oath>.json` file is a serialized [`OathResults`].
3
4use crate::json_escape;
5use std::fmt::Write;
6
7/// One mismatched CELL as a source-offset range plus the runtime value.
8/// `from`/`to` are absolute UTF-16 source offsets; `to` is exclusive.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct CellFailure {
11    pub from: usize,
12    pub to: usize,
13    pub actual: String,
14}
15
16impl CellFailure {
17    pub fn new(from: usize, to: usize, actual: impl Into<String>) -> CellFailure {
18        CellFailure {
19            from,
20            to,
21            actual: actual.into(),
22        }
23    }
24}
25
26/// An example's run outcome.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Status {
29    Passed,
30    Failed,
31}
32
33/// Where a failure points in the source: an offset range, `to` exclusive. The
34/// failing step's match span, or the first mismatched cell's span (the
35/// [`crate::failure_anchor`] rule). This is what lets a renderer underline the
36/// step that failed rather than the whole line it sits on.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct AnchorRange {
39    pub from: usize,
40    pub to: usize,
41}
42
43/// The failure payload of a failed [`ExampleResult`]. `cells` is `None`
44/// when not applicable. `line` may be a caller-supplied fallback (`-1`).
45/// `anchor` is `None` when the failure carries no location for this oath —
46/// optional for the same reason `cells` is, and a renderer then falls back to
47/// `line`.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ExampleFailure {
50    pub line: i64,
51    pub message: String,
52    pub stack: String,
53    pub cells: Option<Vec<CellFailure>>,
54    pub anchor: Option<AnchorRange>,
55}
56
57/// The run result for one BDD example.
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct ExampleResult {
60    pub name: String,
61    pub status: Status,
62    pub lines: Vec<usize>,
63    pub failure: Option<ExampleFailure>,
64}
65
66/// The persisted run result for one oath file.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct OathResults {
69    pub version: u32,
70    pub oath_path: String,
71    pub source_hash: String,
72    pub examples: Vec<ExampleResult>,
73}
74
75/// Projects [`OathResults`] onto the JSON of `.varar/<oath_path>.json` (ADR 0014):
76/// the TypeScript field names, declaration order, 2-space indent, optional
77/// members absent rather than null. No trailing newline — the writer adds it.
78///
79/// Written by hand because the reference implementation writes the payload in
80/// declaration order, and this file is read by humans diffing it as much as by
81/// the language server.
82pub fn to_wire_json(results: &OathResults) -> String {
83    let mut out = String::new();
84    out.push_str("{\n");
85    field(&mut out, 1, "version", &results.version.to_string(), true);
86    string_field(&mut out, 1, "oathPath", &results.oath_path, true);
87    string_field(&mut out, 1, "sourceHash", &results.source_hash, true);
88    indent(&mut out, 1);
89    out.push_str("\"examples\": ");
90    write_examples(&mut out, &results.examples, 1);
91    out.push('\n');
92    out.push('}');
93    out
94}
95
96fn write_examples(out: &mut String, examples: &[ExampleResult], depth: usize) {
97    if examples.is_empty() {
98        out.push_str("[]");
99        return;
100    }
101    out.push_str("[\n");
102    for (i, example) in examples.iter().enumerate() {
103        indent(out, depth + 1);
104        out.push_str("{\n");
105        string_field(out, depth + 2, "name", &example.name, true);
106        let status = match example.status {
107            Status::Passed => "passed",
108            Status::Failed => "failed",
109        };
110        string_field(out, depth + 2, "status", status, true);
111        indent(out, depth + 2);
112        out.push_str("\"lines\": ");
113        write_ints(out, &example.lines, depth + 2);
114        match &example.failure {
115            Some(failure) => {
116                out.push_str(",\n");
117                indent(out, depth + 2);
118                out.push_str("\"failure\": ");
119                write_failure(out, failure, depth + 2);
120                out.push('\n');
121            }
122            None => out.push('\n'),
123        }
124        indent(out, depth + 1);
125        out.push('}');
126        if i + 1 < examples.len() {
127            out.push(',');
128        }
129        out.push('\n');
130    }
131    indent(out, depth);
132    out.push(']');
133}
134
135fn write_failure(out: &mut String, failure: &ExampleFailure, depth: usize) {
136    out.push_str("{\n");
137    field(out, depth + 1, "line", &failure.line.to_string(), true);
138    string_field(out, depth + 1, "message", &failure.message, true);
139    let has_more = failure.cells.is_some() || failure.anchor.is_some();
140    string_field(out, depth + 1, "stack", &failure.stack, has_more);
141    if let Some(cells) = &failure.cells {
142        indent(out, depth + 1);
143        out.push_str("\"cells\": [\n");
144        for (i, cell) in cells.iter().enumerate() {
145            indent(out, depth + 2);
146            out.push_str("{\n");
147            field(out, depth + 3, "from", &cell.from.to_string(), true);
148            field(out, depth + 3, "to", &cell.to.to_string(), true);
149            string_field(out, depth + 3, "actual", &cell.actual, false);
150            indent(out, depth + 2);
151            out.push('}');
152            if i + 1 < cells.len() {
153                out.push(',');
154            }
155            out.push('\n');
156        }
157        indent(out, depth + 1);
158        out.push(']');
159        out.push_str(if failure.anchor.is_some() {
160            ",\n"
161        } else {
162            "\n"
163        });
164    }
165    if let Some(anchor) = &failure.anchor {
166        indent(out, depth + 1);
167        out.push_str("\"anchor\": {\n");
168        field(out, depth + 2, "from", &anchor.from.to_string(), true);
169        field(out, depth + 2, "to", &anchor.to.to_string(), false);
170        indent(out, depth + 1);
171        out.push_str("}\n");
172    }
173    indent(out, depth);
174    out.push('}');
175}
176
177fn write_ints(out: &mut String, values: &[usize], depth: usize) {
178    if values.is_empty() {
179        out.push_str("[]");
180        return;
181    }
182    out.push_str("[\n");
183    for (i, value) in values.iter().enumerate() {
184        indent(out, depth + 1);
185        let _ = write!(out, "{value}");
186        if i + 1 < values.len() {
187            out.push(',');
188        }
189        out.push('\n');
190    }
191    indent(out, depth);
192    out.push(']');
193}
194
195fn field(out: &mut String, depth: usize, key: &str, raw_value: &str, comma: bool) {
196    indent(out, depth);
197    let _ = write!(out, "\"{key}\": {raw_value}");
198    out.push_str(if comma { ",\n" } else { "\n" });
199}
200
201fn string_field(out: &mut String, depth: usize, key: &str, value: &str, comma: bool) {
202    indent(out, depth);
203    let _ = write!(out, "\"{key}\": ");
204    json_escape::write_string(out, value);
205    out.push_str(if comma { ",\n" } else { "\n" });
206}
207
208fn indent(out: &mut String, depth: usize) {
209    for _ in 0..depth {
210        out.push_str("  ");
211    }
212}