Skip to main content

brokapi_sansio/
json_buffer.rs

1//! Incremental JSON frame reassembly for WebSocket text streams.
2
3/// Result of feeding a chunk into [`JsonFrameBuffer`].
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum BufferPush {
6    /// The chunk was stored. No complete JSON value is available yet. More bytes are needed.
7    Incomplete,
8    /// One or more complete JSON values were cut out, in arrival order. Each string is the text of
9    /// one object or array, without the whitespace around it.
10    Complete(Vec<String>),
11}
12
13/// Puts the pieces of a JSON stream back together and cuts it into whole top-level values.
14///
15/// Some WebSocket servers split one JSON message over several text frames, or glue several
16/// messages into one frame. Feed every received chunk to [`push`](Self::push). It keeps the
17/// unfinished tail and returns each complete top-level object or array.
18///
19/// What it does and does not do:
20///
21/// - It counts brackets and understands strings and escapes, so a brace inside `"..."` does not
22///   end a value.
23/// - It does **not** check that the text is valid JSON. `{not json}` is returned as a complete
24///   value. Parse each returned string yourself.
25/// - Only values that start with `{` or `[` are recognised. If the pending text starts with
26///   anything else (a bare number, a word, a stray `}`), nothing is ever returned and the text
27///   stays in the buffer, growing with every chunk. Call [`reset`](Self::reset) when the stream
28///   is reconnected or corrupt.
29///
30/// # Examples
31///
32/// ```
33/// use brokapi_sansio::{BufferPush, JsonFrameBuffer};
34///
35/// let mut buf = JsonFrameBuffer::new();
36///
37/// // One message arrives in two pieces.
38/// assert_eq!(buf.push(r#"{"op":"quote","bid":"#), BufferPush::Incomplete);
39/// assert_eq!(
40///     buf.push(r#"100}"#),
41///     BufferPush::Complete(vec![r#"{"op":"quote","bid":100}"#.to_string()]),
42/// );
43///
44/// // Two messages arrive glued together, and a third starts.
45/// let got = buf.push(r#"{"a":1} {"b":"}"} {"c""#);
46/// assert_eq!(
47///     got,
48///     BufferPush::Complete(vec![r#"{"a":1}"#.to_string(), r#"{"b":"}"}"#.to_string()]),
49/// );
50/// assert_eq!(buf.pending(), r#"{"c""#);
51///
52/// // The caller can drop a half-received message, for example after a reconnect.
53/// buf.reset();
54/// assert!(buf.is_empty());
55///
56/// // Text that cannot start a value blocks the buffer, even if a good value follows it.
57/// assert_eq!(buf.push(r#"oops {"a":1}"#), BufferPush::Incomplete);
58/// assert!(!buf.is_empty());
59/// buf.reset();
60/// ```
61#[derive(Debug, Clone, Default)]
62pub struct JsonFrameBuffer {
63    buffer: String,
64}
65
66impl JsonFrameBuffer {
67    /// Creates an empty buffer.
68    pub fn new() -> Self {
69        Self::default()
70    }
71
72    /// The text received but not yet returned: an unfinished value, or text that cannot start one.
73    pub fn pending(&self) -> &str {
74        &self.buffer
75    }
76
77    /// Whether nothing is pending. Whitespace-only input counts as nothing.
78    pub fn is_empty(&self) -> bool {
79        self.buffer.is_empty()
80    }
81
82    /// Appends a chunk and returns every complete top-level value that is now available.
83    ///
84    /// The returned strings are in arrival order. What follows them stays in the buffer. See the
85    /// type docs for what is not checked.
86    pub fn push(&mut self, chunk: &str) -> BufferPush {
87        self.buffer.push_str(chunk);
88        let mut frames = Vec::new();
89
90        loop {
91            let trimmed = self.buffer.trim_start();
92            if trimmed.is_empty() {
93                self.buffer.clear();
94                break;
95            }
96
97            match extract_one_json(trimmed) {
98                Some((frame, consumed)) => {
99                    frames.push(frame);
100                    self.buffer = trimmed[consumed..].to_string();
101                }
102                None => break,
103            }
104        }
105
106        if frames.is_empty() {
107            BufferPush::Incomplete
108        } else {
109            BufferPush::Complete(frames)
110        }
111    }
112
113    /// Throws away everything pending.
114    pub fn reset(&mut self) {
115        self.buffer.clear();
116    }
117}
118
119/// Returns `(json_text, byte_len)` when `input` starts with one complete JSON value.
120fn extract_one_json(input: &str) -> Option<(String, usize)> {
121    let bytes = input.as_bytes();
122    let mut i = 0;
123    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
124        i += 1;
125    }
126    if i >= bytes.len() {
127        return None;
128    }
129
130    let start = i;
131    let open = bytes[i];
132    let (open_ch, close_ch) = match open {
133        b'{' => (b'{', b'}'),
134        b'[' => (b'[', b']'),
135        _ => return None,
136    };
137
138    let mut depth = 0usize;
139    let mut in_string = false;
140    let mut escaped = false;
141
142    while i < bytes.len() {
143        let ch = bytes[i];
144        if in_string {
145            if escaped {
146                escaped = false;
147            } else if ch == b'\\' {
148                escaped = true;
149            } else if ch == b'"' {
150                in_string = false;
151            }
152        } else {
153            match ch {
154                b'"' => in_string = true,
155                c if c == open_ch => depth += 1,
156                c if c == close_ch => {
157                    depth = depth.saturating_sub(1);
158                    if depth == 0 {
159                        let end = i + 1;
160                        let frame = input[start..end].to_string();
161                        let consumed = end + (input[end..].len() - input[end..].trim_start().len());
162                        return Some((frame, consumed));
163                    }
164                }
165                _ => {}
166            }
167        }
168        i += 1;
169    }
170
171    None
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn complete_object_in_one_chunk() {
180        let mut buf = JsonFrameBuffer::new();
181        match buf.push(r#"{"a":1}"#) {
182            BufferPush::Complete(v) => assert_eq!(v, vec![r#"{"a":1}"#.to_string()]),
183            other => panic!("expected complete, got {other:?}"),
184        }
185        assert!(buf.is_empty());
186    }
187
188    #[test]
189    fn split_object_across_chunks() {
190        let mut buf = JsonFrameBuffer::new();
191        assert_eq!(buf.push(r#"{"a":"#), BufferPush::Incomplete);
192        assert_eq!(
193            buf.push(r#"1}"#),
194            BufferPush::Complete(vec![r#"{"a":1}"#.to_string()])
195        );
196    }
197
198    #[test]
199    fn truncated_json_stays_buffered() {
200        let mut buf = JsonFrameBuffer::new();
201        assert_eq!(buf.push(r#"{"a":1"#), BufferPush::Incomplete);
202        assert!(!buf.is_empty());
203        assert_eq!(
204            buf.push("}"),
205            BufferPush::Complete(vec![r#"{"a":1}"#.to_string()])
206        );
207    }
208}