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}