flagon-io/hello

public

Greets people from the command line. The sample repository for g1t.

hello/src/lib.rs

202 lines5,521 bytesCodeBlame
1//! Greetings.
2
3/// Parsed command-line options.
4#[derive(Debug, PartialEq, Eq)]
5pub struct Options {
6 /// The name to greet, if one was given.
7 pub name: Option<String>,
8 /// Whether to upper-case the greeting (`--shout`).
9 pub shout: bool,
10 /// Whether to print the crate version and exit (`--version`).
11 pub version: bool,
12 /// How many times to print the greeting (`--times N`).
13 pub times: usize,
14}
15
16impl Default for Options {
17 fn default() -> Self {
18 Options {
19 name: None,
20 shout: false,
21 version: false,
22 times: 1,
23 }
24 }
25}
26
27/// Parses command-line arguments (excluding the program name).
28///
29/// `--shout` sets [`Options::shout`], `--version` sets
30/// [`Options::version`], and `--times N` sets [`Options::times`] (the
31/// next argument is consumed as the value); other arguments starting
32/// with `--` are treated as flags and skipped. The name is the first
33/// remaining argument.
34///
35/// Returns an error message if `--times` has a missing or non-numeric value.
36pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> {
37 let mut options = Options::default();
38 let mut args = args.into_iter();
39 while let Some(arg) = args.next() {
40 if arg == "--shout" {
41 options.shout = true;
42 } else if arg == "--version" {
43 options.version = true;
44 } else if arg == "--times" {
45 let value = args
46 .next()
47 .ok_or_else(|| "--times requires a value".to_owned())?;
48 options.times = value
49 .parse()
50 .map_err(|_| format!("--times value must be a number, got {value:?}"))?;
51 } else if !arg.starts_with("--") && options.name.is_none() {
52 options.name = Some(arg);
53 }
54 }
55 Ok(options)
56}
57
58/// `text` upper-cased.
59pub fn shout(text: &str) -> String {
60 text.to_uppercase()
61}
62
63/// The greeting for `name`.
64pub fn greeting(name: &str) -> String {
65 let trimmed = name.trim();
66 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
67 format!("Hello, {display_name}!")
68}
69
70/// The farewell for `name`.
71pub fn farewell(name: &str) -> String {
72 let trimmed = name.trim();
73 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
74 format!("Goodbye, {display_name}!")
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn args(list: &[&str]) -> Vec<String> {
82 list.iter().map(|s| s.to_string()).collect()
83 }
84
85 #[test]
86 fn parse_args_no_args() {
87 assert_eq!(parse_args(args(&[])).unwrap().name, None);
88 }
89
90 #[test]
91 fn parse_args_name() {
92 assert_eq!(parse_args(args(&["Ada"])).unwrap().name.as_deref(), Some("Ada"));
93 }
94
95 #[test]
96 fn parse_args_skips_leading_flag() {
97 assert_eq!(
98 parse_args(args(&["--unknown", "Ada"])).unwrap().name.as_deref(),
99 Some("Ada")
100 );
101 }
102
103 #[test]
104 fn parse_args_shout_defaults_off() {
105 assert!(!parse_args(args(&["Ada"])).unwrap().shout);
106 }
107
108 #[test]
109 fn parse_args_shout_before_name() {
110 let options = parse_args(args(&["--shout", "Ada"])).unwrap();
111 assert!(options.shout);
112 assert_eq!(options.name.as_deref(), Some("Ada"));
113 }
114
115 #[test]
116 fn parse_args_shout_after_name() {
117 let options = parse_args(args(&["Ada", "--shout"])).unwrap();
118 assert!(options.shout);
119 assert_eq!(options.name.as_deref(), Some("Ada"));
120 }
121
122 #[test]
123 fn parse_args_shout_alone() {
124 let options = parse_args(args(&["--shout"])).unwrap();
125 assert!(options.shout);
126 assert_eq!(options.name, None);
127 }
128
129 #[test]
130 fn parse_args_version_defaults_off() {
131 assert!(!parse_args(args(&["Ada"])).unwrap().version);
132 }
133
134 #[test]
135 fn parse_args_version() {
136 let options = parse_args(args(&["--version"])).unwrap();
137 assert!(options.version);
138 assert_eq!(options.name, None);
139 }
140
141 #[test]
142 fn parse_args_times_defaults_to_one() {
143 assert_eq!(parse_args(args(&["Ada"])).unwrap().times, 1);
144 }
145
146 #[test]
147 fn parse_args_times_valid() {
148 let options = parse_args(args(&["--times", "3", "Ada"])).unwrap();
149 assert_eq!(options.times, 3);
150 assert_eq!(options.name.as_deref(), Some("Ada"));
151 }
152
153 #[test]
154 fn parse_args_times_missing_value() {
155 assert!(parse_args(args(&["--times"])).is_err());
156 }
157
158 #[test]
159 fn parse_args_times_non_number() {
160 assert!(parse_args(args(&["--times", "abc", "Ada"])).is_err());
161 }
162
163 #[test]
164 fn shout_upper_cases_greeting() {
165 assert_eq!(shout(&greeting("Ada")), "HELLO, ADA!");
166 }
167
168 #[test]
169 fn greets_by_name() {
170 assert_eq!(greeting("Ada"), "Hello, Ada!");
171 }
172
173 #[test]
174 fn greets_world_with_empty_string() {
175 assert_eq!(greeting(""), "Hello, world!");
176 }
177
178 #[test]
179 fn greets_world_with_whitespace_only() {
180 assert_eq!(greeting(" "), "Hello, world!");
181 }
182
183 #[test]
184 fn trims_whitespace_from_name() {
185 assert_eq!(greeting(" Ada "), "Hello, Ada!");
186 }
187
188 #[test]
189 fn says_goodbye_by_name() {
190 assert_eq!(farewell("Ada"), "Goodbye, Ada!");
191 }
192
193 #[test]
194 fn says_goodbye_to_world_when_blank() {
195 assert_eq!(farewell(" "), "Goodbye, world!");
196 }
197
198 #[test]
199 fn farewell_trims_whitespace() {
200 assert_eq!(farewell(" Ada "), "Goodbye, Ada!");
201 }
202}