use clap::{Parser, ValueEnum};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "toon", version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
#[command(after_help = "EXAMPLES:
toon input.json # Encode JSON to TOON (stdout)
toon input.toon # Decode TOON to JSON (stdout)
toon input.json -o output.toon # Encode to file
cat data.json | toon --encode # Encode from stdin
cat data.toon | toon --decode # Decode from stdin
toon input.json --stats # Show token statistics")]
pub struct Args {
#[arg(value_name = "INPUT")]
pub input: Option<PathBuf>,
#[arg(short, long, value_name = "FILE")]
pub output: Option<PathBuf>,
#[arg(short, long, conflicts_with = "decode")]
pub encode: bool,
#[arg(short, long, conflicts_with = "encode")]
pub decode: bool,
#[arg(long, default_value = ",", value_parser = parse_delimiter)]
pub delimiter: char,
#[arg(long, default_value = "2", value_parser = clap::value_parser!(u8).range(0..=16))]
pub indent: u8,
#[arg(long = "no-strict")]
pub no_strict: bool,
#[arg(long, value_enum, default_value = "off")]
pub key_folding: KeyFoldingArg,
#[arg(long, value_name = "N")]
pub flatten_depth: Option<usize>,
#[arg(long, value_enum, default_value = "off")]
pub expand_paths: ExpandPathsArg,
#[arg(long)]
pub stats: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum KeyFoldingArg {
Off,
Safe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ExpandPathsArg {
Off,
Safe,
}
fn parse_delimiter(s: &str) -> Result<char, String> {
match s {
"," | "comma" => Ok(','),
"|" | "pipe" => Ok('|'),
"\\t" | "\t" | "tab" => Ok('\t'),
_ => Err(format!(
"Invalid delimiter \"{s}\". Valid delimiters are: comma (,), tab (\\t), pipe (|)"
)),
}
}
impl Args {
#[must_use]
pub fn detect_mode(&self) -> Mode {
if self.encode {
return Mode::Encode;
}
if self.decode {
return Mode::Decode;
}
if let Some(ref path) = self.input
&& let Some(ext) = path.extension()
{
let ext = ext.to_string_lossy().to_lowercase();
if ext == "json" {
return Mode::Encode;
}
if ext == "toon" {
return Mode::Decode;
}
}
Mode::Encode
}
#[must_use]
pub fn is_stdin(&self) -> bool {
self.input.is_none() || self.input.as_ref().is_some_and(|p| p.as_os_str() == "-")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Encode,
Decode,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_delimiter() {
assert_eq!(parse_delimiter(","), Ok(','));
assert_eq!(parse_delimiter("|"), Ok('|'));
assert_eq!(parse_delimiter("\\t"), Ok('\t'));
assert_eq!(parse_delimiter("tab"), Ok('\t'));
assert!(parse_delimiter("invalid").is_err());
}
#[test]
fn test_detect_mode_explicit_flags() {
let args = Args {
input: None,
output: None,
encode: true,
decode: false,
delimiter: ',',
indent: 2,
no_strict: false,
key_folding: KeyFoldingArg::Off,
flatten_depth: None,
expand_paths: ExpandPathsArg::Off,
stats: false,
};
assert_eq!(args.detect_mode(), Mode::Encode);
}
#[test]
fn test_detect_mode_by_extension() {
let args = Args {
input: Some(PathBuf::from("data.toon")),
output: None,
encode: false,
decode: false,
delimiter: ',',
indent: 2,
no_strict: false,
key_folding: KeyFoldingArg::Off,
flatten_depth: None,
expand_paths: ExpandPathsArg::Off,
stats: false,
};
assert_eq!(args.detect_mode(), Mode::Decode);
}
}