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