1use crate::{DataRecord, TextFsmError};
2use std::collections::BTreeSet;
3
4#[derive(Debug, Clone, Copy)]
6pub enum OutputFormat {
7 #[cfg(feature = "json")]
9 Json,
10 #[cfg(feature = "yaml")]
12 Yaml,
13 #[cfg(feature = "csv_export")]
15 Csv,
16 Text,
18 Html,
20 Xml,
22}
23
24pub trait TextFsmExport {
26 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 wtr.write_record(&headers)
66 .map_err(|e| TextFsmError::InternalError(e.to_string()))?;
67
68 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('&', "&")
105 .replace('<', "<")
106 .replace('>', ">")
107 .replace('"', """)
108 .replace('\'', "'")
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 let tag_name = h.replace(' ', "_");
130 let value_str = val.to_string();
131 let escaped_value = value_str
133 .replace('&', "&")
134 .replace('<', "<")
135 .replace('>', ">")
136 .replace('"', """)
137 .replace('\'', "'");
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 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 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 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 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}