| 1 | 1 | | //! Greetings. |
| 2 | 2 | | |
| 3 | + | /// A supported greeting language. |
| 4 | + | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 5 | + | pub enum Lang { |
| 6 | + | #[default] |
| 7 | + | En, |
| 8 | + | Es, |
| 9 | + | Fr, |
| 10 | + | } |
| 11 | + | |
| 12 | + | impl 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 | + | |
| 3 | 24 | | /// Parsed command-line options. |
| 4 | 25 | | #[derive(Debug, PartialEq, Eq)] |
| 5 | 26 | | pub struct Options { |
| ⋯ |
| 11 | 32 | | pub version: bool, |
| 12 | 33 | | /// How many times to print the greeting (`--times N`). |
| 13 | 34 | | pub times: usize, |
| 35 | + | /// The greeting language (`--lang CODE`). |
| 36 | + | pub lang: Lang, |
| 14 | 37 | | } |
| 15 | 38 | | |
| 16 | 39 | | impl Default for Options { |
| ⋯ |
| 20 | 43 | | shout: false, |
| 21 | 44 | | version: false, |
| 22 | 45 | | times: 1, |
| 46 | + | lang: Lang::En, |
| 23 | 47 | | } |
| 24 | 48 | | } |
| 25 | 49 | | } |
| ⋯ |
| 28 | 52 | | /// |
| 29 | 53 | | /// `--shout` sets [`Options::shout`], `--version` sets |
| 30 | 54 | | /// [`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. |
| 55 | + | /// next argument is consumed as the value), and `--lang CODE` sets |
| 56 | + | /// [`Options::lang`] (likewise consuming the next argument); other |
| 57 | + | /// arguments starting with `--` are treated as flags and skipped. The |
| 58 | + | /// name is the first remaining argument. |
| 34 | 59 | | /// |
| 35 | | − | /// Returns an error message if `--times` has a missing or non-numeric value. |
| 60 | + | /// Returns an error message if `--times` has a missing or non-numeric |
| 61 | + | /// value, or if `--lang` has a missing or unsupported value. |
| 36 | 62 | | pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> { |
| 37 | 63 | | let mut options = Options::default(); |
| 38 | 64 | | let mut args = args.into_iter(); |
| ⋯ |
| 48 | 74 | | options.times = value |
| 49 | 75 | | .parse() |
| 50 | 76 | | .map_err(|_| format!("--times value must be a number, got {value:?}"))?; |
| 77 | + | } else if arg == "--lang" { |
| 78 | + | let value = args |
| 79 | + | .next() |
| 80 | + | .ok_or_else(|| "--lang requires a value".to_owned())?; |
| 81 | + | options.lang = Lang::parse(&value).ok_or_else(|| { |
| 82 | + | format!("--lang value must be one of en, es, fr, got {value:?}") |
| 83 | + | })?; |
| 51 | 84 | | } else if !arg.starts_with("--") && options.name.is_none() { |
| 52 | 85 | | options.name = Some(arg); |
| 53 | 86 | | } |
| ⋯ |
| 62 | 95 | | |
| 63 | 96 | | /// The greeting for `name`. |
| 64 | 97 | | pub fn greeting(name: &str) -> String { |
| 98 | + | greeting_in(name, Lang::En) |
| 99 | + | } |
| 100 | + | |
| 101 | + | /// The greeting for `name` in `lang`. |
| 102 | + | pub fn greeting_in(name: &str, lang: Lang) -> String { |
| 65 | 103 | | let trimmed = name.trim(); |
| 66 | 104 | | let display_name = if trimmed.is_empty() { "world" } else { trimmed }; |
| 67 | | − | format!("Hi, {display_name}!") |
| 105 | + | match lang { |
| 106 | + | Lang::En => format!("Hi, {display_name}!"), |
| 107 | + | Lang::Es => format!("¡Hola, {display_name}!"), |
| 108 | + | Lang::Fr => format!("Bonjour, {display_name} !"), |
| 109 | + | } |
| 68 | 110 | | } |
| 69 | 111 | | |
| 70 | 112 | | /// The farewell for `name`. |
| ⋯ |
| 161 | 203 | | } |
| 162 | 204 | | |
| 163 | 205 | | #[test] |
| 206 | + | fn parse_args_lang_defaults_to_en() { |
| 207 | + | assert_eq!(parse_args(args(&["Ada"])).unwrap().lang, Lang::En); |
| 208 | + | } |
| 209 | + | |
| 210 | + | #[test] |
| 211 | + | fn parse_args_lang_valid() { |
| 212 | + | let options = parse_args(args(&["--lang", "es", "Ana"])).unwrap(); |
| 213 | + | assert_eq!(options.lang, Lang::Es); |
| 214 | + | assert_eq!(options.name.as_deref(), Some("Ana")); |
| 215 | + | assert_eq!(parse_args(args(&["--lang", "fr"])).unwrap().lang, Lang::Fr); |
| 216 | + | } |
| 217 | + | |
| 218 | + | #[test] |
| 219 | + | fn parse_args_lang_missing_value() { |
| 220 | + | assert_eq!( |
| 221 | + | parse_args(args(&["--lang"])).unwrap_err(), |
| 222 | + | "--lang requires a value" |
| 223 | + | ); |
| 224 | + | } |
| 225 | + | |
| 226 | + | #[test] |
| 227 | + | fn parse_args_lang_unknown() { |
| 228 | + | assert_eq!( |
| 229 | + | parse_args(args(&["--lang", "de"])).unwrap_err(), |
| 230 | + | "--lang value must be one of en, es, fr, got \"de\"" |
| 231 | + | ); |
| 232 | + | } |
| 233 | + | |
| 234 | + | #[test] |
| 235 | + | fn greeting_in_each_language() { |
| 236 | + | assert_eq!(greeting_in("Ada", Lang::En), "Hi, Ada!"); |
| 237 | + | assert_eq!(greeting_in("Ana", Lang::Es), "¡Hola, Ana!"); |
| 238 | + | assert_eq!(greeting_in("Luc", Lang::Fr), "Bonjour, Luc !"); |
| 239 | + | } |
| 240 | + | |
| 241 | + | #[test] |
| 242 | + | fn greeting_in_trims_and_falls_back_to_world() { |
| 243 | + | assert_eq!(greeting_in(" Ana ", Lang::Es), "¡Hola, Ana!"); |
| 244 | + | assert_eq!(greeting_in(" ", Lang::Fr), "Bonjour, world !"); |
| 245 | + | } |
| 246 | + | |
| 247 | + | #[test] |
| 164 | 248 | | fn shout_upper_cases_greeting() { |
| 165 | 249 | | assert_eq!(shout(&greeting("Ada")), "HI, ADA!"); |
| 166 | 250 | | } |