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}