1use std::path::PathBuf;
10use std::str::FromStr;
11
12use clap::Parser;
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum ExpandSpec {
17 Depth(usize),
18 All,
19}
20
21impl FromStr for ExpandSpec {
22 type Err = String;
23
24 fn from_str(s: &str) -> Result<Self, Self::Err> {
25 if s.eq_ignore_ascii_case("all") {
26 return Ok(Self::All);
27 }
28 s.parse()
29 .map(Self::Depth)
30 .map_err(|_| format!("expected a depth or `all`, got {s:?}"))
31 }
32}
33
34#[derive(Parser, Debug)]
36#[command(name = "ite", version, about)]
37pub struct Cli {
38 pub path: Option<PathBuf>,
40
41 #[arg(short, long, value_name = "PATH", conflicts_with = "path")]
45 pub json: Option<PathBuf>,
46
47 #[arg(short = 'l', long, value_name = "PATH", conflicts_with_all = ["path", "json"])]
50 pub jsonl: Option<PathBuf>,
51
52 #[arg(short = 'I', long)]
54 pub no_ignore: bool,
55
56 #[arg(short, long)]
58 pub expand: Option<ExpandSpec>,
59
60 #[arg(short, long)]
62 pub config: Vec<PathBuf>,
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn parse(args: &[&str]) -> Cli {
70 Cli::try_parse_from(std::iter::once("ite").chain(args.iter().copied())).unwrap()
71 }
72
73 #[test]
74 fn defaults() {
75 let cli = parse(&[]);
76 assert_eq!(cli.path, None);
77 assert_eq!(cli.json, None);
78 assert_eq!(cli.jsonl, None);
79 assert!(!cli.no_ignore);
80 assert_eq!(cli.expand, None);
81 assert!(cli.config.is_empty());
82 }
83
84 #[test]
85 fn positional_path() {
86 assert_eq!(parse(&["/some/dir"]).path, Some(PathBuf::from("/some/dir")));
87 }
88
89 #[test]
90 fn json_path_flag_and_alias() {
91 assert_eq!(
92 parse(&["--json", "data.json"]).json,
93 Some(PathBuf::from("data.json"))
94 );
95 assert_eq!(
96 parse(&["-j", "other.json"]).json,
97 Some(PathBuf::from("other.json"))
98 );
99 }
100
101 #[test]
102 fn json_and_directory_paths_are_mutually_exclusive() {
103 assert!(Cli::try_parse_from(["ite", "--json", "data.json", "/some/dir"]).is_err());
104 }
105
106 #[test]
107 fn jsonl_path_flag_and_alias() {
108 assert_eq!(
109 parse(&["--jsonl", "log.jsonl"]).jsonl,
110 Some(PathBuf::from("log.jsonl"))
111 );
112 assert_eq!(
113 parse(&["-l", "log.jsonl"]).jsonl,
114 Some(PathBuf::from("log.jsonl"))
115 );
116 }
117
118 #[test]
119 fn jsonl_conflicts_with_json_and_directory_paths() {
120 assert!(Cli::try_parse_from(["ite", "--jsonl", "a.jsonl", "--json", "b.json"]).is_err());
121 assert!(Cli::try_parse_from(["ite", "--jsonl", "a.jsonl", "/some/dir"]).is_err());
122 }
123
124 #[test]
125 fn no_ignore_flag_and_alias() {
126 assert!(parse(&["--no-ignore"]).no_ignore);
127 assert!(parse(&["-I"]).no_ignore);
128 }
129
130 #[test]
131 fn expand_depth_and_all() {
132 assert_eq!(parse(&["--expand", "2"]).expand, Some(ExpandSpec::Depth(2)));
133 assert_eq!(parse(&["-e", "all"]).expand, Some(ExpandSpec::All));
134 }
135
136 #[test]
137 fn expand_rejects_garbage() {
138 assert!(Cli::try_parse_from(["ite", "--expand", "banana"]).is_err());
139 }
140
141 #[test]
142 fn repeated_config_flags() {
143 let cli = parse(&["-c", "a.toml", "--config", "b.toml"]);
144 assert_eq!(
145 cli.config,
146 vec![PathBuf::from("a.toml"), PathBuf::from("b.toml")]
147 );
148 }
149}