Skip to main content

ripsed_json/
detect.rs

1use std::io::{self, BufRead, Read};
2
3/// The detected input mode based on stdin content.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum InputMode {
6    /// Valid ripsed JSON request detected.
7    Json(String),
8    /// Plain text (pipe mode).
9    Pipe(Vec<u8>),
10}
11
12/// Peek at stdin to determine whether the input is a JSON request or plain text.
13///
14/// Reads the first chunk of stdin, checks if it starts with `{` and contains
15/// an `"operations"` key, and returns the appropriate mode.
16pub 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    // Find first non-whitespace byte
25    let first_nonws = buffer.iter().position(|&b| !b.is_ascii_whitespace());
26
27    match first_nonws {
28        Some(pos) if buffer[pos] == b'{' => {
29            // Try to parse as JSON
30            if let Ok(text) = std::str::from_utf8(&buffer) {
31                if is_ripsed_json(text) {
32                    return Ok(InputMode::Json(text.to_string()));
33                }
34            }
35            Ok(InputMode::Pipe(buffer))
36        }
37        _ => Ok(InputMode::Pipe(buffer)),
38    }
39}
40
41/// Check if a JSON string looks like a ripsed request (has "operations" key).
42fn is_ripsed_json(text: &str) -> bool {
43    // Quick check: does it contain the "operations" key?
44    // We parse it to validate it's actually valid JSON with that key.
45    if let Ok(value) = serde_json::from_str::<serde_json::Value>(text) {
46        value.get("operations").is_some()
47    } else {
48        false
49    }
50}
51
52/// Detect input mode from a buffered reader (for streaming stdin).
53pub 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    // Peek at first non-whitespace
60    let first_nonws = buf.iter().position(|&b| !b.is_ascii_whitespace());
61
62    if first_nonws.is_some_and(|pos| buf[pos] == b'{') {
63        // Read everything and try to parse
64        let mut full = Vec::new();
65        reader.read_to_end(&mut full)?;
66        if let Ok(text) = std::str::from_utf8(&full) {
67            if is_ripsed_json(text) {
68                return Ok(InputMode::Json(text.to_string()));
69            }
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}