Skip to main content

vantage_table/table/
row_fields.rs

1//! The fields a table's returned rows carry, and the in-memory filter that
2//! relies on them.
3
4use indexmap::IndexMap;
5use vantage_types::{Entity, Record};
6
7use crate::{
8    column::flags::ColumnFlag,
9    table::Table,
10    traits::{column_like::ColumnLike, table_source::TableSource},
11};
12
13impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
14    /// The declared columns a returned row carries, in declaration order:
15    /// every column except those flagged
16    /// [`RequestParam`](ColumnFlag::RequestParam), whose values go to the
17    /// backend rather than come back from it. A `Hidden` column is still a
18    /// row field.
19    pub fn row_fields(&self) -> Vec<String> {
20        self.columns()
21            .iter()
22            .filter(|(_, column)| !column.flags().contains(&ColumnFlag::RequestParam))
23            .map(|(name, _)| name.clone())
24            .collect()
25    }
26}
27
28/// Keep the rows that match every `(field, wanted)` equality filter, for a
29/// backend that filters fetched rows in memory.
30///
31/// A row holding `field` passes when `eq(value, wanted)`. A row without it
32/// fails when `field` is one of `row_fields` (see [`Table::row_fields`]):
33/// rows were promised to carry it. Any other field (a `RequestParam` column,
34/// a URL placeholder, a parameter the table doesn't declare) went to the
35/// backend, which applied it without returning it, so its absence passes.
36pub fn retain_matching<V, W: ?Sized>(
37    rows: &mut IndexMap<String, Record<V>>,
38    filters: &[(&str, &W)],
39    row_fields: &[String],
40    eq: impl Fn(&V, &W) -> bool,
41) {
42    rows.retain(|_, row| {
43        filters.iter().all(|(field, wanted)| match row.get(*field) {
44            Some(value) => eq(value, wanted),
45            None => !row_fields.iter().any(|f| f == field),
46        })
47    });
48}
49
50#[cfg(test)]
51mod tests {
52    use indexmap::IndexMap;
53    use vantage_types::{EmptyEntity, Record};
54
55    use super::retain_matching;
56    use crate::column::flags::ColumnFlag;
57    use crate::mocks::{mock_column::MockColumn, mock_table_source::MockTableSource};
58    use crate::table::Table;
59
60    #[test]
61    fn a_missing_field_fails_only_when_rows_carry_it() {
62        let row = |pairs: &[(&str, i64)]| -> Record<i64> {
63            pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
64        };
65        let mut rows: IndexMap<String, Record<i64>> = [
66            ("a".to_string(), row(&[("size", 1)])),
67            ("b".to_string(), row(&[("size", 2)])),
68            ("c".to_string(), row(&[])),
69        ]
70        .into_iter()
71        .collect();
72        let row_fields = vec!["size".to_string()];
73
74        // `prefix` is no row field: its absence passes.
75        retain_matching(&mut rows, &[("prefix", &9)], &row_fields, |a, b| a == b);
76        assert_eq!(rows.len(), 3);
77
78        // `size` is: the row without it fails, as does the mismatch.
79        retain_matching(&mut rows, &[("size", &1)], &row_fields, |a, b| a == b);
80        assert_eq!(rows.keys().collect::<Vec<_>>(), ["a"]);
81    }
82
83    #[test]
84    fn row_fields_leave_out_request_params_but_keep_hidden_columns() {
85        let table = Table::<MockTableSource, EmptyEntity>::new("buckets", MockTableSource::new())
86            .with_column(MockColumn::<String>::new("region").with_flag(ColumnFlag::RequestParam))
87            .with_column(MockColumn::<String>::new("name"))
88            .with_column(MockColumn::<String>::new("owner").with_flag(ColumnFlag::Hidden));
89        assert_eq!(table.row_fields(), ["name", "owner"]);
90    }
91}