textfsm_rs/
export.rs

1use crate::{DataRecord, TextFsmError};
2use std::collections::BTreeSet;
3
4/// Supported output formats for parsed results.
5#[derive(Debug, Clone, Copy)]
6pub enum OutputFormat {
7    /// JSON format (using serde_json)
8    #[cfg(feature = "json")]
9    Json,
10    /// YAML format (using serde_yaml)
11    #[cfg(feature = "yaml")]
12    Yaml,
13    /// Comma-Separated Values (headers sorted alphabetically)
14    #[cfg(feature = "csv_export")]
15    Csv,
16    /// Simple ASCII table
17    Text,
18    /// HTML table
19    Html,
20    /// XML format
21    Xml,
22}
23
24/// Trait to export parsing results to various formats.
25pub trait TextFsmExport {
26    /// Exports the results to the specified format.
27    fn export(&self, format: OutputFormat) -> Result<String, TextFsmError>;
28}
29
30impl TextFsmExport for Vec<DataRecord> {
31    fn export(&self, format: OutputFormat) -> Result<String, TextFsmError> {
32        match format {
33            #[cfg(feature = "json")]
34            OutputFormat::Json => serde_json::to_string_pretty(self)
35                .map_err(|e| TextFsmError::InternalError(e.to_string())),
36            #[cfg(feature = "yaml")]
37            OutputFormat::Yaml => {
38                serde_yaml::to_string(self).map_err(|e| TextFsmError::InternalError(e.to_string()))
39            }
40            #[cfg(feature = "csv_export")]
41            OutputFormat::Csv => export_csv(self),
42            OutputFormat::Text => export_text(self),
43            OutputFormat::Html => export_html(self),
44            OutputFormat::Xml => export_xml(self),
45        }
46    }
47}
48
49fn get_headers(records: &[DataRecord]) -> Vec<String> {
50    let mut headers = BTreeSet::new();
51    for rec in records {
52        for k in rec.fields.keys() {
53            headers.insert(k.clone());
54        }
55    }
56    headers.into_iter().collect()
57}
58
59#[cfg(feature = "csv_export")]
60fn export_csv(records: &[DataRecord]) -> Result<String, TextFsmError> {
61    let headers = get_headers(records);
62    let mut wtr = csv::Writer::from_writer(vec![]);
63
64    // Write header
65    wtr.write_record(&headers)
66        .map_err(|e| TextFsmError::InternalError(e.to_string()))?;
67
68    // Write records
69    for rec in records {
70        let row: Vec<String> = headers
71            .iter()
72            .map(|h| {
73                if let Some(val) = rec.get(h) {
74                    val.to_string()
75                } else {
76                    String::new()
77                }
78            })
79            .collect();
80        wtr.write_record(&row)
81            .map_err(|e| TextFsmError::InternalError(e.to_string()))?;
82    }
83
84    let data = wtr
85        .into_inner()
86        .map_err(|e| TextFsmError::InternalError(e.to_string()))?;
87    String::from_utf8(data).map_err(|e| TextFsmError::InternalError(e.to_string()))
88}
89
90fn export_html(records: &[DataRecord]) -> Result<String, TextFsmError> {
91    let headers = get_headers(records);
92    let mut html = String::from("<table>\n<thead>\n<tr>");
93    for h in &headers {
94        html.push_str(&format!("<th>{}</th>", h));
95    }
96    html.push_str("</tr>\n</thead>\n<tbody>\n");
97
98    for rec in records {
99        html.push_str("<tr>");
100        for h in &headers {
101            let val = if let Some(v) = rec.get(h) {
102                let value_str = v.to_string();
103                value_str
104                    .replace('&', "&amp;")
105                    .replace('<', "&lt;")
106                    .replace('>', "&gt;")
107                    .replace('"', "&quot;")
108                    .replace('\'', "&apos;")
109            } else {
110                String::new()
111            };
112            html.push_str(&format!("<td>{}</td>", val));
113        }
114        html.push_str("</tr>\n");
115    }
116    html.push_str("</tbody>\n</table>");
117    Ok(html)
118}
119
120fn export_xml(records: &[DataRecord]) -> Result<String, TextFsmError> {
121    let headers = get_headers(records);
122    let mut xml = String::from("<results>\n");
123
124    for rec in records {
125        xml.push_str("  <record>\n");
126        for h in &headers {
127            if let Some(val) = rec.get(h) {
128                // Sanitize tag name (XML tags cannot contain spaces)
129                let tag_name = h.replace(' ', "_");
130                let value_str = val.to_string();
131                // Basic XML escaping
132                let escaped_value = value_str
133                    .replace('&', "&amp;")
134                    .replace('<', "&lt;")
135                    .replace('>', "&gt;")
136                    .replace('"', "&quot;")
137                    .replace('\'', "&apos;");
138                xml.push_str(&format!(
139                    "    <{}>{}</{}>\n",
140                    tag_name, escaped_value, tag_name
141                ));
142            }
143        }
144        xml.push_str("  </record>\n");
145    }
146    xml.push_str("</results>");
147    Ok(xml)
148}
149
150fn export_text(records: &[DataRecord]) -> Result<String, TextFsmError> {
151    let headers = get_headers(records);
152    if headers.is_empty() {
153        return Ok(String::new());
154    }
155
156    // Calculate column widths
157    let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
158
159    for rec in records {
160        for (i, h) in headers.iter().enumerate() {
161            if let Some(val) = rec.get(h) {
162                let len = val.to_string().len();
163                if len > widths[i] {
164                    widths[i] = len;
165                }
166            }
167        }
168    }
169
170    let mut out = String::new();
171
172    // Header
173    for (i, h) in headers.iter().enumerate() {
174        out.push_str(&format!("{:<width$}  ", h, width = widths[i]));
175    }
176    out.truncate(out.trim_end().len());
177    out.push('\n');
178
179    // Separator
180    for (i, _) in headers.iter().enumerate() {
181        out.push_str(&format!(
182            "{:<width$}  ",
183            "-".repeat(widths[i]),
184            width = widths[i]
185        ));
186    }
187    out.truncate(out.trim_end().len());
188    out.push('\n');
189
190    // Rows
191    for rec in records {
192        for (i, h) in headers.iter().enumerate() {
193            let val = if let Some(v) = rec.get(h) {
194                v.to_string()
195            } else {
196                String::new()
197            };
198            out.push_str(&format!("{:<width$}  ", val, width = widths[i]));
199        }
200        out.truncate(out.trim_end().len());
201        out.push('\n');
202    }
203
204    Ok(out)
205}