messagepack_cli/
cli.rs

1//! CLI implementation.
2
3use std::{
4	fs::File,
5	io::{BufReader, BufWriter, Read, Write},
6	path::PathBuf,
7};
8
9use clap::{Args, Parser};
10use error_stack::{report, Report, ResultExt};
11
12use crate::{conversion::ConversionDirection, Converter, Error};
13
14/// Converter type with dynamic reader and writer.
15type DynamicConverter = Converter<Box<dyn Read>, Box<dyn Write>>;
16
17/// Simple CLI to convert MessagePack to JSON and vice versa. Automatically
18/// attempts to detect the input format and outputs the respective other format.
19/// Use the config options to override the automatic detection.
20#[derive(Debug, Parser)]
21#[command(author, version, about, long_about = None)]
22pub struct Cli {
23	/// Direction of conversion.
24	#[command(flatten)]
25	direction: ConversionDirectionArgs,
26	/// Input file path to use. Stdin is used if not given.
27	#[arg(short, long)]
28	input: Option<PathBuf>,
29	/// Output file path to use. Stdout is used if not given.
30	#[arg(short, long)]
31	output: Option<PathBuf>,
32}
33
34/// Conversion direction argument group.
35#[derive(Debug, Args)]
36#[group(required = false, multiple = false)]
37struct ConversionDirectionArgs {
38	/// Convert MsgPack to JSON.
39	#[arg(long = "m2j")]
40	msgpack2json: bool,
41	/// Convert JSON to MsgPack.
42	#[arg(long = "j2m")]
43	json2msgpack: bool,
44}
45
46/// Validate the conversion direction to be sure not both directions are
47/// set. Then return the conversion direction as enum.
48impl TryFrom<ConversionDirectionArgs> for ConversionDirection {
49	type Error = Report<Error>;
50
51	fn try_from(direction: ConversionDirectionArgs) -> Result<Self, Self::Error> {
52		if direction.msgpack2json && direction.json2msgpack {
53			return Err(report!(Error::MultipleConversionDirections));
54		}
55
56		if direction.msgpack2json {
57			Ok(Self::MsgPack2Json)
58		} else if direction.json2msgpack {
59			Ok(Self::Json2MsgPack)
60		} else {
61			Ok(Self::Auto)
62		}
63	}
64}
65
66impl Cli {
67	/// Use the input configuration to construct the execution converter.
68	pub fn into_converter(self) -> Result<DynamicConverter, Report<Error>> {
69		let mut direction = self.direction.try_into()?;
70
71		let input: Box<dyn Read> = if let Some(input_file) = self.input {
72			// Use input file name as solid clue of input format.
73			if direction == ConversionDirection::Auto && input_file.ends_with(".json") {
74				direction = ConversionDirection::Json2MsgPack;
75			}
76
77			let file = File::open(input_file).change_context(Error::FileRead)?;
78			Box::new(BufReader::new(file))
79		} else {
80			Box::new(std::io::stdin().lock())
81		};
82
83		let output: Box<dyn Write> = if let Some(output_file) = self.output {
84			let file = File::create(output_file).change_context(Error::FileWrite)?;
85			Box::new(BufWriter::new(file))
86		} else {
87			Box::new(std::io::stdout().lock())
88		};
89
90		Ok(Converter::new(input, output, direction))
91	}
92}
93
94#[cfg(test)]
95mod tests {
96	use clap::CommandFactory;
97
98	use super::Cli;
99
100	#[test]
101	fn cli() {
102		Cli::command().debug_assert();
103	}
104}