1use std::{
2 fs,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use clap::Parser;
8use colored::*;
9
10use crate::config::{ConfigBuilder, Format, format};
11
12#[derive(Parser, Debug)]
13#[command(rename_all = "kebab-case")]
14pub struct Opts {
15 pub(crate) input_path: PathBuf,
18
19 pub(crate) output_path: PathBuf,
21
22 #[arg(long, default_value = "yaml")]
24 pub(crate) output_format: Format,
25}
26
27fn check_paths(opts: &Opts) -> Result<(), String> {
28 let in_metadata = fs::metadata(&opts.input_path)
29 .unwrap_or_else(|_| panic!("Failed to get metadata for: {:?}", &opts.input_path));
30
31 if opts.output_path.exists() {
32 return Err(format!(
33 "Output path {:?} already exists. Please provide a non-existing output path.",
34 opts.output_path
35 ));
36 }
37
38 if opts.output_path.extension().is_none() {
39 if in_metadata.is_file() {
40 return Err(format!(
41 "{:?} points to a file but {:?} points to a directory.",
42 opts.input_path, opts.output_path
43 ));
44 }
45 } else if in_metadata.is_dir() {
46 return Err(format!(
47 "{:?} points to a directory but {:?} points to a file.",
48 opts.input_path, opts.output_path
49 ));
50 }
51
52 Ok(())
53}
54
55pub(crate) fn cmd(opts: &Opts) -> exitcode::ExitCode {
56 if let Err(e) = check_paths(opts) {
57 #[allow(clippy::print_stderr)]
58 {
59 eprintln!("{}", e.red());
60 }
61 return exitcode::SOFTWARE;
62 }
63
64 if opts.input_path.is_file() && opts.output_path.extension().is_some() {
65 if let Some(base_dir) = opts.output_path.parent()
66 && !base_dir.exists()
67 {
68 fs::create_dir_all(base_dir).unwrap_or_else(|_| {
69 panic!("Failed to create output dir(s): {:?}", &opts.output_path)
70 });
71 }
72
73 match convert_config(&opts.input_path, &opts.output_path, opts.output_format) {
74 Ok(_) => exitcode::OK,
75 Err(errors) => {
76 #[allow(clippy::print_stderr)]
77 {
78 errors.iter().for_each(|e| eprintln!("{}", e.red()));
79 }
80 exitcode::SOFTWARE
81 }
82 }
83 } else {
84 match walk_dir_and_convert(&opts.input_path, &opts.output_path, opts.output_format) {
85 Ok(()) => {
86 #[allow(clippy::print_stdout)]
87 {
88 println!(
89 "Finished conversion(s). Results are in {:?}",
90 opts.output_path
91 );
92 }
93 exitcode::OK
94 }
95 Err(errors) => {
96 #[allow(clippy::print_stderr)]
97 {
98 errors.iter().for_each(|e| eprintln!("{}", e.red()));
99 }
100 exitcode::SOFTWARE
101 }
102 }
103 }
104}
105
106fn convert_config(
107 input_path: &Path,
108 output_path: &Path,
109 output_format: Format,
110) -> Result<(), Vec<String>> {
111 if output_path.exists() {
112 return Err(vec![format!("Output path {output_path:?} exists")]);
113 }
114 let input_format = match Format::from_str(
115 input_path
116 .extension()
117 .unwrap_or_else(|| panic!("Failed to get extension for: {input_path:?}"))
118 .to_str()
119 .unwrap_or_else(|| panic!("Failed to convert OsStr to &str for: {input_path:?}")),
120 ) {
121 Ok(format) => format,
122 Err(_) => return Ok(()), };
124
125 if input_format == output_format {
126 return Ok(());
127 }
128
129 #[allow(clippy::print_stdout)]
130 {
131 println!("Converting {input_path:?} config to {output_format:?}.");
132 }
133 let file_contents = fs::read_to_string(input_path).map_err(|e| vec![e.to_string()])?;
134 let builder: ConfigBuilder = format::deserialize(&file_contents, input_format)?;
135 let config = builder.build()?;
136 let output_string =
137 format::serialize(&config, output_format).map_err(|e| vec![e.to_string()])?;
138 fs::write(output_path, output_string).map_err(|e| vec![e.to_string()])?;
139
140 #[allow(clippy::print_stdout)]
141 {
142 println!("Wrote result to {output_path:?}.");
143 }
144 Ok(())
145}
146
147fn walk_dir_and_convert(
148 input_path: &Path,
149 output_dir: &Path,
150 output_format: Format,
151) -> Result<(), Vec<String>> {
152 let mut errors = Vec::new();
153
154 if input_path.is_dir() {
155 for entry in fs::read_dir(input_path)
156 .unwrap_or_else(|_| panic!("Failed to read dir: {input_path:?}"))
157 {
158 let entry_path = entry
159 .unwrap_or_else(|_| panic!("Failed to get entry for dir: {input_path:?}"))
160 .path();
161 let new_output_dir = if entry_path.is_dir() {
162 let last_component = entry_path
163 .file_name()
164 .unwrap_or_else(|| panic!("Failed to get file_name for {entry_path:?}"));
165 let new_dir = output_dir.join(last_component);
166
167 if !new_dir.exists() {
168 fs::create_dir_all(&new_dir)
169 .unwrap_or_else(|_| panic!("Failed to create output dir: {new_dir:?}"));
170 }
171 new_dir
172 } else {
173 output_dir.to_path_buf()
174 };
175
176 if let Err(new_errors) = walk_dir_and_convert(
177 &input_path.join(&entry_path),
178 &new_output_dir,
179 output_format,
180 ) {
181 errors.extend(new_errors);
182 }
183 }
184 } else {
185 let output_path = output_dir.join(
186 input_path
187 .with_extension(output_format.to_string().as_str())
188 .file_name()
189 .ok_or_else(|| {
190 vec![format!(
191 "Cannot create output path for input: {input_path:?}"
192 )]
193 })?,
194 );
195 if let Err(new_errors) = convert_config(input_path, &output_path, output_format) {
196 errors.extend(new_errors);
197 }
198 }
199
200 if errors.is_empty() {
201 Ok(())
202 } else {
203 Err(errors)
204 }
205}
206
207#[cfg(all(
208 test,
209 feature = "sources-demo_logs",
210 feature = "transforms-remap",
211 feature = "sinks-console"
212))]
213mod tests {
214 use std::{
215 env, fs,
216 path::{Path, PathBuf},
217 str::FromStr,
218 };
219
220 use tempfile::tempdir;
221
222 use crate::{
223 config::{ConfigBuilder, Format, format},
224 convert_config::{Opts, check_paths, walk_dir_and_convert},
225 };
226
227 fn test_data_dir() -> PathBuf {
228 PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("tests/data/cmd/config")
229 }
230
231 fn convert_file_to_config_string(path: &Path) -> String {
235 let files_contents = fs::read_to_string(path).unwrap();
236 let extension = path.extension().unwrap().to_str().unwrap();
237 let file_format = Format::from_str(extension).unwrap();
238 let builder: ConfigBuilder = format::deserialize(&files_contents, file_format).unwrap();
239 let config = builder.build().unwrap();
240
241 format::serialize(&config, file_format).unwrap()
242 }
243
244 #[test]
245 fn invalid_path_opts() {
246 let check_error = |opts, pattern| {
247 let error = check_paths(&opts).unwrap_err();
248 assert!(error.contains(pattern));
249 };
250
251 check_error(
252 Opts {
253 input_path: ["./"].iter().collect(),
254 output_path: ["./"].iter().collect(),
255 output_format: Format::Yaml,
256 },
257 "already exists",
258 );
259
260 check_error(
261 Opts {
262 input_path: ["./"].iter().collect(),
263 output_path: ["./out.yaml"].iter().collect(),
264 output_format: Format::Yaml,
265 },
266 "points to a file.",
267 );
268
269 check_error(
270 Opts {
271 input_path: [test_data_dir(), "config_2.toml".into()].iter().collect(),
272 output_path: ["./another_dir"].iter().collect(),
273 output_format: Format::Yaml,
274 },
275 "points to a directory.",
276 );
277 }
278
279 #[test]
280 fn convert_all_from_dir() {
281 let input_path = test_data_dir();
282 let output_dir = tempdir()
283 .expect("Unable to create tempdir for config")
284 .keep();
285 walk_dir_and_convert(&input_path, &output_dir, Format::Yaml).unwrap();
286
287 let mut count: usize = 0;
288 let original_config = convert_file_to_config_string(&test_data_dir().join("config_1.yaml"));
289 for entry in fs::read_dir(&output_dir).unwrap() {
290 let entry = entry.unwrap();
291 let path = entry.path();
292 if path.is_file() {
293 let extension = path.extension().unwrap().to_str().unwrap();
294 if extension == Format::Yaml.to_string() {
295 let converted_config = fs::read_to_string(output_dir.join(&path)).unwrap();
297 assert_eq!(converted_config, original_config);
298 count += 1;
299 }
300 }
301 }
302 assert_eq!(count, 2);
304 }
305}