1use crate::ast::Table;
5use crate::error::StepError;
6use crate::span::Span;
7use crate::value::Value;
8
9#[derive(Clone, Debug, PartialEq)]
12pub struct CellDiff {
13 pub column: String,
14 pub span: Span,
15 pub expected: String,
16 pub actual: String,
17 pub ok: bool,
18 pub expected_value: Option<Value>,
19 pub actual_value: Option<Value>,
20 pub formatted: bool,
21}
22
23impl CellDiff {
24 pub fn new(
26 column: impl Into<String>,
27 span: Span,
28 expected: impl Into<String>,
29 actual: impl Into<String>,
30 ok: bool,
31 ) -> CellDiff {
32 CellDiff {
33 column: column.into(),
34 span,
35 expected: expected.into(),
36 actual: actual.into(),
37 ok,
38 expected_value: None,
39 actual_value: None,
40 formatted: false,
41 }
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct RowCheck {
48 pub column: String,
49 pub value: String,
50 pub span: Span,
51}
52
53impl RowCheck {
54 pub fn new(column: impl Into<String>, value: impl Into<String>, span: Span) -> RowCheck {
55 RowCheck {
56 column: column.into(),
57 value: value.into(),
58 span,
59 }
60 }
61}
62
63pub fn render_cell_value(value: &Value) -> String {
66 match value {
67 Value::String(s) => s.clone(),
68 Value::Int(i) => i.to_string(),
69 Value::Float(d) => format!("{d}"),
70 Value::Bool(b) => b.to_string(),
71 Value::Null => "null".to_string(),
72 Value::List(_) | Value::Map(_) => format!("{value:?}"),
75 }
76}
77
78pub fn compare_row(returned: Option<&Value>, checks: &[RowCheck]) -> Vec<CellDiff> {
81 let Some(Value::Map(obj)) = returned else {
82 return Vec::new();
83 };
84 let mut diffs = Vec::new();
85 for check in checks {
86 let Some(value) = obj.get(&check.column) else {
87 continue;
88 };
89 let actual = render_cell_value(value);
90 let ok = actual == check.value;
91 diffs.push(CellDiff::new(
92 check.column.clone(),
93 check.span,
94 check.value.clone(),
95 actual,
96 ok,
97 ));
98 }
99 diffs
100}
101
102pub fn compare_table(returned: Option<&Value>, input: &Table) -> Result<Vec<CellDiff>, StepError> {
105 let rows = match returned {
106 None => return Ok(Vec::new()),
107 Some(Value::List(rows)) => rows,
108 Some(other) => {
109 return Err(StepError::ReturnShape(format!(
110 "expected a table (array of rows), got {}",
111 other.type_name()
112 )));
113 }
114 };
115 let columns = &input.header.cells;
116 let data_rows = &input.rows;
117 if rows.len() != data_rows.len() {
118 return Err(StepError::ReturnShape(format!(
119 "expected {} row(s), got {}",
120 data_rows.len(),
121 rows.len()
122 )));
123 }
124 let all_arrays = rows.iter().all(|r| matches!(r, Value::List(_)));
125 let all_records = rows.iter().all(|r| matches!(r, Value::Map(_)));
126 if !all_arrays && !all_records {
127 return Err(StepError::ReturnShape(
128 "table rows must be all arrays or all objects".to_string(),
129 ));
130 }
131
132 let mut diffs = Vec::new();
133 for (i, (data_row, ret)) in data_rows.iter().zip(rows).enumerate() {
134 if all_arrays {
135 if let Value::List(cells) = ret {
136 if cells.len() != columns.len() {
137 return Err(StepError::ReturnShape(format!(
138 "row {}: expected {} column(s), got {}",
139 i,
140 columns.len(),
141 cells.len()
142 )));
143 }
144 }
145 }
146 for (j, column) in columns.iter().enumerate() {
147 let actual_value: &Value = if all_arrays {
148 let Value::List(cells) = ret else {
149 unreachable!()
150 };
151 &cells[j]
152 } else {
153 let Value::Map(rec) = ret else { unreachable!() };
154 match rec.get(column) {
155 Some(v) => v,
156 None => {
157 return Err(StepError::ReturnShape(format!(
158 "row {i}: missing column \"{column}\""
159 )));
160 }
161 }
162 };
163 let expected = data_row.cells.get(j).map_or("", |c| c.as_str());
164 let actual = render_cell_value(actual_value);
165 let span = data_row.cell_spans.get(j).copied().unwrap_or(data_row.span);
166 let ok = actual == expected;
167 diffs.push(CellDiff::new(column.clone(), span, expected, actual, ok));
168 }
169 }
170 Ok(diffs)
171}