flagon-io/hello

public

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

hello/src/lib.rs

319 lines9,126 bytesCodeBlame
1//! Greetings.
2
3/// A supported greeting language.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum Lang {
6 #[default]
7 En,
8 Es,
9 Fr,
10}
11
12impl Lang {
13 /// Parses a language code (`en`, `es` or `fr`).
14 pub fn parse(code: &str) -> Option<Lang> {
15 match code {
16 "en" => Some(Lang::En),
17 "es" => Some(Lang::Es),
18 "fr" => Some(Lang::Fr),
19 _ => None,
20 }
21 }
22}
23
24/// Parsed command-line options.
25#[derive(Debug, PartialEq, Eq)]
26pub struct Options {
27 /// The name to greet, if one was given.
28 pub name: Option<String>,
29 /// Whether to upper-case the greeting (`--shout`).
30 pub shout: bool,
31 /// Whether to print the crate version and exit (`--version`).
32 pub version: bool,
33 /// How many times to print the greeting (`--times N`).
34 pub times: usize,
35 /// The greeting language (`--lang CODE`).
36 pub lang: Lang,
37 /// Whether to print nothing (`--quiet`).
38 pub quiet: bool,
39 /// Whether to end the greeting with three exclamation marks (`--loud`).
40 pub loud: bool,
41}
42
43impl Default for Options {
44 fn default() -> Self {
45 Options {
46 name: None,
47 shout: false,
48 version: false,
49 times: 1,
50 lang: Lang::En,
51 quiet: false,
52 loud: false,
53 }
54 }
55}
56
57/// Parses command-line arguments (excluding the program name).
58///
59/// `--shout` sets [`Options::shout`], `--version` sets
60/// [`Options::version`], and `--times N` sets [`Options::times`] (the
61/// next argument is consumed as the value), and `--lang CODE` sets
62/// [`Options::lang`] (likewise consuming the next argument); other
63/// arguments starting with `--` are treated as flags and skipped. The
64/// name is the first remaining argument.
65///
66/// Returns an error message if `--times` has a missing or non-numeric
67/// value, or if `--lang` has a missing or unsupported value.
68pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> {
69 let mut options = Options::default();
70 let mut args = args.into_iter();
71 while let Some(arg) = args.next() {
72 if arg == "--shout" {
73 options.shout = true;
74 } else if arg == "--version" {
75 options.version = true;
76 } else if arg == "--quiet" {
77 options.quiet = true;
78 } else if arg == "--loud" || arg == "-L" {
79 options.loud = true;
80 } else if arg == "--times" {
81 let value = args
82 .next()
83 .ok_or_else(|| "--times requires a value".to_owned())?;
84 options.times = value
85 .parse()
86 .map_err(|_| format!("--times value must be a number, got {value:?}"))?;
87 } else if arg == "--lang" {
88 let value = args
89 .next()
90 .ok_or_else(|| "--lang requires a value".to_owned())?;
91 options.lang = Lang::parse(&value).ok_or_else(|| {
92 format!("--lang value must be one of en, es, fr, got {value:?}")
93 })?;
94 } else if !arg.starts_with("--") && options.name.is_none() {
95 options.name = Some(arg);
96 }
97 }
98 Ok(options)
99}
100
101/// `text` upper-cased.
102pub fn shout(text: &str) -> String {
103 text.to_uppercase()
104}
105
106/// `text` with its final exclamation mark replaced by three.
107pub fn loud(text: &str) -> String {
108 format!("{}!!!", text.strip_suffix('!').unwrap_or(text))
109}
110
111/// The greeting for `name`.
112pub fn greet(name: &str) -> String {
113 greeting_in(name, Lang::En)
114}
115
116/// The greeting for `name` in `lang`.
117pub fn greeting_in(name: &str, lang: Lang) -> String {
118 let trimmed = name.trim();
119 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
120 match lang {
121 Lang::En => format!("Hi, {display_name}!"),
122 Lang::Es => format!("¡Hola, {display_name}!"),
123 Lang::Fr => format!("Bonjour, {display_name} !"),
124 }
125}
126
127/// The farewell for `name`.
128pub fn farewell(name: &str) -> String {
129 let trimmed = name.trim();
130 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
131 format!("Goodbye, {display_name}!")
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn args(list: &[&str]) -> Vec<String> {
139 list.iter().map(|s| s.to_string()).collect()
140 }
141
142 #[test]
143 fn parse_args_quiet() {
144 assert!(parse_args(args(&["--quiet", "Ada"])).unwrap().quiet);
145 assert!(!parse_args(args(&["Ada"])).unwrap().quiet);
146 }
147
148 #[test]
149 fn parse_args_loud() {
150 assert!(parse_args(args(&["--loud", "Ada"])).unwrap().loud);
151 assert!(parse_args(args(&["-L", "Ada"])).unwrap().loud);
152 assert!(!parse_args(args(&["Ada"])).unwrap().loud);
153 }
154
155 #[test]
156 fn loud_uses_three_exclamation_marks() {
157 assert_eq!(loud(&greet("Ada")), "Hi, Ada!!!");
158 }
159
160 #[test]
161 fn parse_args_no_args() {
162 assert_eq!(parse_args(args(&[])).unwrap().name, None);
163 }
164
165 #[test]
166 fn parse_args_name() {
167 assert_eq!(parse_args(args(&["Ada"])).unwrap().name.as_deref(), Some("Ada"));
168 }
169
170 #[test]
171 fn parse_args_skips_leading_flag() {
172 assert_eq!(
173 parse_args(args(&["--unknown", "Ada"])).unwrap().name.as_deref(),
174 Some("Ada")
175 );
176 }
177
178 #[test]
179 fn parse_args_shout_defaults_off() {
180 assert!(!parse_args(args(&["Ada"])).unwrap().shout);
181 }
182
183 #[test]
184 fn parse_args_shout_before_name() {
185 let options = parse_args(args(&["--shout", "Ada"])).unwrap();
186 assert!(options.shout);
187 assert_eq!(options.name.as_deref(), Some("Ada"));
188 }
189
190 #[test]
191 fn parse_args_shout_after_name() {
192 let options = parse_args(args(&["Ada", "--shout"])).unwrap();
193 assert!(options.shout);
194 assert_eq!(options.name.as_deref(), Some("Ada"));
195 }
196
197 #[test]
198 fn parse_args_shout_alone() {
199 let options = parse_args(args(&["--shout"])).unwrap();
200 assert!(options.shout);
201 assert_eq!(options.name, None);
202 }
203
204 #[test]
205 fn parse_args_version_defaults_off() {
206 assert!(!parse_args(args(&["Ada"])).unwrap().version);
207 }
208
209 #[test]
210 fn parse_args_version() {
211 let options = parse_args(args(&["--version"])).unwrap();
212 assert!(options.version);
213 assert_eq!(options.name, None);
214 }
215
216 #[test]
217 fn parse_args_times_defaults_to_one() {
218 assert_eq!(parse_args(args(&["Ada"])).unwrap().times, 1);
219 }
220
221 #[test]
222 fn parse_args_times_valid() {
223 let options = parse_args(args(&["--times", "3", "Ada"])).unwrap();
224 assert_eq!(options.times, 3);
225 assert_eq!(options.name.as_deref(), Some("Ada"));
226 }
227
228 #[test]
229 fn parse_args_times_missing_value() {
230 assert!(parse_args(args(&["--times"])).is_err());
231 }
232
233 #[test]
234 fn parse_args_times_non_number() {
235 assert!(parse_args(args(&["--times", "abc", "Ada"])).is_err());
236 }
237
238 #[test]
239 fn parse_args_lang_defaults_to_en() {
240 assert_eq!(parse_args(args(&["Ada"])).unwrap().lang, Lang::En);
241 }
242
243 #[test]
244 fn parse_args_lang_valid() {
245 let options = parse_args(args(&["--lang", "es", "Ana"])).unwrap();
246 assert_eq!(options.lang, Lang::Es);
247 assert_eq!(options.name.as_deref(), Some("Ana"));
248 assert_eq!(parse_args(args(&["--lang", "fr"])).unwrap().lang, Lang::Fr);
249 }
250
251 #[test]
252 fn parse_args_lang_missing_value() {
253 assert_eq!(
254 parse_args(args(&["--lang"])).unwrap_err(),
255 "--lang requires a value"
256 );
257 }
258
259 #[test]
260 fn parse_args_lang_unknown() {
261 assert_eq!(
262 parse_args(args(&["--lang", "de"])).unwrap_err(),
263 "--lang value must be one of en, es, fr, got \"de\""
264 );
265 }
266
267 #[test]
268 fn greeting_in_each_language() {
269 assert_eq!(greeting_in("Ada", Lang::En), "Hi, Ada!");
270 assert_eq!(greeting_in("Ana", Lang::Es), "¡Hola, Ana!");
271 assert_eq!(greeting_in("Luc", Lang::Fr), "Bonjour, Luc !");
272 }
273
274 #[test]
275 fn greeting_in_trims_and_falls_back_to_world() {
276 assert_eq!(greeting_in(" Ana ", Lang::Es), "¡Hola, Ana!");
277 assert_eq!(greeting_in(" ", Lang::Fr), "Bonjour, world !");
278 }
279
280 #[test]
281 fn shout_upper_cases_greeting() {
282 assert_eq!(shout(&greet("Ada")), "HI, ADA!");
283 }
284
285 #[test]
286 fn greets_by_name() {
287 assert_eq!(greet("Ada"), "Hi, Ada!");
288 }
289
290 #[test]
291 fn greets_world_with_empty_string() {
292 assert_eq!(greet(""), "Hi, world!");
293 }
294
295 #[test]
296 fn greets_world_with_whitespace_only() {
297 assert_eq!(greet(" "), "Hi, world!");
298 }
299
300 #[test]
301 fn trims_whitespace_from_name() {
302 assert_eq!(greet(" Ada "), "Hi, Ada!");
303 }
304
305 #[test]
306 fn says_goodbye_by_name() {
307 assert_eq!(farewell("Ada"), "Goodbye, Ada!");
308 }
309
310 #[test]
311 fn says_goodbye_to_world_when_blank() {
312 assert_eq!(farewell(" "), "Goodbye, world!");
313 }
314
315 #[test]
316 fn farewell_trims_whitespace() {
317 assert_eq!(farewell(" Ada "), "Goodbye, Ada!");
318 }
319}