1use std::{
2 fs::File,
3 io::{self, BufWriter},
4 path::PathBuf,
5};
6
7use anyhow::{anyhow, Context};
8use clap::{ArgAction, Parser, ValueEnum};
9
10#[derive(Clone, Copy, Debug, ValueEnum)]
11pub enum OutputEncoding {
12 Infer,
14 Csv,
15 Json,
16}
17
18#[derive(Debug, Parser)]
19#[clap(version, about)]
20pub struct Args {
21 #[clap(
22 help = "A DBZ file to convert to another encoding. Pass '-' to read from standard input",
23 value_name = "FILE"
24 )]
25 pub input: PathBuf,
26 #[clap(
27 short,
28 long,
29 help = "Saves the result to FILE. If no path is specified, the output will be written to standard output",
30 value_name = "FILE"
31 )]
32 pub output: Option<PathBuf>,
33 #[clap(
34 short = 'J',
35 long,
36 action = ArgAction::SetTrue,
37 default_value = "false",
38 help = "Output the result as NDJSON (newline-delimited JSON)"
39 )]
40 pub json: bool,
41 #[clap(
42 short = 'C',
43 long,
44 action = ArgAction::SetTrue,
45 default_value = "false",
46 conflicts_with = "json",
47 help = "Output the result as CSV"
48 )]
49 pub csv: bool,
50 #[clap(
51 short,
52 long,
53 action = ArgAction::SetTrue,
54 default_value = "false",
55 help = "Allow overwriting of existing files, such as the output file"
56 )]
57 pub force: bool,
58 #[clap(
59 short = 'm',
60 long = "metadata",
61 action = ArgAction::SetTrue,
62 default_value = "false",
63 help = "Output the metadata section instead of the body of the DBZ file"
64 )]
65 pub should_output_metadata: bool,
66 #[clap(
67 short = 'p',
68 long = "pretty-json",
69 action = ArgAction::SetTrue,
70 default_value = "false",
71 help ="Make the JSON output easier to read with spacing and indentation"
72 )]
73 pub should_pretty_print: bool,
74}
75
76impl Args {
77 pub fn output_encoding(&self) -> OutputEncoding {
78 match (self.json, self.csv) {
79 (false, false) => OutputEncoding::Infer,
80 (true, false) => OutputEncoding::Json,
81 (false, true) => OutputEncoding::Csv,
82 (true, true) => unreachable!("Invalid state that clap conflicts_with should prevent"),
83 }
84 }
85}
86
87pub fn infer_encoding(args: &Args) -> anyhow::Result<dbz_lib::OutputEncoding> {
88 match args.output_encoding() {
89 OutputEncoding::Csv => Ok(dbz_lib::OutputEncoding::Csv),
90 OutputEncoding::Json => Ok(dbz_lib::OutputEncoding::Json {
91 should_pretty_print: args.should_pretty_print,
92 }),
93 OutputEncoding::Infer => match args.output.as_ref().and_then(|o| o.extension()) {
94 Some(ext) if ext == "csv" => Ok(dbz_lib::OutputEncoding::Csv),
95 Some(ext) if ext == "json" => Ok(dbz_lib::OutputEncoding::Json {
96 should_pretty_print: args.should_pretty_print,
97 }),
98 Some(ext) => Err(anyhow!(
99 "Unable to infer output encoding from output file with extension '{}'",
100 ext.to_string_lossy()
101 )),
102 None => Err(anyhow!(
103 "Unable to infer output encoding from output file without an extension"
104 )),
105 },
106 }
107}
108
109pub fn output_from_args(args: &Args) -> anyhow::Result<Box<dyn io::Write>> {
110 if let Some(output) = &args.output {
111 let output_file = open_output_file(output, args.force)?;
112 Ok(Box::new(BufWriter::new(output_file)))
113 } else {
114 Ok(Box::new(io::stdout().lock()))
115 }
116}
117
118fn open_output_file(path: &PathBuf, force: bool) -> anyhow::Result<File> {
119 let mut options = File::options();
120 options.write(true);
121 if force {
122 options.create(true);
123 } else if path.exists() {
124 return Err(anyhow!(
125 "Output file exists. Pass --force flag to overwrite the existing file."
126 ));
127 } else {
128 options.create_new(true);
129 }
130 options
131 .open(path)
132 .with_context(|| format!("Unable to open output file '{}'", path.display()))
133}