Skip to main content

textfsm_core/clitable/
table.rs

1//! TextTable - Table data structure for parsed CLI output.
2//!
3//! Provides a table with named columns, row iteration, filtering, sorting,
4//! and formatted output.
5
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use std::fmt;
9use std::ops::Index as IndexTrait;
10use std::sync::Arc;
11
12use crate::types::Value;
13
14/// Natural comparison: try numeric comparison first, fall back to string comparison.
15/// This ensures "2" < "10" (unlike pure string comparison where "10" < "2").
16fn natural_cmp(a: &str, b: &str) -> Ordering {
17    // Try to parse both as integers for numeric comparison
18    match (a.parse::<i64>(), b.parse::<i64>()) {
19        (Ok(na), Ok(nb)) => na.cmp(&nb),
20        _ => a.cmp(b),
21    }
22}
23
24/// Table of parsed CLI output with named columns.
25#[derive(Debug, Clone)]
26pub struct TextTable {
27    /// Column names (header).
28    header: Arc<Vec<String>>,
29
30    /// Data rows.
31    rows: Vec<Row>,
32
33    /// Key columns for superkey sorting.
34    superkey: Vec<String>,
35
36    /// Column name to index mapping (cached for fast lookup).
37    column_index: HashMap<String, usize>,
38}
39
40impl TextTable {
41    /// Create a new empty table with the given header.
42    pub fn new(header: Vec<String>) -> Self {
43        let column_index: HashMap<String, usize> = header
44            .iter()
45            .enumerate()
46            .map(|(i, name)| (name.to_lowercase(), i))
47            .collect();
48
49        Self {
50            header: Arc::new(header),
51            rows: Vec::new(),
52            superkey: Vec::new(),
53            column_index,
54        }
55    }
56
57    /// Create a table from header and value rows.
58    pub fn from_values(header: Vec<String>, values: Vec<Vec<Value>>) -> Self {
59        let column_index: HashMap<String, usize> = header
60            .iter()
61            .enumerate()
62            .map(|(i, name)| (name.to_lowercase(), i))
63            .collect();
64
65        let header = Arc::new(header);
66        let rows: Vec<Row> = values
67            .into_iter()
68            .map(|v| Row::new(v, Arc::clone(&header)))
69            .collect();
70
71        Self {
72            header,
73            rows,
74            superkey: Vec::new(),
75            column_index,
76        }
77    }
78
79    /// Get the header (column names).
80    pub fn header(&self) -> &[String] {
81        &self.header
82    }
83
84    /// Get the number of rows.
85    pub fn len(&self) -> usize {
86        self.rows.len()
87    }
88
89    /// Check if the table is empty.
90    pub fn is_empty(&self) -> bool {
91        self.rows.is_empty()
92    }
93
94    /// Get the superkey columns.
95    pub fn superkey(&self) -> &[String] {
96        &self.superkey
97    }
98
99    /// Set the superkey columns for sorting.
100    pub fn set_superkey(&mut self, keys: Vec<String>) {
101        self.superkey = keys;
102    }
103
104    /// Add key columns to the superkey.
105    pub fn add_keys(&mut self, keys: &[String]) {
106        for key in keys {
107            if !self.superkey.contains(key) {
108                self.superkey.push(key.clone());
109            }
110        }
111    }
112
113    /// Append a row to the table.
114    pub fn append(&mut self, values: Vec<Value>) {
115        self.rows.push(Row::new(values, Arc::clone(&self.header)));
116    }
117
118    /// Append a Row object.
119    pub fn append_row(&mut self, row: Row) {
120        self.rows.push(row);
121    }
122
123    /// Remove a row by index.
124    pub fn remove(&mut self, index: usize) -> Option<Row> {
125        if index < self.rows.len() {
126            Some(self.rows.remove(index))
127        } else {
128            None
129        }
130    }
131
132    /// Get a row by index.
133    pub fn get(&self, index: usize) -> Option<&Row> {
134        self.rows.get(index)
135    }
136
137    /// Get a mutable row by index.
138    pub fn get_mut(&mut self, index: usize) -> Option<&mut Row> {
139        self.rows.get_mut(index)
140    }
141
142    /// Iterate over rows.
143    pub fn iter(&self) -> impl Iterator<Item = &Row> {
144        self.rows.iter()
145    }
146
147    /// Iterate over mutable rows.
148    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Row> {
149        self.rows.iter_mut()
150    }
151
152    /// Filter rows by a predicate.
153    pub fn filter<F>(&self, predicate: F) -> TextTable
154    where
155        F: Fn(&Row) -> bool,
156    {
157        let rows: Vec<Row> = self.rows.iter().filter(|r| predicate(r)).cloned().collect();
158
159        TextTable {
160            header: Arc::clone(&self.header),
161            rows,
162            superkey: self.superkey.clone(),
163            column_index: self.column_index.clone(),
164        }
165    }
166
167    /// Sort by superkey (if set) or by first column.
168    /// Uses natural sorting (numeric values sorted numerically, strings alphabetically).
169    pub fn sort(&mut self) {
170        if self.superkey.is_empty() {
171            // Sort by first column
172            self.rows.sort_by(|a, b| {
173                let va = a.values.first().map(|v| v.as_string()).unwrap_or_default();
174                let vb = b.values.first().map(|v| v.as_string()).unwrap_or_default();
175                natural_cmp(&va, &vb)
176            });
177        } else {
178            // Sort by superkey columns
179            let key_indices: Vec<usize> = self
180                .superkey
181                .iter()
182                .filter_map(|k| self.column_index.get(&k.to_lowercase()).copied())
183                .collect();
184
185            self.rows.sort_by(|a, b| {
186                for &idx in &key_indices {
187                    let va = a.values.get(idx).map(|v| v.as_string()).unwrap_or_default();
188                    let vb = b.values.get(idx).map(|v| v.as_string()).unwrap_or_default();
189                    match natural_cmp(&va, &vb) {
190                        std::cmp::Ordering::Equal => continue,
191                        other => return other,
192                    }
193                }
194                std::cmp::Ordering::Equal
195            });
196        }
197    }
198
199    /// Sort by a key function.
200    pub fn sort_by_key<K, F>(&mut self, f: F)
201    where
202        K: Ord,
203        F: Fn(&Row) -> K,
204    {
205        self.rows.sort_by_key(f);
206    }
207
208    /// Sort by a comparison function.
209    pub fn sort_by<F>(&mut self, compare: F)
210    where
211        F: Fn(&Row, &Row) -> std::cmp::Ordering,
212    {
213        self.rows.sort_by(compare);
214    }
215
216    /// Find the first row where a column has a specific value.
217    pub fn row_with(&self, column: &str, value: &str) -> Option<&Row> {
218        let idx = self.column_index.get(&column.to_lowercase())?;
219        self.rows.iter().find(|r| {
220            r.values
221                .get(*idx)
222                .map(|v| v.as_string() == value)
223                .unwrap_or(false)
224        })
225    }
226
227    /// Get formatted table output.
228    pub fn formatted(&self) -> String {
229        if self.rows.is_empty() {
230            return String::new();
231        }
232
233        // Calculate column widths
234        let mut widths: Vec<usize> = self.header.iter().map(|h| h.len()).collect();
235        for row in &self.rows {
236            for (i, value) in row.values.iter().enumerate() {
237                if i < widths.len() {
238                    widths[i] = widths[i].max(value.as_string().len());
239                }
240            }
241        }
242
243        let mut output = String::new();
244
245        // Header
246        for (i, name) in self.header.iter().enumerate() {
247            if i > 0 {
248                output.push_str("  ");
249            }
250            output.push_str(&format!("{:width$}", name, width = widths[i]));
251        }
252        output.push('\n');
253
254        // Separator
255        for (i, &width) in widths.iter().enumerate() {
256            if i > 0 {
257                output.push_str("  ");
258            }
259            output.push_str(&"-".repeat(width));
260        }
261        output.push('\n');
262
263        // Data rows
264        for row in &self.rows {
265            for (i, value) in row.values.iter().enumerate() {
266                if i > 0 {
267                    output.push_str("  ");
268                }
269                if i < widths.len() {
270                    output.push_str(&format!("{:width$}", value.as_string(), width = widths[i]));
271                }
272            }
273            output.push('\n');
274        }
275
276        output
277    }
278
279    /// Convert to CSV format.
280    pub fn to_csv(&self) -> String {
281        let mut output = String::new();
282
283        // Header
284        output.push_str(&self.header.join(", "));
285        output.push('\n');
286
287        // Data rows
288        for row in &self.rows {
289            let values: Vec<String> = row.values.iter().map(|v| v.as_string()).collect();
290            output.push_str(&values.join(", "));
291            output.push('\n');
292        }
293
294        output
295    }
296
297    /// Convert to Vec<Vec<Value>> for serde compatibility.
298    pub fn into_values(self) -> Vec<Vec<Value>> {
299        self.rows.into_iter().map(|r| r.values).collect()
300    }
301
302    /// Get the raw values as references.
303    pub fn values(&self) -> impl Iterator<Item = &Vec<Value>> {
304        self.rows.iter().map(|r| &r.values)
305    }
306
307    /// Clear all rows.
308    pub fn clear(&mut self) {
309        self.rows.clear();
310    }
311}
312
313impl fmt::Display for TextTable {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "{}", self.formatted())
316    }
317}
318
319impl<'a> IntoIterator for &'a TextTable {
320    type Item = &'a Row;
321    type IntoIter = std::slice::Iter<'a, Row>;
322
323    fn into_iter(self) -> Self::IntoIter {
324        self.rows.iter()
325    }
326}
327
328impl IntoIterator for TextTable {
329    type Item = Row;
330    type IntoIter = std::vec::IntoIter<Row>;
331
332    fn into_iter(self) -> Self::IntoIter {
333        self.rows.into_iter()
334    }
335}
336
337#[cfg(feature = "serde")]
338impl TextTable {
339    /// Deserialize rows into typed structs.
340    pub fn into_deserialize<T>(self) -> Result<Vec<T>, super::CliTableError>
341    where
342        T: serde::de::DeserializeOwned,
343    {
344        // Pre-lowercase headers once
345        let header: Vec<String> = self.header.iter().map(|s| s.to_lowercase()).collect();
346
347        self.rows
348            .into_iter()
349            .map(|row| {
350                crate::de::from_record_borrowed(&header, row.values)
351                    .map_err(|e| super::CliTableError::Parse(crate::ParseError::DeserializeError(e.to_string())))
352            })
353            .collect()
354    }
355}
356
357/// A single row in a TextTable.
358#[derive(Debug, Clone)]
359pub struct Row {
360    /// Values in this row.
361    values: Vec<Value>,
362
363    /// Reference to the header for column name lookup.
364    header: Arc<Vec<String>>,
365}
366
367impl Row {
368    /// Create a new row.
369    pub fn new(values: Vec<Value>, header: Arc<Vec<String>>) -> Self {
370        Self { values, header }
371    }
372
373    /// Get a value by column name.
374    pub fn get(&self, column: &str) -> Option<&Value> {
375        let column_lower = column.to_lowercase();
376        self.header
377            .iter()
378            .position(|h| h.to_lowercase() == column_lower)
379            .and_then(|i| self.values.get(i))
380    }
381
382    /// Get a mutable value by column name.
383    pub fn get_mut(&mut self, column: &str) -> Option<&mut Value> {
384        let column_lower = column.to_lowercase();
385        self.header
386            .iter()
387            .position(|h| h.to_lowercase() == column_lower)
388            .and_then(|i| self.values.get_mut(i))
389    }
390
391    /// Get the raw values.
392    pub fn values(&self) -> &[Value] {
393        &self.values
394    }
395
396    /// Get the header.
397    pub fn header(&self) -> &[String] {
398        &self.header
399    }
400
401    /// Convert to a HashMap.
402    pub fn to_map(&self) -> HashMap<String, Value> {
403        self.header
404            .iter()
405            .zip(self.values.iter())
406            .map(|(k, v)| (k.clone(), v.clone()))
407            .collect()
408    }
409}
410
411impl IndexTrait<&str> for Row {
412    type Output = Value;
413
414    fn index(&self, column: &str) -> &Self::Output {
415        self.get(column)
416            .unwrap_or_else(|| panic!("column '{}' not found", column))
417    }
418}
419
420impl IndexTrait<usize> for Row {
421    type Output = Value;
422
423    fn index(&self, index: usize) -> &Self::Output {
424        &self.values[index]
425    }
426}
427
428impl fmt::Display for Row {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        let values: Vec<String> = self.values.iter().map(|v| v.as_string()).collect();
431        write!(f, "{}", values.join(", "))
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    fn sample_table() -> TextTable {
440        let header = vec![
441            "Interface".into(),
442            "Status".into(),
443            "IP_Address".into(),
444        ];
445        let values = vec![
446            vec![
447                Value::Single("eth0".into()),
448                Value::Single("up".into()),
449                Value::Single("192.168.1.1".into()),
450            ],
451            vec![
452                Value::Single("eth1".into()),
453                Value::Single("down".into()),
454                Value::Single("10.0.0.1".into()),
455            ],
456            vec![
457                Value::Single("lo".into()),
458                Value::Single("up".into()),
459                Value::Single("127.0.0.1".into()),
460            ],
461        ];
462        TextTable::from_values(header, values)
463    }
464
465    #[test]
466    fn test_table_creation() {
467        let table = sample_table();
468        assert_eq!(table.len(), 3);
469        assert_eq!(table.header().len(), 3);
470    }
471
472    #[test]
473    fn test_row_indexing_by_name() {
474        let table = sample_table();
475        let row = table.get(0).unwrap();
476        assert_eq!(row["Interface"].as_string(), "eth0");
477        assert_eq!(row["Status"].as_string(), "up");
478        assert_eq!(row["IP_Address"].as_string(), "192.168.1.1");
479    }
480
481    #[test]
482    fn test_row_indexing_case_insensitive() {
483        let table = sample_table();
484        let row = table.get(0).unwrap();
485        assert_eq!(row["interface"].as_string(), "eth0");
486        assert_eq!(row["INTERFACE"].as_string(), "eth0");
487    }
488
489    #[test]
490    fn test_filter() {
491        let table = sample_table();
492        let up_only = table.filter(|row| row["Status"].as_string() == "up");
493        assert_eq!(up_only.len(), 2);
494    }
495
496    #[test]
497    fn test_sort() {
498        let mut table = sample_table();
499        table.sort();
500        assert_eq!(table.get(0).unwrap()["Interface"].as_string(), "eth0");
501        assert_eq!(table.get(1).unwrap()["Interface"].as_string(), "eth1");
502        assert_eq!(table.get(2).unwrap()["Interface"].as_string(), "lo");
503    }
504
505    #[test]
506    fn test_sort_by_superkey() {
507        let mut table = sample_table();
508        table.set_superkey(vec!["Status".into(), "Interface".into()]);
509        table.sort();
510        // down comes before up alphabetically
511        assert_eq!(table.get(0).unwrap()["Status"].as_string(), "down");
512    }
513
514    #[test]
515    fn test_row_with() {
516        let table = sample_table();
517        let row = table.row_with("Interface", "eth1");
518        assert!(row.is_some());
519        assert_eq!(row.unwrap()["Status"].as_string(), "down");
520    }
521
522    #[test]
523    fn test_formatted_output() {
524        let table = sample_table();
525        let output = table.formatted();
526        assert!(output.contains("Interface"));
527        assert!(output.contains("eth0"));
528        assert!(output.contains("192.168.1.1"));
529    }
530
531    #[test]
532    fn test_iteration() {
533        let table = sample_table();
534        let count = table.iter().count();
535        assert_eq!(count, 3);
536    }
537
538    #[test]
539    fn test_append() {
540        let mut table = sample_table();
541        table.append(vec![
542            Value::Single("eth2".into()),
543            Value::Single("up".into()),
544            Value::Single("172.16.0.1".into()),
545        ]);
546        assert_eq!(table.len(), 4);
547    }
548
549    #[test]
550    fn test_remove() {
551        let mut table = sample_table();
552        let removed = table.remove(1);
553        assert!(removed.is_some());
554        assert_eq!(table.len(), 2);
555    }
556
557    #[test]
558    fn test_to_map() {
559        let table = sample_table();
560        let row = table.get(0).unwrap();
561        let map = row.to_map();
562        assert_eq!(map.get("Interface").unwrap().as_string(), "eth0");
563    }
564}