1 use crate::{Action, Command, Context, Flag, FlagType, Help};
2 
3 /// Multiple action application entry point
4 #[derive(Default)]
5 pub struct App {
6     /// Application name
7     pub name: String,
8     /// Application author
9     pub author: Option<String>,
10     /// Application description
11     pub description: Option<String>,
12     /// Application usage
13     pub usage: Option<String>,
14     /// Application version
15     pub version: Option<String>,
16     /// Application commands
17     pub commands: Option<Vec<Command>>,
18     /// Application action
19     pub action: Option<Action>,
20     /// Application flags
21     pub flags: Option<Vec<Flag>>,
22 }
23 
24 impl App {
25     /// Create new instance of `App`
26     ///
27     /// Example
28     ///
29     /// ```
30     /// use seahorse::App;
31     ///
32     /// let app = App::new("cli");
33     /// ```
new<T: Into<String>>(name: T) -> Self34     pub fn new<T: Into<String>>(name: T) -> Self {
35         Self {
36             name: name.into(),
37             ..Self::default()
38         }
39     }
40 
41     /// Set author of the app
42     ///
43     /// Example
44     ///
45     /// ```
46     /// use seahorse::App;
47     ///
48     /// let app = App::new("cli")
49     ///     .author(env!("CARGO_PKG_AUTHORS"));
50     /// ```
author<T: Into<String>>(mut self, author: T) -> Self51     pub fn author<T: Into<String>>(mut self, author: T) -> Self {
52         self.author = Some(author.into());
53         self
54     }
55 
56     /// Set description of the app
57     ///
58     /// Example
59     ///
60     /// ```
61     /// use seahorse::App;
62     ///
63     /// let app = App::new("cli")
64     ///     .description(env!("CARGO_PKG_DESCRIPTION"));
65     /// ```
description<T: Into<String>>(mut self, description: T) -> Self66     pub fn description<T: Into<String>>(mut self, description: T) -> Self {
67         self.description = Some(description.into());
68         self
69     }
70 
71     /// Set usage of the app
72     ///
73     /// Example
74     ///
75     /// ```
76     /// use seahorse::App;
77     ///
78     /// let app = App::new("cli");
79     /// app.usage("cli [command] [arg]");
80     /// ```
usage<T: Into<String>>(mut self, usage: T) -> Self81     pub fn usage<T: Into<String>>(mut self, usage: T) -> Self {
82         self.usage = Some(usage.into());
83         self
84     }
85 
86     /// Set version of the app
87     ///
88     /// Example
89     ///
90     /// ```
91     /// use seahorse::App;
92     ///
93     /// let app = App::new("cli");
94     /// app.version(env!("CARGO_PKG_VERSION"));
95     /// ```
version<T: Into<String>>(mut self, version: T) -> Self96     pub fn version<T: Into<String>>(mut self, version: T) -> Self {
97         self.version = Some(version.into());
98         self
99     }
100 
101     /// Set command of the app
102     ///
103     /// Example
104     ///
105     /// ```
106     /// use seahorse::{App, Command};
107     ///
108     /// let command = Command::new("hello")
109     ///     .usage("cli hello [arg]")
110     ///     .action(|c| println!("{:?}", c.args));
111     ///
112     /// let app = App::new("cli")
113     ///     .command(command);
114     /// ```
115     ///
116     /// # Panics
117     ///
118     /// You cannot set a command named as same as registered ones.
119     ///
120     /// ```should_panic
121     /// use seahorse::{App, Command};
122     ///
123     /// let command1 = Command::new("hello")
124     ///     .usage("cli hello [arg]")
125     ///     .action(|c| println!("{:?}", c.args));
126     ///
127     /// let command2 = Command::new("hello")
128     ///     .usage("cli hello [arg]")
129     ///     .action(|c| println!("{:?}", c.args));
130     ///
131     /// let app = App::new("cli")
132     ///     .command(command1)
133     ///     .command(command2);
134     /// ```
command(mut self, command: Command) -> Self135     pub fn command(mut self, command: Command) -> Self {
136         if let Some(ref mut commands) = self.commands {
137             if commands
138                 .iter()
139                 .any(|registered| registered.name == command.name)
140             {
141                 panic!(format!(
142                     r#"Command name "{}" is already registered."#,
143                     command.name
144                 ));
145             }
146             (*commands).push(command);
147         } else {
148             self.commands = Some(vec![command]);
149         }
150         self
151     }
152 
153     /// Set action of the app
154     ///
155     /// Example
156     ///
157     /// ```
158     /// use seahorse::{Action, App, Context};
159     ///
160     /// let action: Action = |c: &Context| println!("{:?}", c.args);
161     /// let app = App::new("cli")
162     ///     .action(action);
163     /// ```
action(mut self, action: Action) -> Self164     pub fn action(mut self, action: Action) -> Self {
165         self.action = Some(action);
166         self
167     }
168 
169     /// Set flag of the app
170     ///
171     /// Example
172     ///
173     /// ```
174     /// use seahorse::{App, Flag, FlagType};
175     ///
176     /// let app = App::new("cli")
177     ///     .flag(Flag::new("bool", FlagType::Bool))
178     ///     .flag(Flag::new("int", FlagType::Int));
179     /// ```
flag(mut self, flag: Flag) -> Self180     pub fn flag(mut self, flag: Flag) -> Self {
181         if let Some(ref mut flags) = self.flags {
182             (*flags).push(flag);
183         } else {
184             self.flags = Some(vec![flag]);
185         }
186         self
187     }
188 
189     /// Run app
190     ///
191     /// Example
192     ///
193     /// ```
194     /// use std::env;
195     /// use seahorse::App;
196     ///
197     /// let args: Vec<String> = env::args().collect();
198     /// let app = App::new("cli");
199     /// app.run(args);
200     /// ```
run(&self, args: Vec<String>)201     pub fn run(&self, args: Vec<String>) {
202         let args = Self::normalized_args(args);
203         let (cmd_v, args_v) = match args.len() {
204             1 => args.split_at(1),
205             _ => args[1..].split_at(1),
206         };
207 
208         let cmd = match cmd_v.first() {
209             Some(c) => c,
210             None => {
211                 self.help();
212                 return;
213             }
214         };
215 
216         match self.select_command(&cmd) {
217             Some(command) => command.run(args_v.to_vec()),
218             None => match self.action {
219                 Some(action) => {
220                     if args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) {
221                         self.help();
222                         return;
223                     }
224                     action(&Context::new(
225                         args[1..].to_vec(),
226                         self.flags.clone(),
227                         self.help_text(),
228                     ));
229                 }
230                 None => self.help(),
231             },
232         }
233     }
234 
235     /// Select command
236     /// Gets the Command that matches the string passed in the argument
select_command(&self, cmd: &str) -> Option<&Command>237     fn select_command(&self, cmd: &str) -> Option<&Command> {
238         match &self.commands {
239             Some(commands) => commands.iter().find(|command| match &command.alias {
240                 Some(alias) => command.name == cmd || alias.iter().any(|a| a == cmd),
241                 None => command.name == cmd,
242             }),
243             None => None,
244         }
245     }
246 
247     /// Split arg with "=" to unify arg notations.
248     /// --flag=value => ["--flag", "value"]
249     /// --flag value => ["--flag", "value"]
normalized_args(raw_args: Vec<String>) -> Vec<String>250     fn normalized_args(raw_args: Vec<String>) -> Vec<String> {
251         raw_args.iter().fold(Vec::<String>::new(), |mut acc, cur| {
252             if cur.starts_with('-') && cur.contains('=') {
253                 let mut splitted_flag: Vec<String> =
254                     cur.splitn(2, '=').map(|s| s.to_owned()).collect();
255                 acc.append(&mut splitted_flag);
256             } else {
257                 acc.push(cur.to_owned());
258             }
259             acc
260         })
261     }
262 
flag_help_text(&self) -> String263     fn flag_help_text(&self) -> String {
264         let mut text = String::new();
265         text += "Flags:\n";
266         let help_flag = "-h, --help";
267 
268         if let Some(flags) = &self.flags {
269             let int_val = "<int>";
270             let float_val = "<float>";
271             let string_val = "<string>";
272 
273             let flag_helps = &flags.iter().map(|f| {
274                 let alias = match &f.alias {
275                     Some(alias) => alias
276                         .iter()
277                         .map(|a| format!("-{}", a))
278                         .collect::<Vec<String>>()
279                         .join(", "),
280                     None => String::new(),
281                 };
282                 let val = match f.flag_type {
283                     FlagType::Int => int_val,
284                     FlagType::Float => float_val,
285                     FlagType::String => string_val,
286                     _ => "",
287                 };
288 
289                 let help = if alias.is_empty() {
290                     format!("--{} {}", f.name, val)
291                 } else {
292                     format!("{}, --{} {}", alias, f.name, val)
293                 };
294 
295                 (help, f.description.clone())
296             });
297 
298             let flag_name_max_len = flag_helps
299                 .clone()
300                 .map(|h| h.0.len())
301                 .chain(vec![help_flag.len()].into_iter())
302                 .max()
303                 .unwrap();
304 
305             for flag_help in flag_helps.clone().into_iter() {
306                 text += &format!("\t{}", flag_help.0);
307 
308                 if let Some(usage) = &flag_help.1 {
309                     let flag_name_len = flag_help.0.len();
310                     text += &format!(
311                         "{} : {}\n",
312                         " ".repeat(flag_name_max_len - flag_name_len),
313                         usage
314                     );
315                 }
316             }
317 
318             text += &format!(
319                 "\t{}{} : Show help\n",
320                 help_flag,
321                 " ".repeat(flag_name_max_len - help_flag.len())
322             );
323         } else {
324             text += &format!("\t{} : Show help\n", help_flag);
325         }
326 
327         text
328     }
329 
command_help_text(&self) -> String330     fn command_help_text(&self) -> String {
331         let mut text = String::new();
332 
333         if let Some(commands) = &self.commands {
334             text += "\nCommands:\n";
335 
336             let name_max_len = &commands
337                 .iter()
338                 .map(|c| {
339                     if let Some(alias) = &c.alias {
340                         format!("{}, {}", alias.join(", "), c.name).len()
341                     } else {
342                         c.name.len()
343                     }
344                 })
345                 .max()
346                 .unwrap();
347 
348             for c in commands {
349                 let command_name = if let Some(alias) = &c.alias {
350                     format!("{}, {}", alias.join(", "), c.name)
351                 } else {
352                     c.name.clone()
353                 };
354 
355                 let description = match &c.description {
356                     Some(description) => description,
357                     None => "",
358                 };
359 
360                 text += &format!(
361                     "\t{} {}: {}\n",
362                     command_name,
363                     " ".repeat(name_max_len - command_name.len()),
364                     description
365                 );
366             }
367 
368             text += "\n"
369         }
370 
371         text
372     }
373 }
374 
375 impl Help for App {
help_text(&self) -> String376     fn help_text(&self) -> String {
377         let mut text = String::new();
378 
379         text += &format!("Name\n\t{}\n\n", self.name);
380 
381         if let Some(author) = &self.author {
382             text += &format!("Author:\n\t{}\n\n", author);
383         }
384 
385         if let Some(description) = &self.description {
386             text += &format!("Description:\n\t{}\n\n", description);
387         }
388 
389         if let Some(usage) = &self.usage {
390             text += &format!("Usage:\n\t{}\n\n", usage);
391         }
392 
393         text += &self.flag_help_text();
394         text += &self.command_help_text();
395 
396         if let Some(version) = &self.version {
397             text += &format!("Version:\n\t{}\n", version);
398         }
399 
400         text
401     }
402 }
403 
404 #[cfg(test)]
405 mod tests {
406     use crate::{Action, App, Command, Context, Flag, FlagType};
407 
408     #[test]
app_new_only_test()409     fn app_new_only_test() {
410         let app = App::new("cli");
411         app.run(vec!["cli".to_string()]);
412 
413         assert_eq!(app.name, "cli".to_string());
414         assert_eq!(app.usage, None);
415         assert_eq!(app.author, None);
416         assert_eq!(app.description, None);
417         assert_eq!(app.version, None);
418     }
419 
420     #[test]
multiple_app_test()421     fn multiple_app_test() {
422         let a: Action = |c: &Context| {
423             assert_eq!(true, c.bool_flag("bool"));
424             match c.string_flag("string") {
425                 Ok(flag) => assert_eq!("string".to_string(), flag),
426                 _ => assert!(false, "string test false..."),
427             }
428             match c.int_flag("int") {
429                 Ok(flag) => assert_eq!(100, flag),
430                 _ => assert!(false, "int test false..."),
431             }
432             match c.float_flag("float") {
433                 Ok(flag) => assert_eq!(1.23, flag),
434                 _ => assert!(false, "float test false..."),
435             }
436         };
437         let c = Command::new("hello")
438             .alias("h")
439             .description("hello command")
440             .usage("test hello(h) args")
441             .action(a)
442             .flag(Flag::new("bool", FlagType::Bool))
443             .flag(Flag::new("string", FlagType::String))
444             .flag(Flag::new("int", FlagType::Int))
445             .flag(Flag::new("float", FlagType::Float));
446 
447         let app = App::new("test")
448             .author("Author <author@example.com>")
449             .description("This is a great tool.")
450             .usage("test [command] [arg]")
451             .version("0.0.1")
452             .command(c);
453 
454         app.run(vec![
455             "test".to_string(),
456             "hello".to_string(),
457             "args".to_string(),
458             "--bool".to_string(),
459             "--string".to_string(),
460             "string".to_string(),
461             "--int".to_string(),
462             "100".to_string(),
463             "--float".to_string(),
464             "1.23".to_string(),
465         ]);
466 
467         app.run(vec![
468             "test".to_string(),
469             "h".to_string(),
470             "args".to_string(),
471             "--bool".to_string(),
472             "--string".to_string(),
473             "string".to_string(),
474             "--int".to_string(),
475             "100".to_string(),
476             "--float".to_string(),
477             "1.23".to_string(),
478         ]);
479 
480         assert_eq!(app.name, "test".to_string());
481         assert_eq!(app.usage, Some("test [command] [arg]".to_string()));
482         assert_eq!(app.author, Some("Author <author@example.com>".to_string()));
483         assert_eq!(app.description, Some("This is a great tool.".to_string()));
484         assert_eq!(app.version, Some("0.0.1".to_string()));
485     }
486 
487     #[test]
single_app_test()488     fn single_app_test() {
489         let action: Action = |c: &Context| {
490             assert_eq!(true, c.bool_flag("bool"));
491             match c.string_flag("string") {
492                 Ok(flag) => assert_eq!("string".to_string(), flag),
493                 _ => assert!(false, "string test false..."),
494             }
495             match c.int_flag("int") {
496                 Ok(flag) => assert_eq!(100, flag),
497                 _ => assert!(false, "int test false..."),
498             }
499             match c.float_flag("float") {
500                 Ok(flag) => assert_eq!(1.23, flag),
501                 _ => assert!(false, "float test false..."),
502             }
503         };
504 
505         let app = App::new("test")
506             .author("Author <author@example.com>")
507             .description("This is a great tool.")
508             .usage("test [arg]")
509             .version("0.0.1")
510             .action(action)
511             .flag(Flag::new("bool", FlagType::Bool))
512             .flag(Flag::new("string", FlagType::String))
513             .flag(Flag::new("int", FlagType::Int))
514             .flag(Flag::new("float", FlagType::Float));
515 
516         app.run(vec![
517             "test".to_string(),
518             "args".to_string(),
519             "--bool".to_string(),
520             "--string".to_string(),
521             "string".to_string(),
522             "--int".to_string(),
523             "100".to_string(),
524             "--float".to_string(),
525             "1.23".to_string(),
526         ]);
527 
528         assert_eq!(app.name, "test".to_string());
529         assert_eq!(app.usage, Some("test [arg]".to_string()));
530         assert_eq!(app.author, Some("Author <author@example.com>".to_string()));
531         assert_eq!(app.description, Some("This is a great tool.".to_string()));
532         assert_eq!(app.version, Some("0.0.1".to_string()));
533     }
534 
535     #[test]
flag_only_app_test()536     fn flag_only_app_test() {
537         let action: Action = |c: &Context| {
538             assert_eq!(true, c.bool_flag("bool"));
539             match c.string_flag("string") {
540                 Ok(flag) => assert_eq!("string".to_string(), flag),
541                 _ => assert!(false, "string test false..."),
542             }
543             match c.int_flag("int") {
544                 Ok(flag) => assert_eq!(100, flag),
545                 _ => assert!(false, "int test false..."),
546             }
547             match c.float_flag("float") {
548                 Ok(flag) => assert_eq!(1.23, flag),
549                 _ => assert!(false, "float test false..."),
550             }
551         };
552 
553         let app = App::new("test")
554             .author("Author <author@example.com>")
555             .description("This is a great tool.")
556             .usage("test")
557             .version("0.0.1")
558             .action(action)
559             .flag(Flag::new("bool", FlagType::Bool))
560             .flag(Flag::new("string", FlagType::String))
561             .flag(Flag::new("int", FlagType::Int))
562             .flag(Flag::new("float", FlagType::Float));
563 
564         app.run(vec![
565             "test".to_string(),
566             "--bool".to_string(),
567             "--string".to_string(),
568             "string".to_string(),
569             "--int".to_string(),
570             "100".to_string(),
571             "--float".to_string(),
572             "1.23".to_string(),
573         ]);
574 
575         assert_eq!(app.name, "test".to_string());
576         assert_eq!(app.usage, Some("test".to_string()));
577         assert_eq!(app.author, Some("Author <author@example.com>".to_string()));
578         assert_eq!(app.description, Some("This is a great tool.".to_string()));
579         assert_eq!(app.version, Some("0.0.1".to_string()));
580     }
581 
582     #[test]
single_app_equal_notation_test()583     fn single_app_equal_notation_test() {
584         let action: Action = |c: &Context| {
585             assert_eq!(true, c.bool_flag("bool"));
586             match c.string_flag("string") {
587                 Ok(flag) => assert_eq!("str=ing".to_string(), flag),
588                 _ => assert!(false, "string test false..."),
589             }
590             match c.int_flag("int") {
591                 Ok(flag) => assert_eq!(100, flag),
592                 _ => assert!(false, "int test false..."),
593             }
594             match c.float_flag("float") {
595                 Ok(flag) => assert_eq!(1.23, flag),
596                 _ => assert!(false, "float test false..."),
597             }
598         };
599 
600         let app = App::new("test")
601             .author("Author <author@example.com>")
602             .description("This is a great tool.")
603             .usage("test [arg]")
604             .version("0.0.1")
605             .action(action)
606             .flag(Flag::new("bool", FlagType::Bool))
607             .flag(Flag::new("string", FlagType::String))
608             .flag(Flag::new("int", FlagType::Int))
609             .flag(Flag::new("float", FlagType::Float).alias("f"));
610 
611         app.run(vec![
612             "test".to_string(),
613             "args".to_string(),
614             "--bool".to_string(),
615             "--string=str=ing".to_string(),
616             "--int=100".to_string(),
617             "-f=1.23".to_string(),
618         ]);
619 
620         assert_eq!(app.name, "test".to_string());
621         assert_eq!(app.usage, Some("test [arg]".to_string()));
622         assert_eq!(app.author, Some("Author <author@example.com>".to_string()));
623         assert_eq!(app.description, Some("This is a great tool.".to_string()));
624         assert_eq!(app.version, Some("0.0.1".to_string()));
625     }
626 }
627