flagon-io/hello

public

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

hello/src/lib.rs

286 lines8,074 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}
38
39impl Default for Options {
40 fn default() -> Self {
41 Options {
42 name: None,
43 shout: false,
44 version: false,
45 times: 1,
46 lang: Lang::En,
47 }
48 }
49}
50
51/// Parses command-line arguments (excluding the program name).
52///
53/// `--shout` sets [`Options::shout`], `--version` sets
54/// [`Options::version`], and `--times N` sets [`Options::times`] (the
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.
59///
60/// Returns an error message if `--times` has a missing or non-numeric
61/// value, or if `--lang` has a missing or unsupported value.
62pub fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Options, String> {
63 let mut options = Options::default();
64 let mut args = args.into_iter();
65 while let Some(arg) = args.next() {
66 if arg == "--shout" {
67 options.shout = true;
68 } else if arg == "--version" {
69 options.version = true;
70 } else if arg == "--times" {
71 let value = args
72 .next()
73 .ok_or_else(|| "--times requires a value".to_owned())?;
74 options.times = value
75 .parse()
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 })?;
84 } else if !arg.starts_with("--") && options.name.is_none() {
85 options.name = Some(arg);
86 }
87 }
88 Ok(options)
89}
90
91/// `text` upper-cased.
92pub fn shout(text: &str) -> String {
93 text.to_uppercase()
94}
95
96/// The greeting for `name`.
97pub fn greeting(name: &str) -> String {
98 greeting_in(name, Lang::En)
99}
100
101/// The greeting for `name` in `lang`.
102pub fn greeting_in(name: &str, lang: Lang) -> String {
103 let trimmed = name.trim();
104 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
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 }
110}
111
112/// The farewell for `name`.
113pub fn farewell(name: &str) -> String {
114 let trimmed = name.trim();
115 let display_name = if trimmed.is_empty() { "world" } else { trimmed };
116 format!("Goodbye, {display_name}!")
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn args(list: &[&str]) -> Vec<String> {
124 list.iter().map(|s| s.to_string()).collect()
125 }
126
127 #[test]
128 fn parse_args_no_args() {
129 assert_eq!(parse_args(args(&[])).unwrap().name, None);
130 }
131
132 #[test]
133 fn parse_args_name() {
134 assert_eq!(parse_args(args(&["Ada"])).unwrap().name.as_deref(), Some("Ada"));
135 }
136
137 #[test]
138 fn parse_args_skips_leading_flag() {
139 assert_eq!(
140 parse_args(args(&["--unknown", "Ada"])).unwrap().name.as_deref(),
141 Some("Ada")
142 );
143 }
144
145 #[test]
146 fn parse_args_shout_defaults_off() {
147 assert!(!parse_args(args(&["Ada"])).unwrap().shout);
148 }
149
150 #[test]
151 fn parse_args_shout_before_name() {
152 let options = parse_args(args(&["--shout", "Ada"])).unwrap();
153 assert!(options.shout);
154 assert_eq!(options.name.as_deref(), Some("Ada"));
155 }
156
157 #[test]
158 fn parse_args_shout_after_name() {
159 let options = parse_args(args(&["Ada", "--shout"])).unwrap();
160 assert!(options.shout);
161 assert_eq!(options.name.as_deref(), Some("Ada"));
162 }
163
164 #[test]
165 fn parse_args_shout_alone() {
166 let options = parse_args(args(&["--shout"])).unwrap();
167 assert!(options.shout);
168 assert_eq!(options.name, None);
169 }
170
171 #[test]
172 fn parse_args_version_defaults_off() {
173 assert!(!parse_args(args(&["Ada"])).unwrap().version);
174 }
175
176 #[test]
177 fn parse_args_version() {
178 let options = parse_args(args(&["--version"])).unwrap();
179 assert!(options.version);
180 assert_eq!(options.name, None);
181 }
182
183 #[test]
184 fn parse_args_times_defaults_to_one() {
185 assert_eq!(parse_args(args(&["Ada"])).unwrap().times, 1);
186 }
187
188 #[test]
189 fn parse_args_times_valid() {
190 let options = parse_args(args(&["--times", "3", "Ada"])).unwrap();
191 assert_eq!(options.times, 3);
192 assert_eq!(options.name.as_deref(), Some("Ada"));
193 }
194
195 #[test]
196 fn parse_args_times_missing_value() {
197 assert!(parse_args(args(&["--times"])).is_err());
198 }
199
200 #[test]
201 fn parse_args_times_non_number() {
202 assert!(parse_args(args(&["--times", "abc", "Ada"])).is_err());
203 }
204
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]
248 fn shout_upper_cases_greeting() {
249 assert_eq!(shout(&greeting("Ada")), "HI, ADA!");
250 }
251
252 #[test]
253 fn greets_by_name() {
254 assert_eq!(greeting("Ada"), "Hi, Ada!");
255 }
256
257 #[test]
258 fn greets_world_with_empty_string() {
259 assert_eq!(greeting(""), "Hi, world!");
260 }
261
262 #[test]
263 fn greets_world_with_whitespace_only() {
264 assert_eq!(greeting(" "), "Hi, world!");
265 }
266
267 #[test]
268 fn trims_whitespace_from_name() {
269 assert_eq!(greeting(" Ada "), "Hi, Ada!");
270 }
271
272 #[test]
273 fn says_goodbye_by_name() {
274 assert_eq!(farewell("Ada"), "Goodbye, Ada!");
275 }
276
277 #[test]
278 fn says_goodbye_to_world_when_blank() {
279 assert_eq!(farewell(" "), "Goodbye, world!");
280 }
281
282 #[test]
283 fn farewell_trims_whitespace() {
284 assert_eq!(farewell(" Ada "), "Goodbye, Ada!");
285 }
286}