dbz_cli/
lib.rs

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    /// `dbz` will infer based on the extension of the specified output file
13    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}