Skip to main content

varar_core/
cell_diff.rs

1//! Table row/cell comparison — port of `cell-diff.ts` / `CellDiff.java`.
2//! `Object` + `instanceof Map`/`List` duck-typing becomes matching on [`Value`].
3
4use crate::ast::Table;
5use crate::error::StepError;
6use crate::span::Span;
7use crate::value::Value;
8
9/// The verdict for one checked column: expected vs actual, plus raw values and
10/// whether a parameter-type `format` produced `actual` (inline-parameter path).
11#[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    /// The five-component form (row/table paths): raw values `None`, not formatted.
25    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/// One checked column of one header-bound row.
46#[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
63/// Display rules 2–4 of the mismatch-rendering chain: a string as-is, anything
64/// else a best-effort stringification.
65pub 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        // Port-native fallback (deliberately outside conformance): a bundle that
73        // pins an object-valued actual must give the parameter type a `format`.
74        Value::List(_) | Value::Map(_) => format!("{value:?}"),
75    }
76}
77
78/// Compares a row step's returned map against the row's cells. Only columns
79/// present on `returned` are checked; a non-map/`None` return checks nothing.
80pub 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
102/// Compares a whole-table step's returned table against the input table. `None`
103/// checks nothing; type/shape problems return [`StepError::ReturnShape`].
104pub 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}