1use crate::json_escape;
5use std::fmt::Write;
6
7#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Status {
29 Passed,
30 Failed,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct AnchorRange {
39 pub from: usize,
40 pub to: usize,
41}
42
43#[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#[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#[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
75pub 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}