windjammer_runtime/
cli.rs1use clap::{Arg, ArgAction, ArgMatches, Command};
7
8pub fn new(name: impl Into<String>) -> AppBuilder {
10 AppBuilder {
11 name: name.into(),
12 version: None,
13 author: None,
14 about: None,
15 args: Vec::new(),
16 }
17}
18
19pub fn arg(name: impl Into<String>) -> ArgBuilder {
21 ArgBuilder {
22 name: name.into(),
23 short: None,
24 long: None,
25 help: None,
26 required: false,
27 multiple: false,
28 default_value: None,
29 arg_type: ArgType::Positional,
30 }
31}
32
33pub fn flag(name: impl Into<String>) -> ArgBuilder {
35 ArgBuilder {
36 name: name.into(),
37 short: None,
38 long: None,
39 help: None,
40 required: false,
41 multiple: false,
42 default_value: None,
43 arg_type: ArgType::Flag,
44 }
45}
46
47pub fn option(name: impl Into<String>) -> ArgBuilder {
49 ArgBuilder {
50 name: name.into(),
51 short: None,
52 long: None,
53 help: None,
54 required: false,
55 multiple: false,
56 default_value: None,
57 arg_type: ArgType::Option,
58 }
59}
60
61#[derive(Debug, Clone)]
62enum ArgType {
63 Positional,
64 Flag,
65 Option,
66}
67
68#[derive(Debug, Clone)]
70pub struct AppBuilder {
71 name: String,
72 version: Option<String>,
73 author: Option<String>,
74 about: Option<String>,
75 args: Vec<ArgBuilder>,
76}
77
78#[derive(Debug, Clone)]
80pub struct ArgBuilder {
81 name: String,
82 short: Option<String>,
83 long: Option<String>,
84 help: Option<String>,
85 required: bool,
86 multiple: bool,
87 default_value: Option<String>,
88 arg_type: ArgType,
89}
90
91impl AppBuilder {
92 pub fn version(mut self, version: impl Into<String>) -> Self {
94 self.version = Some(version.into());
95 self
96 }
97
98 pub fn author(mut self, author: impl Into<String>) -> Self {
100 self.author = Some(author.into());
101 self
102 }
103
104 pub fn about(mut self, about: impl Into<String>) -> Self {
106 self.about = Some(about.into());
107 self
108 }
109
110 pub fn arg(mut self, arg: ArgBuilder) -> Self {
112 self.args.push(arg);
113 self
114 }
115
116 pub fn parse(self) -> CliMatches {
118 let name: &'static str = Box::leak(self.name.into_boxed_str());
120 let mut cmd = Command::new(name);
121
122 if let Some(version) = self.version {
123 let version_static: &'static str = Box::leak(version.into_boxed_str());
124 cmd = cmd.version(version_static);
125 }
126 if let Some(author) = self.author {
127 let author_static: &'static str = Box::leak(author.into_boxed_str());
128 cmd = cmd.author(author_static);
129 }
130 if let Some(about) = self.about {
131 let about_static: &'static str = Box::leak(about.into_boxed_str());
132 cmd = cmd.about(about_static);
133 }
134
135 for arg_builder in self.args {
136 let name: &'static str = Box::leak(arg_builder.name.into_boxed_str());
138
139 let mut arg = Arg::new(name);
140
141 if let Some(help_str) = arg_builder.help {
142 let help: &'static str = Box::leak(help_str.into_boxed_str());
143 arg = arg.help(help);
144 }
145
146 if let Some(short_str) = arg_builder.short {
147 if let Some(c) = short_str.chars().next() {
148 arg = arg.short(c);
149 }
150 }
151
152 if let Some(long_str) = arg_builder.long {
153 let long: &'static str = Box::leak(long_str.into_boxed_str());
154 arg = arg.long(long);
155 }
156
157 match arg_builder.arg_type {
158 ArgType::Positional => {
159 arg = arg.required(arg_builder.required);
160 if arg_builder.multiple {
161 arg = arg.num_args(1..);
162 }
163 if let Some(default_str) = arg_builder.default_value {
164 let default: &'static str = Box::leak(default_str.into_boxed_str());
165 arg = arg.default_value(default);
166 }
167 }
168 ArgType::Flag => {
169 arg = arg.action(ArgAction::SetTrue);
170 }
171 ArgType::Option => {
172 arg = arg.required(arg_builder.required);
173 if arg_builder.multiple {
174 arg = arg.num_args(0..);
175 arg = arg.action(ArgAction::Append);
176 } else {
177 arg = arg.num_args(0..=1);
178 }
179 if let Some(default_str) = arg_builder.default_value {
180 let default: &'static str = Box::leak(default_str.into_boxed_str());
181 arg = arg.default_value(default);
182 }
183 }
184 }
185
186 cmd = cmd.arg(arg);
187 }
188
189 let matches = cmd.get_matches();
190 CliMatches { matches }
191 }
192
193 pub fn get_matches(self) -> CliMatches {
195 self.parse()
196 }
197}
198
199impl ArgBuilder {
200 pub fn help(mut self, help: impl Into<String>) -> Self {
202 self.help = Some(help.into());
203 self
204 }
205
206 pub fn short(mut self, short: impl Into<String>) -> Self {
208 self.short = Some(short.into());
209 self
210 }
211
212 pub fn long(mut self, long: impl Into<String>) -> Self {
214 self.long = Some(long.into());
215 self
216 }
217
218 pub fn required(mut self, required: bool) -> Self {
220 self.required = required;
221 self
222 }
223
224 pub fn multiple(mut self, multiple: bool) -> Self {
226 self.multiple = multiple;
227 self
228 }
229
230 pub fn default_value(mut self, value: impl Into<String>) -> Self {
232 self.default_value = Some(value.into());
233 self
234 }
235}
236
237pub struct CliMatches {
239 matches: ArgMatches,
240}
241
242impl CliMatches {
243 pub fn get(&self, name: &str) -> Option<String> {
245 self.matches.get_one::<String>(name).cloned()
246 }
247
248 pub fn value_of(&self, name: &str) -> Option<String> {
250 self.get(name)
251 }
252
253 pub fn is_present(&self, name: &str) -> bool {
255 self.matches.get_flag(name)
256 }
257
258 pub fn get_many(&self, name: &str) -> Vec<String> {
260 self.matches
261 .get_many::<String>(name)
262 .map(|vals| vals.map(|s| s.to_string()).collect())
263 .unwrap_or_default()
264 }
265
266 pub fn values_of(&self, name: &str) -> Option<Vec<String>> {
268 let vals = self
269 .matches
270 .get_many::<String>(name)
271 .map(|vals| vals.map(|s| s.to_string()).collect::<Vec<String>>());
272 vals
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_cli_builder() {
282 let app = new("test")
283 .version("1.0")
284 .author("Test Author")
285 .about("Test app")
286 .arg(arg("pattern").help("Pattern to search").required(true))
287 .arg(flag("verbose").short("v").help("Verbose output"))
288 .arg(
289 option("threads")
290 .short("j")
291 .help("Number of threads")
292 .default_value("4"),
293 );
294
295 assert_eq!(app.name, "test");
296 assert_eq!(app.version, Some("1.0".to_string()));
297 assert_eq!(app.args.len(), 3);
298 }
299
300 #[test]
301 fn test_arg_builder() {
302 let arg = arg("pattern").help("Pattern").required(true);
303 assert_eq!(arg.name, "pattern");
304 assert!(arg.required);
305 }
306
307 #[test]
308 fn test_flag_builder() {
309 let flag = flag("verbose").short("v").help("Verbose");
310 assert_eq!(flag.name, "verbose");
311 matches!(flag.arg_type, ArgType::Flag);
312 }
313
314 #[test]
315 fn test_option_builder() {
316 let opt = option("threads").short("j").default_value("4");
317 assert_eq!(opt.name, "threads");
318 matches!(opt.arg_type, ArgType::Option);
319 }
320}