Skip to main content

soma_cli_core/
confirmation.rs

1//! Confirmation I/O primitives.
2//!
3//! This module owns the mechanics of asking a human to confirm an action by
4//! typing a specific token back — it does not decide *which* operations
5//! require confirmation. That is business/product confirmation policy and
6//! belongs in the consuming application layer, never here.
7
8use std::io::{self, BufRead, Write};
9
10/// Write `prompt` to `writer`, flush it, read one line from `reader`, and
11/// report whether the trimmed input equals `expected`.
12///
13/// Returns `Ok(false)` (not an error) when the input does not match —
14/// callers decide how to react to a declined confirmation (abort, retry,
15/// exit code) since that is product policy.
16pub fn confirm_typed<R, W>(
17    writer: &mut W,
18    reader: &mut R,
19    prompt: &str,
20    expected: &str,
21) -> io::Result<bool>
22where
23    R: BufRead,
24    W: Write,
25{
26    write!(writer, "{prompt}")?;
27    writer.flush()?;
28
29    let mut input = String::new();
30    reader.read_line(&mut input)?;
31    Ok(input.trim() == expected)
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn confirm_typed_writes_prompt_and_matches_input() {
40        let mut input = io::Cursor::new(b"yes\n".to_vec());
41        let mut output = Vec::new();
42        let confirmed = confirm_typed(&mut output, &mut input, "Type yes: ", "yes").unwrap();
43        assert!(confirmed);
44        assert_eq!(String::from_utf8(output).unwrap(), "Type yes: ");
45    }
46
47    #[test]
48    fn confirm_typed_reports_mismatch_without_erroring() {
49        let mut input = io::Cursor::new(b"nope\n".to_vec());
50        let mut output = Vec::new();
51        let confirmed = confirm_typed(&mut output, &mut input, "Type yes: ", "yes").unwrap();
52        assert!(!confirmed);
53    }
54
55    #[test]
56    fn confirm_typed_trims_trailing_whitespace() {
57        let mut input = io::Cursor::new(b"yes  \r\n".to_vec());
58        let mut output = Vec::new();
59        let confirmed = confirm_typed(&mut output, &mut input, "Type yes: ", "yes").unwrap();
60        assert!(confirmed);
61    }
62
63    #[test]
64    fn confirm_typed_treats_closed_stdin_as_a_non_match() {
65        // An empty reader mimics closed/EOF stdin: `read_line` returns
66        // `Ok(0)` with `input` left empty, which trims to `""` — never
67        // equal to a non-empty `expected` token, so this fails closed
68        // (`Ok(false)`) rather than erroring or matching.
69        let mut input = io::Cursor::new(Vec::new());
70        let mut output = Vec::new();
71        let confirmed = confirm_typed(&mut output, &mut input, "Type yes: ", "yes").unwrap();
72        assert!(!confirmed);
73    }
74}