Skip to main content

ferridriver_bdd/
data_table.rs

1//! DataTable: a structured Gherkin data table with utility methods.
2
3use std::ops::{Deref, DerefMut};
4
5use rustc_hash::FxHashMap;
6
7/// A Gherkin data table (rows of string cells) with helper methods.
8#[derive(Debug, Clone)]
9pub struct DataTable {
10  rows: Vec<Vec<String>>,
11}
12
13impl DataTable {
14  pub fn new(rows: Vec<Vec<String>>) -> Self {
15    Self { rows }
16  }
17
18  pub fn raw(&self) -> &[Vec<String>] {
19    &self.rows
20  }
21
22  pub fn is_empty(&self) -> bool {
23    self.rows.is_empty()
24  }
25
26  pub fn len(&self) -> usize {
27    self.rows.len()
28  }
29
30  /// First row as headers.
31  pub fn headers(&self) -> Option<&[String]> {
32    self.rows.first().map(|r| r.as_slice())
33  }
34
35  /// All rows except the header row.
36  pub fn data_rows(&self) -> &[Vec<String>] {
37    if self.rows.len() > 1 { &self.rows[1..] } else { &[] }
38  }
39
40  /// Convert to array of header→value maps (one per data row).
41  pub fn hashes(&self) -> Vec<FxHashMap<&str, &str>> {
42    let Some(headers) = self.headers() else {
43      return Vec::new();
44    };
45    self
46      .data_rows()
47      .iter()
48      .map(|row| {
49        headers
50          .iter()
51          .zip(row.iter())
52          .map(|(h, v)| (h.as_str(), v.as_str()))
53          .collect()
54      })
55      .collect()
56  }
57
58  /// Convert two-column table to key→value map (first col = key, second col = value).
59  pub fn rows_hash(&self) -> FxHashMap<&str, &str> {
60    self
61      .rows
62      .iter()
63      .filter(|r| r.len() >= 2)
64      .map(|r| (r[0].as_str(), r[1].as_str()))
65      .collect()
66  }
67
68  /// Transpose rows and columns.
69  pub fn transpose(&self) -> DataTable {
70    if self.rows.is_empty() {
71      return DataTable::new(Vec::new());
72    }
73    let max_cols = self.rows.iter().map(|r| r.len()).max().unwrap_or(0);
74    let mut transposed = vec![Vec::with_capacity(self.rows.len()); max_cols];
75    for row in &self.rows {
76      for (col_idx, cell) in row.iter().enumerate() {
77        transposed[col_idx].push(cell.clone());
78      }
79    }
80    DataTable::new(transposed)
81  }
82
83  /// Get a specific cell value.
84  pub fn cell(&self, row: usize, col: usize) -> Option<&str> {
85    self.rows.get(row).and_then(|r| r.get(col)).map(String::as_str)
86  }
87}
88
89impl Deref for DataTable {
90  type Target = [Vec<String>];
91
92  fn deref(&self) -> &[Vec<String>] {
93    &self.rows
94  }
95}
96
97impl DerefMut for DataTable {
98  fn deref_mut(&mut self) -> &mut [Vec<String>] {
99    &mut self.rows
100  }
101}
102
103impl From<Vec<Vec<String>>> for DataTable {
104  fn from(rows: Vec<Vec<String>>) -> Self {
105    Self::new(rows)
106  }
107}
108
109/// Trait for converting a DataTable into typed rows.
110pub trait FromDataTable: Sized {
111  fn from_row(headers: &[String], row: &[String]) -> ferridriver::error::Result<Self>;
112}
113
114impl DataTable {
115  /// Convert data rows to typed structs using the `FromDataTable` trait.
116  pub fn as_type<T: FromDataTable>(&self) -> ferridriver::error::Result<Vec<T>> {
117    let headers = self
118      .headers()
119      .ok_or_else(|| ferridriver::FerriError::invalid_argument("data-table", "table has no header row"))?;
120    self.data_rows().iter().map(|row| T::from_row(headers, row)).collect()
121  }
122}