smartsheet_rs/
helpers.rs

1//! Public helper utilities
2//!
3use crate::models::{Cell, CellValue, Column, IndexResult, Row, Sheet};
4use crate::types::Result;
5
6use std::collections::HashMap;
7use std::io::{Error, ErrorKind};
8
9/// Define type aliases for the column mappings so that we can DRY.
10pub type ColumnNameToId<'a> = HashMap<&'a str, u64>;
11pub type ColumnIdToName<'a> = HashMap<u64, &'a str>;
12pub type ColumnNameToCell<'a> = HashMap<&'a str, &'a Cell>;
13
14/// **Column Mapper** - Utility to generate the `name` <-> `id` mappings for
15/// columns in a sheet.
16///
17pub struct ColumnMapper<'a> {
18    /// Represents a mapping of *Column Name* to *Column ID*
19    ///
20    /// Note that the ID value is unique, internal, and used mainly in the
21    /// Smartsheet API.
22    pub name_to_id: ColumnNameToId<'a>,
23    /// Represents a mapping of *Column ID* to *Column Name*
24    ///
25    /// Note that the ID value is unique, internal, and used mainly in the
26    /// Smartsheet API.
27    pub id_to_name: ColumnIdToName<'a>,
28}
29
30impl<'a> From<&'a Sheet> for ColumnMapper<'a> {
31    fn from(sheet_ref: &'a Sheet) -> Self {
32        Self::new(&sheet_ref.columns)
33    }
34}
35
36impl<'a> From<&'a Row> for ColumnMapper<'a> {
37    fn from(row_ref: &'a Row) -> Self {
38        Self::new(&row_ref.columns)
39    }
40}
41
42impl<'a> From<&'a IndexResult<Column>> for ColumnMapper<'a> {
43    fn from(index_ref: &'a IndexResult<Column>) -> Self {
44        Self::new(&index_ref.data)
45    }
46}
47
48impl<'a> ColumnMapper<'a> {
49    /// Create a new `ColumnMapper` object from a list of `columns`.
50    pub fn new(columns: &'a [Column]) -> Self {
51        let (name_to_id, id_to_name) = Self::get_mappings(columns);
52
53        Self {
54            name_to_id,
55            id_to_name,
56        }
57    }
58
59    /// Retrieve the `name` <-> `id` mappings for *columns* in a sheet.
60    fn get_mappings(columns: &'a [Column]) -> (ColumnNameToId, ColumnIdToName) {
61        let num_columns = columns.len();
62        if num_columns == 0 {
63            // TODO maybe don't panic
64            panic!("No column data for the Row - please ensure you call `get_row_with_column_data()` *or* \
65        pass `RowIncludeFlags::Columns` as an `include` argument to `get_row_with_params()`")
66        }
67
68        let mut name_to_id: ColumnNameToId<'a> = HashMap::with_capacity(num_columns);
69        let mut id_to_name: ColumnIdToName<'a> = HashMap::with_capacity(num_columns);
70
71        for c in columns {
72            let title = &c.title;
73
74            name_to_id.insert(title, c.id);
75            id_to_name.insert(c.id, title);
76        }
77
78        (name_to_id, id_to_name)
79    }
80}
81
82/// **Cell Getter** - Utility to make it easier to retrieve a `Cell` from a
83/// `Row` object.
84///
85pub struct CellGetter<'a> {
86    column_name_to_id: &'a ColumnNameToId<'a>,
87    id_to_column_name: &'a ColumnIdToName<'a>,
88}
89
90impl<'a> CellGetter<'a> {
91    /// Create a new `CellGetter` from a reference to a `ColumnMapper` object
92    pub fn new(columns: &'a ColumnMapper<'a>) -> Self {
93        Self {
94            column_name_to_id: &columns.name_to_id,
95            id_to_column_name: &columns.id_to_name,
96        }
97    }
98
99    /// Create a new `CellGetter` from a reference to a `ColumnMapper` object
100    pub fn from_mapper(columns: &'a ColumnMapper<'a>) -> Self {
101        Self::new(columns)
102    }
103
104    /// Create a new `CellGetter` from a reference to a mapping of *column name*
105    /// to *column id*
106    ///
107    /// NOTE: Disabling this for now, because I can't get the lifetimes to work.
108    // pub fn from_name_to_id<'b>(column_name_to_id: &'b ColumnNameToId<'b>) -> CellGetter<'a> {
109    //     let mut id_to_column_name: ColumnIdToName<'b> =
110    //         HashMap::with_capacity(column_name_to_id.len());
111    //     for (&name, &id) in column_name_to_id {
112    //         id_to_column_name.insert(id, name);
113    //     }
114    //
115    //     Self {
116    //         column_name_to_id,
117    //         id_to_column_name: &id_to_column_name,
118    //     }
119    // }
120
121    /// Find a `cell` in a `row` by its *column name*.
122    pub fn by_name<'b>(&'a self, row: &'a Row, name: &'b str) -> Result<&Cell> {
123        match self.column_name_to_id.get(name) {
124            Some(&col_id) => row.get_cell_by_id(col_id),
125            None => Err(Box::from(Error::new(
126                ErrorKind::InvalidInput,
127                format!("A column named `{}` was not found in the Sheet", name),
128            ))),
129        }
130    }
131
132    /// Find a `cell` in a `row` by its *column id*.
133    pub fn by_id(&'a self, row: &'a Row, column_id: u64) -> Result<&Cell> {
134        row.get_cell_by_id(column_id)
135    }
136
137    /// Retrieve a mapping of *column name* to `Cell` object.
138    ///
139    /// Note: this is likely more efficient when multiple `Cell`s are to be
140    /// retrieved from an individual `Row`.
141    pub fn name_to_cell(&'a self, row: &'a Row) -> ColumnNameToCell<'a> {
142        let mut col_name_to_cell: ColumnNameToCell<'a> = HashMap::with_capacity(row.cells.len());
143
144        for cell in &row.cells {
145            if let Some(&col_name) = self.id_to_column_name.get(&cell.column_id) {
146                col_name_to_cell.insert(col_name, cell);
147            }
148        }
149
150        col_name_to_cell
151    }
152}
153
154/// **Row Getter** - Utility to make it easier to find and retrieve row(s)
155/// in an array of `Row` objects, based on a predicate or a pre-defined
156/// filter condition.
157///
158/// For example, a common use case is finding a `Row` where a cell is
159/// *equal* to a specific value.
160///
161pub struct RowGetter<'a> {
162    /// Represents an array of *Row* objects that we want to run a search on.
163    pub rows: &'a [Row],
164    /// Represents a mapping of *Column Name* to *Column ID*
165    ///
166    /// Note that the ID value is unique, internal, and used mainly in the
167    /// Smartsheet API.
168    pub column_name_to_id: &'a ColumnNameToId<'a>,
169}
170
171impl<'a> RowGetter<'a> {
172    /// Create a new `RowGetter` from a reference to a `ColumnMapper` object
173    pub fn new(rows: &'a [Row], columns: &'a ColumnMapper<'a>) -> RowGetter<'a> {
174        Self {
175            rows,
176            column_name_to_id: &columns.name_to_id,
177        }
178    }
179
180    /// Uses an **equals (eq)** condition to compare a cell for a *Column
181    /// Name* against a specified *Value*.
182    pub fn where_eq<V: Into<CellValue>>(
183        &'a self,
184        column_name: &'a str,
185        value: V,
186    ) -> Result<RowFinder<'a>> {
187        RowFinder::new(
188            self.rows,
189            self.column_name_to_id,
190            column_name,
191            value.into(),
192            Comp::EQ,
193        )
194    }
195
196    /// Uses an **equals (eq)** condition to compare a cell for a *Column
197    /// ID* against a specified *Value*.
198    pub fn where_eq_by_id<V: Into<CellValue>>(&'a self, column_id: u64, value: V) -> RowFinder<'a> {
199        RowFinder::new_by_id(self.rows, column_id, value.into(), Comp::EQ)
200    }
201
202    /// Uses a **not equals (ne)** condition to compare a cell for a *Column
203    /// Name* against a specified *Value*.
204    pub fn where_ne<V: Into<CellValue>>(
205        &'a self,
206        column_name: &'a str,
207        value: V,
208    ) -> Result<RowFinder<'a>> {
209        RowFinder::new(
210            self.rows,
211            self.column_name_to_id,
212            column_name,
213            value.into(),
214            Comp::NE,
215        )
216    }
217
218    /// Uses a **not equals (ne)** condition to compare a cell for a *Column
219    /// ID* against a specified *Value*.
220    pub fn where_ne_by_id<V: Into<CellValue>>(&'a self, column_id: u64, value: V) -> RowFinder<'a> {
221        RowFinder::new_by_id(self.rows, column_id, value.into(), Comp::NE)
222    }
223}
224
225/// Enum which represents a Comparison operator or a Search Criteria
226pub enum Comp {
227    EQ,
228    NE,
229}
230
231impl Comp {
232    /// Return a closure which compares two `CellValue`s to determine
233    /// equality.
234    pub fn get_cell_comparator<'a>(&'a self) -> fn(&'a CellValue, &'a CellValue) -> bool {
235        match self {
236            Comp::EQ => |v1: &'a CellValue, v2: &'a CellValue| v1 == v2,
237            Comp::NE => |v1: &'a CellValue, v2: &'a CellValue| v1 != v2,
238        }
239    }
240}
241
242/// **Row Finder**: Find row(s) in an array of `Row`s that match a pre-defined
243/// condition.
244///
245/// # Note
246/// It's preferable to use the [`RowGetter`] implementation instead, as
247/// that's a little easier to work with.
248///
249pub struct RowFinder<'a> {
250    /// Represents an array of *Row* objects that we want to run a search on.
251    pub rows: &'a [Row],
252    /// Column Id to filter the value by.
253    column_id: u64,
254    /// Value to filter by.
255    value: CellValue,
256    /// Determines how we intend to compare cell value against `value`.
257    cmp: Comp,
258}
259
260impl<'a> RowFinder<'a> {
261    /// Create a new `RowFinder`.
262    ///
263    /// # Note
264    /// It's preferable to use the [`RowGetter`] implementation instead, as
265    /// that's a little easier to work with.
266    ///
267    pub fn new(
268        rows: &'a [Row],
269        column_name_to_id: &'a ColumnNameToId<'a>,
270        column_name: &'a str,
271        value: CellValue,
272        cmp: Comp,
273    ) -> Result<Self> {
274        let column_id = match column_name_to_id.get(column_name) {
275            Some(&v) => v,
276            None => {
277                return Err(Box::from(Error::new(
278                    ErrorKind::NotFound,
279                    format!(
280                        "The column name `{}` does not exist in the sheet",
281                        column_name
282                    ),
283                )));
284            }
285        };
286
287        Ok(Self::new_by_id(rows, column_id, value, cmp))
288    }
289
290    /// Create a new `RowFinder` by *Column Id* instead of *Column Name*.
291    pub fn new_by_id(rows: &'a [Row], column_id: u64, value: CellValue, cmp: Comp) -> Self {
292        Self {
293            rows,
294            column_id,
295            value,
296            cmp,
297        }
298    }
299
300    /// Find the *first* `Row` matching a specified condition.
301    pub fn first<'b>(&'b self) -> Result<&'a Row> {
302        let cmp = self.cmp.get_cell_comparator();
303
304        return match self.rows.iter().find(|row| {
305            if let Ok(cell) = row.get_cell_by_id(self.column_id) {
306                matches!(&cell.value, Some(cv) if cmp(&self.value, cv))
307            } else {
308                false
309            }
310        }) {
311            Some(row) => Ok(row),
312            None => Err(Box::from(Error::new(
313                ErrorKind::NotFound,
314                "No matching row for the condition",
315            ))),
316        };
317    }
318
319    /// Find *all* `Row`s matching a specified condition.
320    pub fn find_all<'b>(&'b self) -> Result<Vec<&'a Row>> {
321        let cmp = self.cmp.get_cell_comparator();
322
323        Ok(self
324            .rows
325            .iter()
326            .filter(|row| {
327                if let Ok(cell) = row.get_cell_by_id(self.column_id) {
328                    matches!(&cell.value, Some(cv) if cmp(&self.value, cv))
329                } else {
330                    false
331                }
332            })
333            .collect::<Vec<_>>())
334    }
335}