input_validation/
lib.rs

1//! Prompts the user for input and parses the input as a value of type 'T'.
2//!
3//! If the input cannot be parsed as the request 'T', the function will prompt the user again
4//! until a valid input is entered.
5//! 
6//! # Arguments
7//! 
8//! * 'prompt' - A string that will be printed to the console to prompt the user for input.
9//! 
10//! # Returns
11//! 
12//! A value of type 'T' that was parsed from the user's input.
13//! 
14//! # Panics
15//! This function will panic if 'T' does not implement the 'FromStr' trait.
16use std::io::{self, Write};
17use regex::Regex;
18
19/// Returns the decimal separator
20fn get_separator() -> char {
21    let integer = 1234;
22    let float = integer as f32 / 100 as f32;
23    // Parse the decimal separator from the float
24    let separator = float.to_string().chars().nth(2).unwrap();
25    separator
26}
27
28/// Prompts the user for input and parses the user's response as a specified type.
29///
30/// This function repeatedly prompts the user for input until the user provides input that can be
31/// successfully parsed as the specified type `T`. If the user's input cannot be parsed as `T`,
32/// the function will continue to prompt the user for input.
33///
34/// # Examples
35///
36/// ```
37/// use input_validation::get_input;
38///
39/// let name: String = get_input("What is your name? ");
40/// println!("Hello, {}!", name);
41/// ```
42///
43/// ```
44/// use input_validation::get_input;
45///
46/// let age: u32 = get_input("How old are you? ");
47/// println!("You are {} years old.", age);
48/// ```
49///
50/// # Panics
51///
52/// This function will panic if it is unable to write to the standard output stream.
53///
54/// # Errors
55///
56/// This function will return an error if it is unable to read from the standard input stream or
57/// if the user's input cannot be parsed as the specified type `T`.
58pub fn get_input<T: std::str::FromStr>(prompt: &str) -> T
59where
60    <T as std::str::FromStr>::Err: std::fmt::Debug,
61{
62    // First letter of T type
63    let first_letter = std::any::type_name::<T>().chars().next().unwrap();
64
65    loop {
66        print!("{}", prompt);
67        io::stdout().flush().unwrap();
68
69        let mut input = String::new();
70
71        // If first_letter is i, u or f, remove any commas from input
72        if first_letter == 'i' || first_letter == 'u' || first_letter == 'f' {
73            // If the decimal separator is '.' set thousands separator to ',', else set it to '.'
74            let locale = get_separator();
75            let thousands_separator = if locale == '.' { ',' } else { '.' };
76            
77            match io::stdin().read_line(&mut input) {
78                Ok(_) => match input.trim().replace(thousands_separator, "").parse::<T>() {
79                    Ok(val) => return val,
80                    Err(_) => continue,
81                },
82                Err(_) => continue,
83            }
84        }
85
86        match io::stdin().read_line(&mut input) {
87            Ok(_) => match input.trim().parse::<T>() {
88                Ok(val) => return val,
89                Err(_) => continue,
90            },
91            Err(_) => continue,
92        }
93    }
94}
95
96/// Reads input from the user and returns a vector of elements of type T.
97///
98/// This function prompts the user for input using the specified `prompt` and then splits
99/// the input string into individual elements using the specified `separator`. If all of the
100/// elements can be parsed into type `T`, then they are returned as a `Vec<T>`. If any of the
101/// elements fail to parse, the function will loop and prompt the user for input again.
102///
103/// # Examples
104///
105/// ```rust
106/// let numbers: Vec<i32> = get_list("Enter some numbers, separated by commas: ", ",");
107/// ```
108///
109/// # Panics
110///
111/// This function will panic if `separator` is an empty string.
112///
113/// # Errors
114///
115/// This function will keep looping and prompting for input if any of the elements fail to parse
116/// into type `T`. The error message from the parsing failure is printed to stderr.
117///
118pub fn get_list<T: std::str::FromStr>(prompt: &str, separator: &str) -> Vec<T>
119where
120    <T as std::str::FromStr>::Err: std::fmt::Debug,
121{
122    loop {
123        let input = get_input::<String>(prompt);
124        let values: Vec<_> = input.split(separator).map(str::trim).collect();
125        if values.iter().all(|s| s.parse::<T>().is_ok()) {
126            return values.iter().map(|s| s.parse::<T>().unwrap()).collect();
127        }
128    }
129}
130
131/// Prompts the user for a boolean input, returning `true` if the input is
132/// "y" or "yes" (case insensitive), and `false` if the input is "n" or "no"
133/// (case insensitive). If the input does not match either "y", "yes", "n", or
134/// "no", the function will loop and prompt the user again.
135///
136/// # Arguments
137///
138/// * `prompt` - A string slice that will be displayed to the user as the prompt
139/// for their input.
140///
141/// # Example
142///
143/// ```
144/// use user_input::{get_bool};
145///
146/// let confirm = get_bool("Are you sure you want to proceed? (y/n) ");
147/// if confirm {
148///     println!("User confirmed.");
149/// } else {
150///     println!("User declined.");
151/// }
152/// ```
153pub fn get_bool(prompt: &str) -> bool {
154    loop {
155        let input = get_input::<String>(prompt).to_lowercase();
156        match input.as_str() {
157            "y" | "yes" => return true,
158            "n" | "no" => return false,
159            _ => continue,
160        }
161    }
162}
163
164/// Prompts the user to select a choice from a list of options and returns the index of the selected choice.
165///
166/// # Arguments
167///
168/// * `prompt` - A string slice containing the prompt to display to the user.
169/// * `choices` - A slice containing the available choices.
170///
171/// # Examples
172///
173/// ```
174/// let choices = vec!["Option 1", "Option 2", "Option 3"];
175/// let index = get_choice("Please select an option", &choices);
176/// println!("You selected: {}", choices[index]);
177/// ```
178    pub fn get_choice(prompt: &str, choices: &[&str]) -> usize {
179    loop {
180        let choice = get_input::<String>(&format!("{} ({}) ", prompt, choices.join("/")));
181        match choices
182            .iter()
183            .position(|&c| c.to_lowercase() == choice.to_lowercase().trim())
184        {
185            Some(index) => return index,
186            None => continue,
187        }
188    }
189}
190
191
192/// Prompts the user to enter an email address and returns it as a string.
193///
194/// The function ensures that the email address is valid and has the correct format.
195///
196/// # Examples
197///
198/// ```
199/// use rust_input_lib::get_email;
200///
201/// let email = get_email("Enter your email address: ");
202/// println!("Your email address is: {}", email);
203/// ```
204pub fn get_email(prompt: &str) -> String {
205    let re = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
206
207    loop {
208        let email = get_input::<String>(prompt);
209        if re.is_match(&email) {
210            return email;
211        } else {
212            println!("Invalid email address. Please enter a valid email address.");
213            continue;
214        }
215    }
216}
217
218#[cfg(test)]
219mod integration_tests;