input_macro/
lib.rs

1use std::fmt::Arguments;
2use std::io::{self, BufRead, Write};
3use std::str::FromStr;
4
5/// A unified error type indicating either an I/O error, a parse error, or EOF.
6#[derive(Debug)]
7pub enum InputError<E> {
8    /// An I/O error occurred (e.g., closed stdin).
9    Io(io::Error),
10    /// Failed to parse the input into the desired type.
11    Parse(E),
12    /// EOF encountered (read_line returned 0).
13    Eof,
14}
15
16impl<E: std::fmt::Display + std::fmt::Debug> std::fmt::Display for InputError<E> {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            InputError::Io(e) => write!(f, "I/O error: {}", e),
20            InputError::Parse(e) => write!(f, "Parse error: {}", e),
21            InputError::Eof => write!(f, "EOF encountered"),
22        }
23    }
24}
25
26impl<E: std::fmt::Display + std::fmt::Debug> std::error::Error for InputError<E> {}
27
28/// A single function that:
29/// 1. Optionally prints a prompt (and flushes).
30/// 2. Reads one line from the provided `BufRead`.
31/// 3. Returns `Err(InputError::Eof)` if EOF is reached.
32/// 4. Parses into type `T`, returning `Err(InputError::Parse)` on failure.
33/// 5. Returns `Err(InputError::Io)` on I/O failure.
34pub fn read_input_from<R, T>(
35    reader: &mut R,
36    prompt: Option<Arguments<'_>>,
37) -> Result<T, InputError<T::Err>>
38where
39    R: BufRead,
40    T: FromStr,
41    T::Err: std::fmt::Display + std::fmt::Debug,
42{
43    if let Some(prompt_args) = prompt {
44        print!("{}", prompt_args);
45        // Always flush so the user sees the prompt immediately
46        io::stdout().flush().map_err(InputError::Io)?;
47    }
48
49    let mut input = String::new();
50    let bytes_read = reader.read_line(&mut input).map_err(InputError::Io)?;
51
52    // If 0, that's EOF — return Eof error
53    if bytes_read == 0 {
54        return Err(InputError::Eof);
55    }
56
57    let trimmed = input.trim_end_matches(['\r', '\n'].as_ref());
58    trimmed.parse::<T>().map_err(InputError::Parse)
59}
60
61/// A convenience wrapper that reads from stdin (locking it), without printing a prompt.
62pub fn read_input<T>() -> Result<T, InputError<T::Err>>
63where
64    T: FromStr,
65    T::Err: std::fmt::Display + std::fmt::Debug,
66{
67    let stdin = io::stdin();
68    let mut locked = stdin.lock();
69    read_input_from(&mut locked, None)
70}
71
72/// A convenience wrapper that reads from stdin, printing the given prompt first.
73pub fn read_input_with_prompt<T>(prompt: Arguments<'_>) -> Result<T, InputError<T::Err>>
74where
75    T: FromStr,
76    T::Err: std::fmt::Display + std::fmt::Debug,
77{
78    let stdin = io::stdin();
79    let mut locked = stdin.lock();
80    read_input_from(&mut locked, Some(prompt))
81}
82
83/// A macro that:
84/// - reads **one line** from stdin (as `String` by default),
85/// - returns `Ok(None)` if EOF is encountered (`InputError::Eof`).
86///
87/// # Usage:
88/// ```no_run
89/// // No prompt
90/// let text: Option<String> = input!().unwrap();
91///
92/// // With prompt
93/// let name: Option<String> = input!("Enter your name: ").unwrap();
94///
95/// // Formatted prompt
96/// let user = "Alice";
97/// let age: Option<String> = input!("Enter {}'s age: ", user).unwrap();
98/// ```
99#[macro_export]
100macro_rules! input {
101    () => {{
102        // If you'd like a different type, just replace <String> below:
103        match $crate::read_input_from(&mut ::std::io::stdin().lock(), None) {
104            Ok(val) => Ok(Some(val)),
105            Err($crate::InputError::Eof) => Ok(None),
106            Err(err) => Err(err),
107        }
108    }};
109    ($($arg:tt)*) => {{
110        match $crate::read_input_from(
111            &mut ::std::io::stdin().lock(),
112            Some(format_args!($($arg)*))
113        ) {
114            Ok(val) => Ok(Some(val)),
115            Err($crate::InputError::Eof) => Ok(None),
116            Err(err) => Err(err),
117        }
118    }};
119}
120
121/// A macro that:
122/// - prints the prompt on its own line (with `println!`),
123/// - then reads one line,
124/// - returns `Ok(None)` on EOF,
125/// - otherwise parses into `String`.
126///
127/// # Usage:
128/// ```no_run
129/// let line: Option<String> = inputln!("What's your favorite color?").unwrap();
130/// ```
131#[macro_export]
132macro_rules! inputln {
133    () => {{
134        match $crate::read_input_from(&mut ::std::io::stdin().lock(), None) {
135            Ok(val) => Ok(Some(val)),
136            Err($crate::InputError::Eof) => Ok(None),
137            Err(err) => Err(err),
138        }
139    }};
140    ($($arg:tt)*) => {{
141        println!("{}", format_args!($($arg)*));
142        ::std::io::Write::flush(&mut ::std::io::stdout()).unwrap();
143        match $crate::read_input_from(&mut ::std::io::stdin().lock(), None) {
144            Ok(val) => Ok(Some(val)),
145            Err($crate::InputError::Eof) => Ok(None),
146            Err(err) => Err(err),
147        }
148    }};
149}
150
151/// A macro that:
152/// - reads one line from stdin,
153/// - tries to parse into `String`,
154/// - **treats EOF as an error** (no `Ok(None)`).
155///
156/// # Usage:
157/// ```no_run
158/// // No prompt
159/// let line: String = input_no_eof!().unwrap();
160///
161/// // With prompt
162/// let age: i32 = input_no_eof!("Enter your age: ").unwrap();
163/// ```
164#[macro_export]
165macro_rules! input_no_eof {
166    () => {{
167        // If you want a different type, change <String> here:
168        $crate::read_input_from(&mut ::std::io::stdin().lock(), None)
169    }};
170    ($($arg:tt)*) => {{
171        $crate::read_input_from(
172            &mut ::std::io::stdin().lock(),
173            Some(format_args!($($arg)*))
174        )
175    }};
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::io::{Cursor, Error, ErrorKind};
182
183    /// Basic test reading an integer
184    #[test]
185    fn test_read_input_integer() {
186        let mut reader = Cursor::new("42\n");
187        let res: Result<i32, _> = read_input_from(&mut reader, None);
188        assert_eq!(res.unwrap(), 42);
189    }
190
191    /// Test reading a floating-point number
192    #[test]
193    fn test_read_input_float() {
194        let mut reader = Cursor::new("3.14159\n");
195        let res: Result<f64, _> = read_input_from(&mut reader, None);
196        assert!((res.unwrap() - 3.14159).abs() < f64::EPSILON);
197    }
198
199    /// Test reading an unsigned integer
200    #[test]
201    fn test_read_input_unsigned() {
202        let mut reader = Cursor::new("255\n");
203        let res: Result<u32, _> = read_input_from(&mut reader, None);
204        assert_eq!(res.unwrap(), 255);
205    }
206
207    /// EOF immediately (0 bytes read)
208    #[test]
209    fn test_read_input_eof() {
210        let mut reader = Cursor::new("");
211        let res: Result<i32, _> = read_input_from(&mut reader, None);
212        assert!(matches!(res, Err(InputError::Eof)));
213    }
214
215    /// Parse error: not an integer
216    #[test]
217    fn test_read_input_parse_error() {
218        let mut reader = Cursor::new("not an int\n");
219        let res: Result<i32, _> = read_input_from(&mut reader, None);
220        assert!(matches!(res, Err(InputError::Parse(_))));
221    }
222
223    /// Reading a standard string
224    #[test]
225    fn test_read_input_string() {
226        let mut reader = Cursor::new("hello world\r\n");
227        let res: Result<String, _> = read_input_from(&mut reader, None);
228        assert_eq!(res.unwrap(), "hello world");
229    }
230
231    /// Test with an explicit prompt passed as `format_args!`
232    #[test]
233    fn test_with_prompt() {
234        let mut reader = Cursor::new("100\n");
235        // Demonstrate passing "Enter: " as a &str with format_args!
236        let prompt = format_args!("Enter: ");
237        let res: Result<i32, _> = read_input_from(&mut reader, Some(prompt));
238        assert_eq!(res.unwrap(), 100);
239    }
240
241    /// Multiple lines: read first line (valid), then second line (valid)
242    #[test]
243    fn test_multiple_lines_valid() {
244        let mut reader = Cursor::new("123\n456\n");
245        // Read first line
246        let first: i32 = read_input_from(&mut reader, None).unwrap();
247        assert_eq!(first, 123);
248
249        // Read second line
250        let second: i32 = read_input_from(&mut reader, None).unwrap();
251        assert_eq!(second, 456);
252    }
253
254    /// Multiple lines: read first line (valid), second line (invalid parse), third line (EOF)
255    #[test]
256    fn test_multiple_lines_parse_error_then_eof() {
257        let mut reader = Cursor::new("42\nnotanint\n");
258        // Read first line
259        let first: i32 = read_input_from(&mut reader, None).unwrap();
260        assert_eq!(first, 42);
261
262        // Read second line: parse error
263        let second = read_input_from::<_, i32>(&mut reader, None);
264        assert!(matches!(second, Err(InputError::Parse(_))));
265
266        // Next read is EOF (because we've consumed all input)
267        let third = read_input_from::<_, i32>(&mut reader, None);
268        assert!(matches!(third, Err(InputError::Eof)));
269    }
270
271    /// Test what happens with an empty line (just "\n")
272    /// - By default, we interpret empty line as an empty string -> parse error for integer
273    #[test]
274    fn test_empty_line_behavior() {
275        let mut reader = Cursor::new("\n");
276        let res: Result<i32, _> = read_input_from(&mut reader, None);
277        // Typically this is a parse error, because "" can't parse into i32
278        assert!(matches!(res, Err(InputError::Parse(_))));
279    }
280
281    /// Test that macros compile and work as expected (this is a basic usage check).
282    /// We simulate a single line of input, then confirm `input!()` returns `Ok(Some(...))`.
283    #[test]
284    fn test_input_macro() {
285        let mut reader = Cursor::new("HelloFromMacro\n");
286        // Because macros read from stdin, we can temporarily override stdin by locking,
287        // but to test here, we'll just manually call `read_input_from`.
288        // In a real scenario, you might do an integration test or skip macro tests in unit tests.
289
290        // Direct call for demonstration:
291        let result: Result<String, _> = read_input_from(&mut reader, None);
292        assert_eq!(result.unwrap(), "HelloFromMacro");
293    }
294
295    /// Check that a custom `Read` implementation that returns an error triggers `InputError::Io`.
296    #[test]
297    fn test_io_error() {
298        struct ErrorReader;
299
300        impl BufRead for ErrorReader {
301            fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
302                // Force an I/O error
303                Err(Error::new(ErrorKind::Other, "Simulated I/O failure"))
304            }
305            fn consume(&mut self, _amt: usize) {}
306        }
307
308        // We only need `read_line` to fail:
309        impl std::io::Read for ErrorReader {
310            fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
311                Err(Error::new(ErrorKind::Other, "Simulated I/O failure"))
312            }
313        }
314
315        let mut reader = ErrorReader;
316        let res: Result<String, _> = read_input_from(&mut reader, None);
317        assert!(matches!(res, Err(InputError::Io(_))));
318    }
319}