1use clap::{Parser, ValueEnum};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
6#[command(name = "toon", version, about, long_about = None)]
7#[allow(clippy::struct_excessive_bools)]
8#[command(after_help = "EXAMPLES:
9 toon input.json # Encode JSON to TOON (stdout)
10 toon input.toon # Decode TOON to JSON (stdout)
11 toon input.json -o output.toon # Encode to file
12 cat data.json | toon --encode # Encode from stdin
13 cat data.toon | toon --decode # Decode from stdin
14 toon input.json --stats # Show token statistics")]
15pub struct Args {
16 #[arg(value_name = "INPUT")]
18 pub input: Option<PathBuf>,
19
20 #[arg(short, long, value_name = "FILE")]
22 pub output: Option<PathBuf>,
23
24 #[arg(short, long, conflicts_with = "decode")]
26 pub encode: bool,
27
28 #[arg(short, long, conflicts_with = "encode")]
30 pub decode: bool,
31
32 #[arg(long, default_value = ",", value_parser = parse_delimiter)]
34 pub delimiter: char,
35
36 #[arg(long, default_value = "2", value_parser = clap::value_parser!(u8).range(0..=16))]
38 pub indent: u8,
39
40 #[arg(long = "no-strict")]
42 pub no_strict: bool,
43
44 #[arg(long, value_enum, default_value = "off")]
46 pub key_folding: KeyFoldingArg,
47
48 #[arg(long, value_name = "N")]
50 pub flatten_depth: Option<usize>,
51
52 #[arg(long, value_enum, default_value = "off")]
54 pub expand_paths: ExpandPathsArg,
55
56 #[arg(long)]
58 pub stats: bool,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
62pub enum KeyFoldingArg {
63 Off,
64 Safe,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
68pub enum ExpandPathsArg {
69 Off,
70 Safe,
71}
72
73fn parse_delimiter(s: &str) -> Result<char, String> {
74 match s {
75 "," | "comma" => Ok(','),
76 "|" | "pipe" => Ok('|'),
77 "\\t" | "\t" | "tab" => Ok('\t'),
78 _ => Err(format!(
79 "Invalid delimiter \"{s}\". Valid delimiters are: comma (,), tab (\\t), pipe (|)"
80 )),
81 }
82}
83
84impl Args {
85 #[must_use]
87 pub fn detect_mode(&self) -> Mode {
88 if self.encode {
90 return Mode::Encode;
91 }
92 if self.decode {
93 return Mode::Decode;
94 }
95
96 if let Some(ref path) = self.input
98 && let Some(ext) = path.extension()
99 {
100 let ext = ext.to_string_lossy().to_lowercase();
101 if ext == "json" {
102 return Mode::Encode;
103 }
104 if ext == "toon" {
105 return Mode::Decode;
106 }
107 }
108
109 Mode::Encode
111 }
112
113 #[must_use]
115 pub fn is_stdin(&self) -> bool {
116 self.input.is_none() || self.input.as_ref().is_some_and(|p| p.as_os_str() == "-")
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Mode {
122 Encode,
123 Decode,
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_parse_delimiter() {
132 assert_eq!(parse_delimiter(","), Ok(','));
133 assert_eq!(parse_delimiter("|"), Ok('|'));
134 assert_eq!(parse_delimiter("\\t"), Ok('\t'));
135 assert_eq!(parse_delimiter("tab"), Ok('\t'));
136 assert!(parse_delimiter("invalid").is_err());
137 }
138
139 #[test]
140 fn test_detect_mode_explicit_flags() {
141 let args = Args {
142 input: None,
143 output: None,
144 encode: true,
145 decode: false,
146 delimiter: ',',
147 indent: 2,
148 no_strict: false,
149 key_folding: KeyFoldingArg::Off,
150 flatten_depth: None,
151 expand_paths: ExpandPathsArg::Off,
152 stats: false,
153 };
154 assert_eq!(args.detect_mode(), Mode::Encode);
155 }
156
157 #[test]
158 fn test_detect_mode_by_extension() {
159 let args = Args {
160 input: Some(PathBuf::from("data.toon")),
161 output: None,
162 encode: false,
163 decode: false,
164 delimiter: ',',
165 indent: 2,
166 no_strict: false,
167 key_folding: KeyFoldingArg::Off,
168 flatten_depth: None,
169 expand_paths: ExpandPathsArg::Off,
170 stats: false,
171 };
172 assert_eq!(args.detect_mode(), Mode::Decode);
173 }
174}