Skip to main content

reddb_server/utils/
json.rs

1//! Minimal JSON parser and serializer with zero dependencies.
2//! Implements a subset of JSON sufficient for MCP message handling.
3
4use std::fmt;
5
6/// Simplified JSON value representation.
7#[derive(Clone, Debug, PartialEq)]
8pub enum JsonValue {
9    Null,
10    Bool(bool),
11    Number(f64),
12    String(String),
13    Array(Vec<JsonValue>),
14    Object(Vec<(String, JsonValue)>),
15}
16
17impl JsonValue {
18    /// Returns the value as string reference if it is a string.
19    pub fn as_str(&self) -> Option<&str> {
20        match self {
21            JsonValue::String(s) => Some(s.as_str()),
22            _ => None,
23        }
24    }
25
26    /// Returns the value as f64 if it is a number.
27    pub fn as_f64(&self) -> Option<f64> {
28        match self {
29            JsonValue::Number(n) => Some(*n),
30            _ => None,
31        }
32    }
33
34    /// Returns the value as boolean.
35    pub fn as_bool(&self) -> Option<bool> {
36        match self {
37            JsonValue::Bool(b) => Some(*b),
38            _ => None,
39        }
40    }
41
42    /// Returns the value as array.
43    pub fn as_array(&self) -> Option<&[JsonValue]> {
44        match self {
45            JsonValue::Array(items) => Some(items.as_slice()),
46            _ => None,
47        }
48    }
49
50    /// Returns the value as mutable array.
51    pub fn as_array_mut(&mut self) -> Option<&mut Vec<JsonValue>> {
52        match self {
53            JsonValue::Array(items) => Some(items),
54            _ => None,
55        }
56    }
57
58    /// Returns the object entries if the value is an object.
59    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
60        match self {
61            JsonValue::Object(entries) => Some(entries.as_slice()),
62            _ => None,
63        }
64    }
65
66    /// Returns a mutable reference to the object entries.
67    pub fn as_object_mut(&mut self) -> Option<&mut Vec<(String, JsonValue)>> {
68        match self {
69            JsonValue::Object(entries) => Some(entries),
70            _ => None,
71        }
72    }
73
74    /// Retrieves field value from object by key.
75    pub fn get(&self, key: &str) -> Option<&JsonValue> {
76        if let JsonValue::Object(entries) = self {
77            for (k, v) in entries {
78                if k == key {
79                    return Some(v);
80                }
81            }
82        }
83        None
84    }
85
86    /// Retrieves mutable field value from object by key.
87    pub fn get_mut(&mut self, key: &str) -> Option<&mut JsonValue> {
88        if let JsonValue::Object(entries) = self {
89            for (k, v) in entries.iter_mut() {
90                if k == key {
91                    return Some(v);
92                }
93            }
94        }
95        None
96    }
97
98    /// Convenience constructor for JSON objects.
99    pub fn object(entries: Vec<(String, JsonValue)>) -> JsonValue {
100        JsonValue::Object(entries)
101    }
102
103    /// Convenience constructor for JSON arrays.
104    pub fn array(items: Vec<JsonValue>) -> JsonValue {
105        JsonValue::Array(items)
106    }
107
108    /// Serializes the value into a compact JSON string.
109    ///
110    /// **Deprecation note (ADR 0010 / issue #177):** the canonical
111    /// JSON encoder for serialization-boundary-sensitive paths
112    /// (audit log, HelloAck, PayloadReply, anything reaching a
113    /// downstream parser) is `crate::serde_json::Value::escape_string`
114    /// using `to_string_compact`. This local encoder is correct after
115    /// the F-01 hotfix (#181) but is not the canonical owner; new
116    /// audit / wire emission code should not call it. Existing MCP
117    /// JSON-RPC callers may keep using it pending a follow-up
118    /// retirement slice.
119    #[deprecated(
120        note = "Use crate::serde_json::Value::to_string_compact for boundary emission; see ADR 0010 / issue #177"
121    )]
122    pub fn to_json_string(&self) -> String {
123        let mut out = String::new();
124        self.write_json(&mut out);
125        out
126    }
127
128    fn write_json(&self, out: &mut String) {
129        match self {
130            JsonValue::Null => out.push_str("null"),
131            JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
132            JsonValue::Number(n) => {
133                if n.fract() == 0.0 {
134                    out.push_str(&format!("{}", *n as i64));
135                } else {
136                    out.push_str(&format!("{}", n));
137                }
138            }
139            JsonValue::String(s) => {
140                out.push('"');
141                for ch in s.chars() {
142                    match ch {
143                        '"' => out.push_str("\\\""),
144                        '\\' => out.push_str("\\\\"),
145                        '\n' => out.push_str("\\n"),
146                        '\r' => out.push_str("\\r"),
147                        '\t' => out.push_str("\\t"),
148                        c if c.is_control() => {
149                            out.push_str(&format!("\\u{:04x}", c as u32));
150                        }
151                        c => out.push(c),
152                    }
153                }
154                out.push('"');
155            }
156            JsonValue::Array(items) => {
157                out.push('[');
158                for (idx, item) in items.iter().enumerate() {
159                    if idx > 0 {
160                        out.push(',');
161                    }
162                    item.write_json(out);
163                }
164                out.push(']');
165            }
166            JsonValue::Object(entries) => {
167                out.push('{');
168                for (idx, (key, value)) in entries.iter().enumerate() {
169                    if idx > 0 {
170                        out.push(',');
171                    }
172                    JsonValue::String(key.clone()).write_json(out);
173                    out.push(':');
174                    value.write_json(out);
175                }
176                out.push('}');
177            }
178        }
179    }
180}
181
182impl From<&str> for JsonValue {
183    fn from(value: &str) -> JsonValue {
184        JsonValue::String(value.to_string())
185    }
186}
187
188impl From<String> for JsonValue {
189    fn from(value: String) -> JsonValue {
190        JsonValue::String(value)
191    }
192}
193
194impl From<bool> for JsonValue {
195    fn from(value: bool) -> JsonValue {
196        JsonValue::Bool(value)
197    }
198}
199
200impl From<f64> for JsonValue {
201    fn from(value: f64) -> JsonValue {
202        JsonValue::Number(value)
203    }
204}
205
206impl From<i64> for JsonValue {
207    fn from(value: i64) -> JsonValue {
208        JsonValue::Number(value as f64)
209    }
210}
211
212impl From<usize> for JsonValue {
213    fn from(value: usize) -> JsonValue {
214        JsonValue::Number(value as f64)
215    }
216}
217
218impl fmt::Display for JsonValue {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        // Internal route โ€” Display is the legacy entry point and
221        // routes through the (now deprecated for boundary use)
222        // `to_json_string`. Silence the warning here so the lint
223        // surfaces only at external call sites.
224        #[allow(deprecated)]
225        let s = self.to_json_string();
226        write!(f, "{s}")
227    }
228}
229
230/// Parser for JSON strings into [`JsonValue`].
231pub struct JsonParser<'a> {
232    input: &'a [u8],
233    pos: usize,
234}
235
236impl<'a> JsonParser<'a> {
237    /// Creates a new parser from input slice.
238    pub fn new(input: &'a str) -> Self {
239        Self {
240            input: input.as_bytes(),
241            pos: 0,
242        }
243    }
244
245    /// Parses a JSON value from the current position.
246    pub fn parse_value(&mut self) -> Result<JsonValue, String> {
247        self.skip_whitespace();
248        if self.eof() {
249            return Err("unexpected end of input".to_string());
250        }
251        let ch = self.current_char();
252        match ch {
253            b'n' => self.parse_null(),
254            b't' | b'f' => self.parse_bool(),
255            b'-' | b'0'..=b'9' => self.parse_number(),
256            b'"' => self.parse_string().map(JsonValue::String),
257            b'[' => self.parse_array(),
258            b'{' => self.parse_object(),
259            _ => Err(format!("unexpected character '{}'", ch as char)),
260        }
261    }
262
263    fn parse_null(&mut self) -> Result<JsonValue, String> {
264        self.expect_bytes(b"null")?;
265        Ok(JsonValue::Null)
266    }
267
268    fn parse_bool(&mut self) -> Result<JsonValue, String> {
269        if self.matches_bytes(b"true") {
270            self.pos += 4;
271            Ok(JsonValue::Bool(true))
272        } else if self.matches_bytes(b"false") {
273            self.pos += 5;
274            Ok(JsonValue::Bool(false))
275        } else {
276            Err("invalid boolean literal".to_string())
277        }
278    }
279
280    fn parse_number(&mut self) -> Result<JsonValue, String> {
281        let start = self.pos;
282        if self.current_char() == b'-' {
283            self.pos += 1;
284        }
285        if self.eof() {
286            return Err("invalid number literal".to_string());
287        }
288
289        match self.current_char() {
290            b'0' => {
291                self.pos += 1;
292            }
293            b'1'..=b'9' => {
294                self.pos += 1;
295                while !self.eof() && self.current_char().is_ascii_digit() {
296                    self.pos += 1;
297                }
298            }
299            _ => return Err("invalid number literal".to_string()),
300        }
301
302        if !self.eof() && self.current_char() == b'.' {
303            self.pos += 1;
304            if self.eof() || !self.current_char().is_ascii_digit() {
305                return Err("invalid number literal".to_string());
306            }
307            while !self.eof() && self.current_char().is_ascii_digit() {
308                self.pos += 1;
309            }
310        }
311
312        if !self.eof() && (self.current_char() == b'e' || self.current_char() == b'E') {
313            self.pos += 1;
314            if !self.eof() && (self.current_char() == b'+' || self.current_char() == b'-') {
315                self.pos += 1;
316            }
317            if self.eof() || !self.current_char().is_ascii_digit() {
318                return Err("invalid number literal".to_string());
319            }
320            while !self.eof() && self.current_char().is_ascii_digit() {
321                self.pos += 1;
322            }
323        }
324
325        let slice = &self.input[start..self.pos];
326        let s = std::str::from_utf8(slice).map_err(|_| "invalid UTF-8 in number".to_string())?;
327        let value = s
328            .parse::<f64>()
329            .map_err(|_| "failed to parse number".to_string())?;
330        Ok(JsonValue::Number(value))
331    }
332
333    fn parse_string(&mut self) -> Result<String, String> {
334        self.expect_char(b'"')?;
335        let mut result = String::new();
336        while !self.eof() {
337            let ch = self.current_char();
338            match ch {
339                b'"' => {
340                    self.pos += 1;
341                    return Ok(result);
342                }
343                b'\\' => {
344                    self.pos += 1;
345                    if self.eof() {
346                        return Err("unexpected end of input in escape".to_string());
347                    }
348                    let esc = self.current_char();
349                    self.pos += 1;
350                    match esc {
351                        b'"' => result.push('"'),
352                        b'\\' => result.push('\\'),
353                        b'/' => result.push('/'),
354                        b'b' => result.push('\x08'),
355                        b'f' => result.push('\x0c'),
356                        b'n' => result.push('\n'),
357                        b'r' => result.push('\r'),
358                        b't' => result.push('\t'),
359                        b'u' => {
360                            // RFC 8259 ยง7: `\uXXXX` covers code points in
361                            // the BMP. Code points above U+FFFF (e.g. emoji,
362                            // U+1F4A9 PILE OF POO) are represented in JSON
363                            // as a UTF-16 surrogate pair `๐Ÿ’ฉ` and
364                            // MUST be decoded as one Unicode scalar.
365                            let code = self.parse_unicode_escape()?;
366                            if (0xD800..=0xDBFF).contains(&code) {
367                                // High surrogate โ€” must be followed by a
368                                // `\uXXXX` low surrogate.
369                                if self.pos + 2 > self.input.len()
370                                    || self.input[self.pos] != b'\\'
371                                    || self.input[self.pos + 1] != b'u'
372                                {
373                                    return Err(
374                                        "expected low surrogate after high surrogate".to_string()
375                                    );
376                                }
377                                self.pos += 2;
378                                let low = self.parse_unicode_escape()?;
379                                if !(0xDC00..=0xDFFF).contains(&low) {
380                                    return Err("invalid low surrogate".to_string());
381                                }
382                                let scalar = 0x10000 + (((code - 0xD800) << 10) | (low - 0xDC00));
383                                if let Some(chr) = char::from_u32(scalar) {
384                                    result.push(chr);
385                                } else {
386                                    return Err("invalid unicode escape".to_string());
387                                }
388                            } else if (0xDC00..=0xDFFF).contains(&code) {
389                                return Err(
390                                    "unexpected low surrogate without preceding high surrogate"
391                                        .to_string(),
392                                );
393                            } else if let Some(chr) = char::from_u32(code) {
394                                result.push(chr);
395                            } else {
396                                return Err("invalid unicode escape".to_string());
397                            }
398                        }
399                        _ => return Err("invalid escape sequence".to_string()),
400                    }
401                }
402                _ if ch < 0x80 => {
403                    // ASCII fast path: byte == codepoint.
404                    self.pos += 1;
405                    result.push(ch as char);
406                }
407                _ => {
408                    // Multi-byte UTF-8 sequence. The parser's input came
409                    // from `&str` (validated UTF-8 at JsonParser::new), so
410                    // a complete codepoint starts here. Decoding byte-by-
411                    // byte via `ch as char` would map each continuation
412                    // byte to a Latin-1 codepoint, producing mojibake (the
413                    // root cause of issue #191 โ€” `รฉ` โ†’ `รƒยฉ`, `๐Ÿฆ€` โ†’ four
414                    // garbled chars). Instead, lift a full codepoint out
415                    // of the underlying UTF-8 stream.
416                    let next = std::str::from_utf8(&self.input[self.pos..])
417                        .map_err(|_| "invalid utf-8 in string body".to_string())?
418                        .chars()
419                        .next()
420                        .ok_or_else(|| "unterminated string literal".to_string())?;
421                    self.pos += next.len_utf8();
422                    result.push(next);
423                }
424            }
425        }
426        Err("unterminated string literal".to_string())
427    }
428
429    fn parse_unicode_escape(&mut self) -> Result<u32, String> {
430        if self.pos + 4 > self.input.len() {
431            return Err("invalid unicode escape".to_string());
432        }
433        let mut value = 0u32;
434        for _ in 0..4 {
435            let ch = self.current_char();
436            self.pos += 1;
437            value <<= 4;
438            value |= match ch {
439                b'0'..=b'9' => (ch - b'0') as u32,
440                b'a'..=b'f' => (ch - b'a' + 10) as u32,
441                b'A'..=b'F' => (ch - b'A' + 10) as u32,
442                _ => return Err("invalid unicode escape".to_string()),
443            };
444        }
445        Ok(value)
446    }
447
448    fn parse_array(&mut self) -> Result<JsonValue, String> {
449        self.expect_char(b'[')?;
450        let mut items = Vec::new();
451        self.skip_whitespace();
452        if self.peek_char() == Some(b']') {
453            self.pos += 1;
454            return Ok(JsonValue::Array(items));
455        }
456        loop {
457            let value = self.parse_value()?;
458            items.push(value);
459            self.skip_whitespace();
460            match self.peek_char() {
461                Some(b',') => {
462                    self.pos += 1;
463                }
464                Some(b']') => {
465                    self.pos += 1;
466                    break;
467                }
468                _ => return Err("expected ',' or ']' in array".to_string()),
469            }
470        }
471        Ok(JsonValue::Array(items))
472    }
473
474    fn parse_object(&mut self) -> Result<JsonValue, String> {
475        self.expect_char(b'{')?;
476        let mut entries = Vec::new();
477        self.skip_whitespace();
478        if self.peek_char() == Some(b'}') {
479            self.pos += 1;
480            return Ok(JsonValue::Object(entries));
481        }
482        loop {
483            self.skip_whitespace();
484            let key = self.parse_string()?;
485            self.skip_whitespace();
486            self.expect_char(b':')?;
487            let value = self.parse_value()?;
488            entries.push((key, value));
489            self.skip_whitespace();
490            match self.peek_char() {
491                Some(b',') => {
492                    self.pos += 1;
493                }
494                Some(b'}') => {
495                    self.pos += 1;
496                    break;
497                }
498                _ => return Err("expected ',' or '}' in object".to_string()),
499            }
500        }
501        Ok(JsonValue::Object(entries))
502    }
503
504    fn skip_whitespace(&mut self) {
505        while !self.eof() {
506            match self.current_char() {
507                b' ' | b'\n' | b'\r' | b'\t' => self.pos += 1,
508                _ => break,
509            }
510        }
511    }
512
513    fn expect_bytes(&mut self, expected: &[u8]) -> Result<(), String> {
514        if self.remaining().starts_with(expected) {
515            self.pos += expected.len();
516            Ok(())
517        } else {
518            Err("unexpected token".to_string())
519        }
520    }
521
522    fn matches_bytes(&self, expected: &[u8]) -> bool {
523        self.remaining().starts_with(expected)
524    }
525
526    fn expect_char(&mut self, expected: u8) -> Result<(), String> {
527        if self.peek_char() == Some(expected) {
528            self.pos += 1;
529            Ok(())
530        } else {
531            Err(format!("expected '{}'", expected as char))
532        }
533    }
534
535    fn peek_char(&self) -> Option<u8> {
536        if self.pos >= self.input.len() {
537            None
538        } else {
539            Some(self.input[self.pos])
540        }
541    }
542
543    fn current_char(&self) -> u8 {
544        self.input[self.pos]
545    }
546
547    fn remaining(&self) -> &[u8] {
548        &self.input[self.pos..]
549    }
550
551    fn eof(&self) -> bool {
552        self.pos >= self.input.len()
553    }
554}
555
556/// Parses the provided JSON string into a [`JsonValue`].
557pub fn parse_json(input: &str) -> Result<JsonValue, String> {
558    let mut parser = JsonParser::new(input);
559    let value = parser.parse_value()?;
560    parser.skip_whitespace();
561    if parser.eof() {
562        Ok(value)
563    } else {
564        Err("unexpected trailing data".to_string())
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    #[test]
573    fn parse_simple_object() {
574        let json = r#"{"name":"reddb","active":true,"count":3}"#;
575        let value = parse_json(json).unwrap();
576        let obj = value.as_object().unwrap();
577        assert_eq!(obj.len(), 3);
578    }
579
580    #[test]
581    fn parse_nested_array() {
582        let json = r#"{"items":[1,2,{"flag":false}]}"#;
583        let value = parse_json(json).unwrap();
584        assert!(value.get("items").unwrap().as_array().is_some());
585    }
586
587    #[test]
588    fn stringify_roundtrip() {
589        let json = r#"{"message":"hello","value":42}"#;
590        let value = parse_json(json).unwrap();
591        #[allow(deprecated)]
592        let output = value.to_json_string();
593        assert!(output.contains("hello"));
594    }
595}