1use crate::error::Result;
4use serde::Serialize;
5use std::fs;
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8use tracing::debug;
9
10#[derive(Debug, Clone)]
12pub struct JsonWriter {
13 output_path: Option<PathBuf>,
14 pretty: bool,
15}
16
17impl JsonWriter {
18 pub fn new(output_path: Option<PathBuf>, pretty: bool) -> Self {
19 Self {
20 output_path,
21 pretty,
22 }
23 }
24
25 pub fn stdout(pretty: bool) -> Self {
27 Self {
28 output_path: None,
29 pretty,
30 }
31 }
32
33 pub fn file(path: impl Into<PathBuf>, pretty: bool) -> Self {
35 Self {
36 output_path: Some(path.into()),
37 pretty,
38 }
39 }
40
41 pub fn output_path(&self) -> Option<&Path> {
42 self.output_path.as_deref()
43 }
44
45 pub fn is_pretty(&self) -> bool {
46 self.pretty
47 }
48
49 pub fn write<T: Serialize>(&self, data: &T) -> Result<()> {
51 let json = if self.pretty {
52 serde_json::to_string_pretty(data)?
53 } else {
54 serde_json::to_string(data)?
55 };
56
57 match &self.output_path {
58 Some(path) => {
59 if let Some(parent) = path.parent() {
61 if !parent.exists() {
62 fs::create_dir_all(parent)?;
63 }
64 }
65 fs::write(path, &json)?;
66 debug!("Wrote JSON to: {:?}", path);
67 }
68 None => {
69 writeln!(io::stdout(), "{}", json)?;
70 }
71 }
72 Ok(())
73 }
74
75 pub fn to_string<T: Serialize>(&self, data: &T) -> Result<String> {
77 let json = if self.pretty {
78 serde_json::to_string_pretty(data)?
79 } else {
80 serde_json::to_string(data)?
81 };
82 Ok(json)
83 }
84}
85
86impl Default for JsonWriter {
87 fn default() -> Self {
88 Self::stdout(true)
89 }
90}
91
92#[cfg(feature = "csv")]
94#[derive(Debug, Clone)]
95pub struct CsvWriter {
96 output_path: Option<PathBuf>,
97}
98
99#[cfg(feature = "csv")]
100impl CsvWriter {
101 pub fn new(output_path: Option<PathBuf>) -> Self {
102 Self { output_path }
103 }
104
105 pub fn stdout() -> Self {
106 Self { output_path: None }
107 }
108
109 pub fn file(path: impl Into<PathBuf>) -> Self {
110 Self {
111 output_path: Some(path.into()),
112 }
113 }
114
115 pub fn write<'a, I, R>(&self, headers: &[&str], records: I) -> Result<()>
116 where
117 I: IntoIterator<Item = R>,
118 R: AsRef<[&'a str]>,
119 {
120 let mut output = headers.join(",");
121 output.push('\n');
122 for record in records {
123 let fields: Vec<String> = record
124 .as_ref()
125 .iter()
126 .map(|f| escape_csv_field(f))
127 .collect();
128 output.push_str(&fields.join(","));
129 output.push('\n');
130 }
131
132 match &self.output_path {
133 Some(path) => {
134 if let Some(parent) = path.parent() {
135 if !parent.exists() {
136 fs::create_dir_all(parent)?;
137 }
138 }
139 fs::write(path, &output)?;
140 debug!("Wrote CSV to: {:?}", path);
141 }
142 None => {
143 write!(io::stdout(), "{}", output)?;
144 }
145 }
146 Ok(())
147 }
148}
149
150#[cfg(feature = "csv")]
151impl Default for CsvWriter {
152 fn default() -> Self {
153 Self::stdout()
154 }
155}
156
157#[cfg(feature = "csv")]
158fn escape_csv_field(field: &str) -> String {
159 if field.contains(',') || field.contains('"') || field.contains('\n') {
160 format!("\"{}\"", field.replace('"', "\"\""))
161 } else {
162 field.to_string()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use serde::{Deserialize, Serialize};
170 use tempfile::tempdir;
171
172 #[derive(Serialize, Deserialize, Debug, PartialEq)]
173 struct TestData {
174 name: String,
175 value: i32,
176 }
177
178 #[test]
179 fn test_json_writer_default() {
180 let writer = JsonWriter::default();
181 assert!(writer.output_path().is_none());
182 assert!(writer.is_pretty());
183 }
184
185 #[test]
186 fn test_json_writer_file() {
187 let dir = tempdir().unwrap();
188 let path = dir.path().join("test.json");
189 let writer = JsonWriter::file(&path, false);
190 let data = TestData {
191 name: "test".to_string(),
192 value: 42,
193 };
194 writer.write(&data).unwrap();
195 assert!(path.exists());
196 let content = fs::read_to_string(&path).unwrap();
197 let read_data: TestData = serde_json::from_str(&content).unwrap();
198 assert_eq!(read_data, data);
199 }
200
201 #[test]
202 fn test_json_writer_to_string() {
203 let writer = JsonWriter::stdout(false);
204 let data = TestData {
205 name: "test".to_string(),
206 value: 42,
207 };
208 let json = writer.to_string(&data).unwrap();
209 assert!(json.contains("\"name\":\"test\""));
210 }
211
212 #[test]
213 fn test_json_writer_creates_parent_dirs() {
214 let dir = tempdir().unwrap();
215 let nested = dir.path().join("a").join("b").join("test.json");
216 let writer = JsonWriter::file(&nested, false);
217 let data = TestData {
218 name: "test".to_string(),
219 value: 42,
220 };
221 writer.write(&data).unwrap();
222 assert!(nested.exists());
223 }
224}