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