1#[derive(Debug, Clone)]
2pub struct SourceRow {
3 columns: Vec<String>,
4 values: Vec<String>,
5}
6
7impl SourceRow {
8 pub fn new(headers: &[String], values: Vec<String>) -> Self {
9 Self {
10 columns: headers.to_vec(),
11 values,
12 }
13 }
14
15 pub fn from_csv_line(line: &str) -> Self {
16 let mut columns = Vec::new();
17 let mut values = Vec::new();
18 for part in line.split(',') {
19 let part = part.trim_ascii();
20 values.push(part.to_string());
21 if columns.len() < values.len() {
22 columns.push(format!("col_{}", columns.len()));
23 }
24 }
25 Self { columns, values }
26 }
27
28 pub fn from_pairs(pairs: Vec<(String, String)>) -> Self {
29 let mut columns = Vec::with_capacity(pairs.len());
30 let mut values = Vec::with_capacity(pairs.len());
31 for (k, v) in pairs {
32 columns.push(k);
33 values.push(v);
34 }
35 Self { columns, values }
36 }
37
38 pub fn get(&self, column: &str) -> Option<&str> {
39 let idx = self.columns.iter().position(|c| c == column)?;
40 self.values.get(idx).map(|s| s.as_str())
41 }
42
43 pub fn get_or(&self, column: &str, default: &str) -> String {
44 self.get(column)
45 .map(|s| s.to_string())
46 .unwrap_or_else(|| default.to_string())
47 }
48
49 pub fn columns(&self) -> &[String] {
50 &self.columns
51 }
52
53 pub fn values(&self) -> &[String] {
54 &self.values
55 }
56
57 pub fn to_map(&self) -> std::collections::HashMap<String, String> {
58 let mut map = std::collections::HashMap::with_capacity(self.columns.len());
59 for (i, col) in self.columns.iter().enumerate() {
60 if let Some(v) = self.values.get(i) {
61 map.insert(col.clone(), v.clone());
62 }
63 }
64 map
65 }
66
67 pub fn len(&self) -> usize {
68 self.values.len()
69 }
70
71 pub fn is_empty(&self) -> bool {
72 self.values.is_empty()
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn row_from_csv_line() {
82 let row = SourceRow::from_csv_line("1,Alice,engineer");
83 assert_eq!(row.columns(), &["col_0", "col_1", "col_2"]);
84 assert_eq!(row.values(), &["1", "Alice", "engineer"]);
85 assert_eq!(row.len(), 3);
86 }
87
88 #[test]
89 fn row_from_csv_empty_line() {
90 let row = SourceRow::from_csv_line("");
91 assert_eq!(row.len(), 1);
93 assert_eq!(row.get("col_0"), Some(""));
94 }
95
96 #[test]
97 fn row_new_and_get() {
98 let headers = vec!["id".into(), "name".into()];
99 let row = SourceRow::new(&headers, vec!["42".into(), "Alice".into()]);
100 assert_eq!(row.get("id"), Some("42"));
101 assert_eq!(row.get("name"), Some("Alice"));
102 assert_eq!(row.get("missing"), None);
103 }
104
105 #[test]
106 fn row_from_pairs() {
107 let row = SourceRow::from_pairs(vec![("x".into(), "1".into()), ("y".into(), "2".into())]);
108 assert_eq!(row.get("x"), Some("1"));
109 assert_eq!(row.get("y"), Some("2"));
110 }
111
112 #[test]
113 fn row_get_or_default() {
114 let headers = vec!["id".into()];
115 let row = SourceRow::new(&headers, vec!["1".into()]);
116 assert_eq!(row.get_or("id", "fallback"), "1");
117 assert_eq!(row.get_or("missing", "fallback"), "fallback");
118 }
119
120 #[test]
121 fn row_to_map() {
122 let headers = vec!["a".into(), "b".into()];
123 let row = SourceRow::new(&headers, vec!["1".into(), "2".into()]);
124 let map = row.to_map();
125 assert_eq!(map.get("a"), Some(&"1".to_string()));
126 assert_eq!(map.get("b"), Some(&"2".to_string()));
127 }
128
129 #[test]
130 fn row_len_and_empty() {
131 let empty_row = SourceRow::new(&[], vec![]);
132 assert!(empty_row.is_empty());
133 assert_eq!(empty_row.len(), 0);
134
135 let row = SourceRow::new(&["x".into()], vec!["1".into()]);
136 assert!(!row.is_empty());
137 assert_eq!(row.len(), 1);
138 }
139}