1use std::io::{self, BufRead, Read};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum InputMode {
6 Json(String),
8 Pipe(Vec<u8>),
10}
11
12pub fn detect_stdin(stdin: &mut impl Read) -> io::Result<InputMode> {
17 let mut buffer = Vec::new();
18 stdin.read_to_end(&mut buffer)?;
19
20 if buffer.is_empty() {
21 return Ok(InputMode::Pipe(buffer));
22 }
23
24 let first_nonws = buffer.iter().position(|&b| !b.is_ascii_whitespace());
26
27 match first_nonws {
28 Some(pos) if buffer[pos] == b'{' => {
29 if let Ok(text) = std::str::from_utf8(&buffer)
31 && is_ripsed_json(text)
32 {
33 return Ok(InputMode::Json(text.to_string()));
34 }
35 Ok(InputMode::Pipe(buffer))
36 }
37 _ => Ok(InputMode::Pipe(buffer)),
38 }
39}
40
41fn is_ripsed_json(text: &str) -> bool {
48 let trimmed = text.trim_start();
49 trimmed.starts_with('{') && trimmed.contains("\"operations\"")
50}
51
52pub fn detect_buffered(reader: &mut impl BufRead) -> io::Result<InputMode> {
54 let buf = reader.fill_buf()?;
55 if buf.is_empty() {
56 return Ok(InputMode::Pipe(vec![]));
57 }
58
59 let first_nonws = buf.iter().position(|&b| !b.is_ascii_whitespace());
61
62 if first_nonws.is_some_and(|pos| buf[pos] == b'{') {
63 let mut full = Vec::new();
65 reader.read_to_end(&mut full)?;
66 if let Ok(text) = std::str::from_utf8(&full)
67 && is_ripsed_json(text)
68 {
69 return Ok(InputMode::Json(text.to_string()));
70 }
71 Ok(InputMode::Pipe(full))
72 } else {
73 let mut full = Vec::new();
74 reader.read_to_end(&mut full)?;
75 Ok(InputMode::Pipe(full))
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn test_detect_json() {
85 let input = r#"{"operations": [{"op": "replace", "find": "a", "replace": "b"}]}"#;
86 let mut cursor = io::Cursor::new(input.as_bytes());
87 let mode = detect_stdin(&mut cursor).unwrap();
88 assert!(matches!(mode, InputMode::Json(_)));
89 }
90
91 #[test]
92 fn test_detect_plain_text() {
93 let input = "just some plain text\n";
94 let mut cursor = io::Cursor::new(input.as_bytes());
95 let mode = detect_stdin(&mut cursor).unwrap();
96 assert!(matches!(mode, InputMode::Pipe(_)));
97 }
98
99 #[test]
100 fn test_detect_json_without_operations() {
101 let input = r#"{"key": "value"}"#;
102 let mut cursor = io::Cursor::new(input.as_bytes());
103 let mode = detect_stdin(&mut cursor).unwrap();
104 assert!(matches!(mode, InputMode::Pipe(_)));
105 }
106
107 #[test]
108 fn test_detect_empty() {
109 let input = "";
110 let mut cursor = io::Cursor::new(input.as_bytes());
111 let mode = detect_stdin(&mut cursor).unwrap();
112 assert!(matches!(mode, InputMode::Pipe(_)));
113 }
114}