Skip to main content

csv_cat/
row.rs

1//! Row: a newtype over a CSV record.
2
3use crate::error::CsvError;
4
5/// A single row from a CSV file.
6///
7/// Wraps a `csv::StringRecord` with accessor methods.
8/// All fields are private per CLAUDE.md.
9#[derive(Debug, Clone)]
10pub struct Row {
11    record: csv::StringRecord,
12}
13
14impl Row {
15    /// Wrap a raw `StringRecord`.
16    #[must_use]
17    pub(crate) fn from_record(record: csv::StringRecord) -> Self {
18        Self { record }
19    }
20
21    /// Get a field by index.
22    ///
23    /// # Errors
24    ///
25    /// Returns `CsvError::MissingField` if the index is out of bounds.
26    pub fn get(&self, index: usize) -> Result<&str, CsvError> {
27        self.record.get(index)
28            .ok_or(CsvError::MissingField { index })
29    }
30
31    /// Number of fields in this row.
32    #[must_use]
33    pub fn len(&self) -> usize {
34        self.record.len()
35    }
36
37    /// Whether this row has zero fields.
38    #[must_use]
39    pub fn is_empty(&self) -> bool {
40        self.record.is_empty()
41    }
42
43    /// Iterate over all fields as string slices.
44    pub fn fields(&self) -> impl Iterator<Item = &str> {
45        self.record.iter()
46    }
47
48    /// Deserialize this row into a typed value.
49    ///
50    /// Requires that the type implements `serde::Deserialize` and
51    /// that headers have been provided.
52    ///
53    /// # Errors
54    ///
55    /// Returns `CsvError::Deserialize` if the record cannot be
56    /// deserialized into the target type.
57    pub fn deserialize<'de, T: serde::Deserialize<'de>>(
58        &'de self,
59        headers: Option<&'de csv::StringRecord>,
60    ) -> Result<T, CsvError> {
61        self.record.deserialize(headers)
62            .map_err(|e| CsvError::Deserialize(e.to_string()))
63    }
64
65    /// Convert to a `Vec<String>` of all fields.
66    #[must_use]
67    pub fn to_vec(&self) -> Vec<String> {
68        self.record.iter().map(String::from).collect()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn sample_row() -> Row {
77        let record = csv::StringRecord::from(vec!["alice", "30", "seattle"]);
78        Row::from_record(record)
79    }
80
81    #[test]
82    fn get_returns_field_by_index() -> Result<(), CsvError> {
83        let row = sample_row();
84        assert_eq!(row.get(0)?, "alice");
85        assert_eq!(row.get(1)?, "30");
86        assert_eq!(row.get(2)?, "seattle");
87        Ok(())
88    }
89
90    #[test]
91    fn get_out_of_bounds_returns_error() {
92        let row = sample_row();
93        assert!(row.get(99).is_err());
94    }
95
96    #[test]
97    fn len_and_is_empty() {
98        let row = sample_row();
99        assert_eq!(row.len(), 3);
100        assert!(!row.is_empty());
101
102        let empty = Row::from_record(csv::StringRecord::new());
103        assert!(empty.is_empty());
104    }
105
106    #[test]
107    fn to_vec_collects_all_fields() {
108        let row = sample_row();
109        assert_eq!(row.to_vec(), vec!["alice", "30", "seattle"]);
110    }
111}