dia_args/ask.rs
1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Args
5
6Copyright (C) 2018-2019, 2021-2025 Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2018-2019".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program. If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Kit for working with user
28
29use {
30 core::{
31 fmt::{self, Display, Formatter},
32 hash::{Hash, Hasher},
33 str::FromStr,
34 },
35 std::{
36 collections::HashSet,
37 io::{Error, ErrorKind},
38 },
39 crate::Result,
40};
41
42/// # Answer
43///
44/// For variants which have an optional representation string:
45///
46/// - If you don't provide the representation strings, defaults will be used.
47/// - Implementations of [`Eq`][trait:core/cmp/Eq], [`PartialEq`][trait:core/cmp/PartialEq], [`Hash`][trait:core/hash/Hash] do _NOT_ work on the
48/// representation strings.
49///
50/// For [`UserDefined`](#variant.UserDefined):
51///
52/// - Implementations of [`Eq`][trait:core/cmp/Eq], [`PartialEq`][trait:core/cmp/PartialEq], [`Hash`][trait:core/hash/Hash] _work_ on data you
53/// provide.
54///
55/// For example, if you want 2 answers of `Resume` and `Ignore`, but there are no such variants, you can do this:
56///
57/// ```
58/// use core::fmt::{self, Display, Formatter};
59/// use dia_args::Answer;
60///
61/// #[derive(Debug, Eq, PartialEq, Hash)]
62/// enum CustomAnswer {
63/// Resume,
64/// Ignore,
65/// }
66///
67/// impl Display for CustomAnswer {
68///
69/// fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
70/// f.write_str(match self {
71/// CustomAnswer::Resume => "Resume",
72/// CustomAnswer::Ignore => "Ignore",
73/// })
74/// }
75///
76/// }
77///
78/// # // We can't test ask_user() because it will need to read data from stdin, which is impossible in current Rust tests.
79/// # // Do NOT use #[cfg(test)] -- it will ignore the whole block.
80/// # // Using the macro cfg!(test) will make the compiler verify what's following it.
81/// # if cfg!(test) {
82/// match dia_args::ask_user(
83/// "What's your desire?",
84/// &[
85/// Answer::UserDefined(CustomAnswer::Resume),
86/// Answer::UserDefined(CustomAnswer::Ignore),
87/// Answer::Cancel(None),
88/// ],
89/// )? {
90/// Answer::UserDefined(answer) => match answer {
91/// CustomAnswer::Resume => {},
92/// CustomAnswer::Ignore => {},
93/// },
94/// Answer::Cancel(_) => {},
95/// _ => {},
96/// };
97/// # }
98///
99/// # dia_args::Result::Ok(())
100/// ```
101///
102/// ## See also
103///
104/// [`ask_user()`][fn:ask_user].
105///
106/// [trait:core/cmp/Eq]: https://doc.rust-lang.org/core/cmp/trait.Eq.html
107/// [trait:core/cmp/PartialEq]: https://doc.rust-lang.org/core/cmp/trait.PartialEq.html
108/// [trait:core/hash/Hash]: https://doc.rust-lang.org/core/hash/trait.Hash.html
109///
110/// [fn:ask_user]: fn.ask_user.html
111#[derive(Debug, Eq)]
112pub enum Answer<'a, T> where T: Eq + PartialEq + Hash + Display {
113
114 /// # Yes
115 Yes(Option<&'a str>),
116
117 /// # No
118 No(Option<&'a str>),
119
120 /// # Retry
121 Retry(Option<&'a str>),
122
123 /// # Next
124 Next(Option<&'a str>),
125
126 /// # Cancel
127 Cancel(Option<&'a str>),
128
129 /// # User-defined
130 UserDefined(T),
131
132}
133
134impl<T> PartialEq for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {
135
136 fn eq(&self, other: &Self) -> bool {
137 match (self, other) {
138 (Answer::Yes(_), Answer::Yes(_)) => true,
139 (Answer::No(_), Answer::No(_)) => true,
140 (Answer::Retry(_), Answer::Retry(_)) => true,
141 (Answer::Next(_), Answer::Next(_)) => true,
142 (Answer::Cancel(_), Answer::Cancel(_)) => true,
143 (Answer::UserDefined(first), Answer::UserDefined(second)) => first == second,
144 _ => false,
145 }
146 }
147
148}
149
150impl<T> Hash for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {
151
152 fn hash<H>(&self, h: &mut H) where H: Hasher {
153 let id = match self {
154 Answer::Yes(_) => "yes",
155 Answer::No(_) => "no",
156 Answer::Retry(_) => "retry",
157 Answer::Next(_) => "next",
158 Answer::Cancel(_) => "cancel",
159 Answer::UserDefined(t) => {
160 t.hash(h);
161 return;
162 },
163 };
164
165 crate::ID.hash(h);
166 id.hash(h);
167 }
168
169}
170
171#[test]
172fn test_answers() {
173 let answers = &[
174 Answer::Yes(Some(concat!())), Answer::Yes(None),
175 Answer::No(Some(concat!())), Answer::No(None),
176 Answer::Retry(Some(concat!())), Answer::Retry(None),
177 Answer::Next(Some(concat!())), Answer::Next(None),
178 Answer::Cancel(Some(concat!())), Answer::Cancel(None),
179 Answer::UserDefined("first"), Answer::UserDefined("second"),
180 Answer::UserDefined("second"), Answer::UserDefined("first"),
181 ];
182 assert_eq!(answers.iter().collect::<HashSet<_>>().len(), 7);
183}
184
185impl<T> Display for Answer<'_, T> where T: Eq + PartialEq + Hash + Display {
186
187 fn fmt(&self, f: &mut Formatter) -> core::result::Result<(), fmt::Error> {
188 match self {
189 Answer::Yes(Some(s)) | Answer::No(Some(s)) | Answer::Retry(Some(s)) | Answer::Next(Some(s)) | Answer::Cancel(Some(s))
190 => f.write_str(s),
191 Answer::UserDefined(t) => f.write_str(&t.to_string()),
192 Answer::Yes(None) => f.write_str("Yes"),
193 Answer::No(None) => f.write_str("No"),
194 Answer::Retry(None) => f.write_str("Retry"),
195 Answer::Next(None) => f.write_str("Next"),
196 Answer::Cancel(None) => f.write_str("Cancel"),
197 }
198 }
199
200}
201
202/// # Asks user some question
203///
204/// Notes:
205///
206/// - The question will be printed first. Then one line break. Then each answer will be printed on each line.
207///
208/// - The user can enter either:
209///
210/// + An answer index.
211/// + Or some part of an answer.
212///
213/// - The function will start again if:
214///
215/// + The user enters a wrong index.
216/// + The phrase entered is presented in more than one answer.
217///
218/// - The function returns an error if you provide duplicate answers, or no answers at all.
219pub fn ask_user<'a, 'b, S, T>(question: S, answers: &'a[Answer<'b, T>]) -> Result<&'a Answer<'b, T>>
220where S: AsRef<str>, T: Eq + PartialEq + Hash + Display {
221 if answers.is_empty() {
222 return Err(Error::new(ErrorKind::InvalidData, "There are no answers"));
223 }
224 if answers.iter().collect::<HashSet<_>>().len() != answers.len() {
225 return Err(Error::new(ErrorKind::InvalidData, "There are duplicate answers"));
226 }
227
228 let question = question.as_ref().trim();
229
230 loop {
231 crate::lock_write_out(format!("{}\n\n", question));
232 for (idx, answer) in answers.iter().enumerate() {
233 crate::lock_write_out(format!("[{idx}] {answer}\n", idx=idx + 1, answer=answer));
234 }
235
236 crate::lock_write_out("\n-> ");
237
238 let user_choice = crate::read_line::<String>()?;
239 if user_choice.is_empty() {
240 continue;
241 }
242
243 // Try index first
244 match usize::from_str(&user_choice).map(|i| i.checked_sub(1)) {
245 Ok(Some(idx)) => if idx < answers.len() {
246 return Ok(&answers[idx]);
247 },
248 Ok(None) => continue,
249 // Now try phrase
250 Err(_) => {
251 let user_choice = user_choice.to_lowercase();
252 match answers.iter().try_fold(None, |found_answer, answer| if answer.to_string().to_lowercase().contains(&user_choice) {
253 match found_answer {
254 None => Ok(Some(answer)),
255 Some(_) => Err("User choice matches more than one answer"),
256 }
257 } else {
258 Ok(found_answer)
259 }) {
260 Ok(Some(answer)) => return Ok(answer),
261 _ => continue,
262 };
263 },
264 };
265 }
266}