flagon-io/hello

public

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

hello/src/lib.rs

417 lines12,068 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 lower-case the greeting and end it with `...` (`--whisper`).
32 pub whisper: bool,
33 /// Whether to spell the name backwards (`--reverse`).
34 pub reverse: bool,
35 /// Whether to print the crate version and exit (`--version`).
36 pub version: bool,
37 /// How many times to print the greeting (`--times N`).
38 pub times: usize,
39 /// The greeting language (`--lang CODE`).
40 pub lang: Lang,
41 /// Whether to print nothing (`--quiet`).
42 pub quiet: bool,
43 /// Whether to end the greeting with three exclamation marks (`--loud`).
44 pub loud: bool,
45 /// Whether to print a goodbye after the greeting (`--bye`).
46 pub bye: bool,
47 /// Whether to print a wave after the greeting (`--wave`).
48 pub wave: bool,
49 /// Whether to print the greeting and a goodbye on one line (`--both`).
50 pub both: bool,
51}
52
53impl Default for Options {
54 fn default() -> Self {
55 Options {
56 name: None,
57 shout: false,
58 whisper: false,
59 reverse: false,
60 version: false,
61 times: 1,
62 lang: Lang::En,
63 quiet: false,
64 loud: false,
65 bye: false,
66 wave: false,
67 both: false,
68 }
69 }
70}
71
72/// Parses command-line arguments (excluding the program name).
73///
74/// `--shout` sets [`Options::shout`], `--version` sets
75/// [`Options::version`], and `--times N` sets [`Options::times`] (the
76/// next argument is consumed as the value), and `--lang CODE` sets
77/// [`Options::lang`] (likewise consuming the next argument); other
78/// arguments starting with `--` are treated as flags and skipped. The
79/// name is the first remaining argument.
80///
81/// Returns an error message if `--times` has a missing or non-numeric
82/// value, or if `--lang` has a missing or unsupported value.
83pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> {
84 let mut options = Options::default();
85 let mut args = args.into_iter();
86 while let Some(arg) = args.next() {
87 if arg == "--shout" {
88 options.shout = true;
89 options.whisper = false;
90 } else if arg == "--whisper" {
91 options.whisper = true;
92 options.shout = false;
93 } else if arg == "--reverse" {
94 options.reverse = true;
95 } else if arg == "--version" {
96 options.version = true;
97 } else if arg == "--quiet" {
98 options.quiet = true;
99 } else if arg == "--loud" || arg == "-L" {
100 options.loud = true;
101 } else if arg == "--bye" {
102 options.bye = true;
103 } else if arg == "--wave" {
104 options.wave = true;
105 } else if arg == "--both" {
106 options.both = true;
107 } else if arg == "--times" {
108 let value = args
109 .next()
110 .ok_or_else(|| "--times requires a value".to_owned())?;
111 options.times = value
112 .parse()
113 .map_err(|_| format!("--times value must be a number, got {value:?}"))?;
114 } else if arg == "--lang" {
115 let value = args
116 .next()
117 .ok_or_else(|| "--lang requires a value".to_owned())?;
118 options.lang = Lang::parse(&value)
119 .ok_or_else(|| format!("--lang value must be one of en, es, fr, got {value:?}"))?;
120 } else if !arg.starts_with("--") && options.name.is_none() {
121 options.name = Some(arg);
122 }
123 }
124 Ok(options)
125}
126
127/// `name` spelled backwards.
128pub fn reverse(name: &str) -> String {
129 name.chars().rev().collect()
130}
131
132/// `text` upper-cased.
133pub fn shout(text: &str) -> String {
134 text.to_uppercase()
135}
136
137/// `text` lower-cased, with its final exclamation mark replaced by `...`.
138pub fn whisper(text: &str) -> String {
139 let body = text.to_lowercase();
140 format!("{}...", body.trim_end_matches('!').trim_end())
141}
142
143/// `text` with its final exclamation mark replaced by three.
144pub fn loud(text: &str) -> String {
145 format!("{}!!!", text.strip_suffix('!').unwrap_or(text))
146}
147
148/// The greeting for `name`.
149pub fn hail(name: &str) -> String {
150 greeting_in(name, Lang::En)
151}
152
153/// The greeting for `name` in `lang`.
154pub fn greeting_in(name: &str, lang: Lang) -> String {
155 let trimmed = name.trim();
156 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
157 match lang {
158 Lang::En => format!("Hi, {display_name}!"),
159 Lang::Es => format!("¡Hola, {display_name}!"),
160 Lang::Fr => format!("Bonjour, {display_name} !"),
161 }
162}
163
164/// The wave printed by `--wave`.
165pub fn wave() -> &'static str {
166 "o/"
167}
168
169/// The farewell for `name`.
170pub fn part(name: &str) -> String {
171 let trimmed = name.trim();
172 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
173 format!("Goodbye, {display_name}!")
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn args(list: &[&str]) -> Vec<String> {
181 list.iter().map(|s| s.to_string()).collect()
182 }
183
184 #[test]
185 fn parse_args_quiet() {
186 assert!(parse_args(args(&["--quiet", "Ada"])).unwrap().quiet);
187 assert!(!parse_args(args(&["Ada"])).unwrap().quiet);
188 }
189
190 #[test]
191 fn parse_args_bye() {
192 assert!(parse_args(args(&["--bye", "Ada"])).unwrap().bye);
193 assert!(!parse_args(args(&["Ada"])).unwrap().bye);
194 }
195
196 #[test]
197 fn parse_args_wave() {
198 assert!(parse_args(args(&["--wave", "Ada"])).unwrap().wave);
199 assert!(!parse_args(args(&["Ada"])).unwrap().wave);
200 }
201
202 #[test]
203 fn parse_args_both() {
204 assert!(parse_args(args(&["--both", "Ada"])).unwrap().both);
205 assert!(!parse_args(args(&["Ada"])).unwrap().both);
206 }
207
208 #[test]
209 fn wave_is_o_slash() {
210 assert_eq!(wave(), "o/");
211 }
212
213 #[test]
214 fn parse_args_loud() {
215 assert!(parse_args(args(&["--loud", "Ada"])).unwrap().loud);
216 assert!(parse_args(args(&["-L", "Ada"])).unwrap().loud);
217 assert!(!parse_args(args(&["Ada"])).unwrap().loud);
218 }
219
220 #[test]
221 fn loud_uses_three_exclamation_marks() {
222 assert_eq!(loud(&hail("Ada")), "Hi, Ada!!!");
223 }
224
225 #[test]
226 fn parse_args_no_args() {
227 assert_eq!(parse_args(args(&[])).unwrap().name, None);
228 }
229
230 #[test]
231 fn parse_args_name() {
232 assert_eq!(
233 parse_args(args(&["Ada"])).unwrap().name.as_deref(),
234 Some("Ada")
235 );
236 }
237
238 #[test]
239 fn parse_args_skips_leading_flag() {
240 assert_eq!(
241 parse_args(args(&["--unknown", "Ada"]))
242 .unwrap()
243 .name
244 .as_deref(),
245 Some("Ada")
246 );
247 }
248
249 #[test]
250 fn parse_args_shout_defaults_off() {
251 assert!(!parse_args(args(&["Ada"])).unwrap().shout);
252 }
253
254 #[test]
255 fn parse_args_shout_before_name() {
256 let options = parse_args(args(&["--shout", "Ada"])).unwrap();
257 assert!(options.shout);
258 assert_eq!(options.name.as_deref(), Some("Ada"));
259 }
260
261 #[test]
262 fn parse_args_shout_after_name() {
263 let options = parse_args(args(&["Ada", "--shout"])).unwrap();
264 assert!(options.shout);
265 assert_eq!(options.name.as_deref(), Some("Ada"));
266 }
267
268 #[test]
269 fn parse_args_shout_alone() {
270 let options = parse_args(args(&["--shout"])).unwrap();
271 assert!(options.shout);
272 assert_eq!(options.name, None);
273 }
274
275 #[test]
276 fn parse_args_version_defaults_off() {
277 assert!(!parse_args(args(&["Ada"])).unwrap().version);
278 }
279
280 #[test]
281 fn parse_args_version() {
282 let options = parse_args(args(&["--version"])).unwrap();
283 assert!(options.version);
284 assert_eq!(options.name, None);
285 }
286
287 #[test]
288 fn parse_args_times_defaults_to_one() {
289 assert_eq!(parse_args(args(&["Ada"])).unwrap().times, 1);
290 }
291
292 #[test]
293 fn parse_args_times_valid() {
294 let options = parse_args(args(&["--times", "3", "Ada"])).unwrap();
295 assert_eq!(options.times, 3);
296 assert_eq!(options.name.as_deref(), Some("Ada"));
297 }
298
299 #[test]
300 fn parse_args_times_missing_value() {
301 assert!(parse_args(args(&["--times"])).is_err());
302 }
303
304 #[test]
305 fn parse_args_times_non_number() {
306 assert!(parse_args(args(&["--times", "abc", "Ada"])).is_err());
307 }
308
309 #[test]
310 fn parse_args_lang_defaults_to_en() {
311 assert_eq!(parse_args(args(&["Ada"])).unwrap().lang, Lang::En);
312 }
313
314 #[test]
315 fn parse_args_lang_valid() {
316 let options = parse_args(args(&["--lang", "es", "Ana"])).unwrap();
317 assert_eq!(options.lang, Lang::Es);
318 assert_eq!(options.name.as_deref(), Some("Ana"));
319 assert_eq!(parse_args(args(&["--lang", "fr"])).unwrap().lang, Lang::Fr);
320 }
321
322 #[test]
323 fn parse_args_lang_missing_value() {
324 assert_eq!(
325 parse_args(args(&["--lang"])).unwrap_err(),
326 "--lang requires a value"
327 );
328 }
329
330 #[test]
331 fn parse_args_lang_unknown() {
332 assert_eq!(
333 parse_args(args(&["--lang", "de"])).unwrap_err(),
334 "--lang value must be one of en, es, fr, got \"de\""
335 );
336 }
337
338 #[test]
339 fn greeting_in_each_language() {
340 assert_eq!(greeting_in("Ada", Lang::En), "Hi, Ada!");
341 assert_eq!(greeting_in("Ana", Lang::Es), "¡Hola, Ana!");
342 assert_eq!(greeting_in("Luc", Lang::Fr), "Bonjour, Luc !");
343 }
344
345 #[test]
346 fn greeting_in_trims_and_falls_back_to_world() {
347 assert_eq!(greeting_in(" Ana ", Lang::Es), "¡Hola, Ana!");
348 assert_eq!(greeting_in(" ", Lang::Fr), "Bonjour, world !");
349 }
350
351 #[test]
352 fn shout_upper_cases_greeting() {
353 assert_eq!(shout(&hail("Ada")), "HI, ADA!");
354 }
355
356 #[test]
357 fn whisper_lower_cases_and_trails_off() {
358 assert_eq!(whisper(&hail("Ada")), "hi, ada...");
359 assert_eq!(whisper(&greeting_in("Luc", Lang::Fr)), "bonjour, luc...");
360 }
361
362 #[test]
363 fn parse_args_whisper_and_shout_last_wins() {
364 let o = parse_args(args(&["--whisper", "Ada"])).unwrap();
365 assert!(o.whisper && !o.shout);
366 let o = parse_args(args(&["--shout", "--whisper"])).unwrap();
367 assert!(o.whisper && !o.shout);
368 let o = parse_args(args(&["--whisper", "--shout"])).unwrap();
369 assert!(o.shout && !o.whisper);
370 }
371
372 #[test]
373 fn parse_args_reverse() {
374 assert!(parse_args(args(&["--reverse", "Ada"])).unwrap().reverse);
375 assert!(!parse_args(args(&["Ada"])).unwrap().reverse);
376 }
377
378 #[test]
379 fn reverse_spells_backwards() {
380 assert_eq!(hail(&reverse("Ada")), "Hi, adA!");
381 }
382
383 #[test]
384 fn greets_by_name() {
385 assert_eq!(hail("Ada"), "Hi, Ada!");
386 }
387
388 #[test]
389 fn greets_world_with_empty_string() {
390 assert_eq!(hail(""), "Hi, world!");
391 }
392
393 #[test]
394 fn greets_world_with_whitespace_only() {
395 assert_eq!(hail(" "), "Hi, world!");
396 }
397
398 #[test]
399 fn trims_whitespace_from_name() {
400 assert_eq!(hail(" Ada "), "Hi, Ada!");
401 }
402
403 #[test]
404 fn says_goodbye_by_name() {
405 assert_eq!(part("Ada"), "Goodbye, Ada!");
406 }
407
408 #[test]
409 fn says_goodbye_to_world_when_blank() {
410 assert_eq!(part(" "), "Goodbye, world!");
411 }
412
413 #[test]
414 fn farewell_trims_whitespace() {
415 assert_eq!(part(" Ada "), "Goodbye, Ada!");
416 }
417}