1#![allow(
4 clippy::cast_lossless,
5 clippy::cast_possible_truncation,
6 clippy::cast_possible_wrap,
7 clippy::cast_sign_loss,
8 clippy::doc_markdown,
9 clippy::format_push_string,
10 clippy::needless_continue,
11 clippy::needless_range_loop,
12 clippy::single_match,
13 clippy::uninlined_format_args
14)]
15
16use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35
36use spg_storage::Value;
37
38use crate::eval::EvalError;
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum JsonValue {
42 Null,
43 Bool(bool),
44 Number(f64),
45 NumberText(String),
49 String(String),
50 Array(Vec<JsonValue>),
51 Object(Vec<(String, JsonValue)>),
52}
53
54impl JsonValue {
55 fn as_text(&self) -> String {
56 match self {
57 Self::Null => "null".into(),
58 Self::Bool(b) => if *b { "true" } else { "false" }.into(),
59 Self::Number(x) => alloc::format!("{x}"),
60 Self::NumberText(s) | Self::String(s) => s.clone(),
61 Self::Array(_) | Self::Object(_) => self.to_json_text(),
62 }
63 }
64
65 fn to_json_text(&self) -> String {
66 let mut out = String::new();
67 write_json(self, &mut out);
68 out
69 }
70}
71
72fn write_json(v: &JsonValue, out: &mut String) {
73 match v {
74 JsonValue::Null => out.push_str("null"),
75 JsonValue::Bool(true) => out.push_str("true"),
76 JsonValue::Bool(false) => out.push_str("false"),
77 JsonValue::Number(x) => out.push_str(&alloc::format!("{x}")),
78 JsonValue::NumberText(s) => out.push_str(s),
79 JsonValue::String(s) => {
80 out.push('"');
81 for c in s.chars() {
82 match c {
83 '"' => out.push_str("\\\""),
84 '\\' => out.push_str("\\\\"),
85 '\n' => out.push_str("\\n"),
86 '\r' => out.push_str("\\r"),
87 '\t' => out.push_str("\\t"),
88 c if (c as u32) < 0x20 => {
89 out.push_str(&alloc::format!("\\u{:04x}", c as u32));
90 }
91 c => out.push(c),
92 }
93 }
94 out.push('"');
95 }
96 JsonValue::Array(items) => {
97 out.push('[');
98 for (i, it) in items.iter().enumerate() {
99 if i > 0 {
100 out.push(',');
101 }
102 write_json(it, out);
103 }
104 out.push(']');
105 }
106 JsonValue::Object(entries) => {
107 out.push('{');
108 for (i, (k, val)) in entries.iter().enumerate() {
109 if i > 0 {
110 out.push(',');
111 }
112 write_json(&JsonValue::String(k.clone()), out);
113 out.push(':');
114 write_json(val, out);
115 }
116 out.push('}');
117 }
118 }
119}
120
121pub fn path_walk(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value, EvalError> {
127 let src = match lhs {
128 Value::Json(s) | Value::Text(s) => s.as_str(),
129 Value::Null => return Ok(Value::Null),
130 other => {
131 return Err(EvalError::TypeMismatch {
132 detail: alloc::format!(
133 "JSON path walk: left side must be JSON or TEXT, got {:?}",
134 other.data_type()
135 ),
136 });
137 }
138 };
139 let path_text = match rhs {
140 Value::Text(s) | Value::Json(s) => s.as_str(),
141 Value::Null => return Ok(Value::Null),
142 other => {
143 return Err(EvalError::TypeMismatch {
144 detail: alloc::format!(
145 "JSON path walk: right side must be TEXT, got {:?}",
146 other.data_type()
147 ),
148 });
149 }
150 };
151 let path = parse_text_array(path_text)?;
152 let mut cur = parse(src).map_err(|e| EvalError::TypeMismatch {
153 detail: alloc::format!("invalid JSON for path walk: {e}"),
154 })?;
155 for step in &path {
156 let next = match (&cur, step.as_str()) {
157 (JsonValue::Object(entries), key) => entries
158 .iter()
159 .find(|(k, _)| k == key)
160 .map(|(_, v)| v.clone()),
161 (JsonValue::Array(items), key) => {
162 let Ok(idx) = key.parse::<i64>() else {
163 return Ok(Value::Null);
164 };
165 if idx >= 0 {
166 items.get(idx as usize).cloned()
167 } else {
168 let from_end = items.len() as i64 + idx;
169 if from_end >= 0 {
170 items.get(from_end as usize).cloned()
171 } else {
172 None
173 }
174 }
175 }
176 _ => return Ok(Value::Null),
177 };
178 cur = match next {
179 None => return Ok(Value::Null),
180 Some(v) => v,
181 };
182 }
183 if matches!(cur, JsonValue::Null) {
184 return Ok(Value::Null);
185 }
186 if as_text {
187 Ok(Value::Text(cur.as_text()))
188 } else {
189 Ok(Value::Json(cur.to_json_text()))
190 }
191}
192
193pub fn contains(lhs: &Value, rhs: &Value) -> Result<Value, EvalError> {
201 let lhs_text = match lhs {
202 Value::Json(s) | Value::Text(s) => s.as_str(),
203 Value::Null => return Ok(Value::Null),
204 other => {
205 return Err(EvalError::TypeMismatch {
206 detail: alloc::format!(
207 "JSON @>: left side must be JSON or TEXT, got {:?}",
208 other.data_type()
209 ),
210 });
211 }
212 };
213 let rhs_text = match rhs {
214 Value::Json(s) | Value::Text(s) => s.as_str(),
215 Value::Null => return Ok(Value::Null),
216 other => {
217 return Err(EvalError::TypeMismatch {
218 detail: alloc::format!(
219 "JSON @>: right side must be JSON or TEXT, got {:?}",
220 other.data_type()
221 ),
222 });
223 }
224 };
225 let lhs_doc = parse(lhs_text).map_err(|e| EvalError::TypeMismatch {
226 detail: alloc::format!("invalid JSON on left of @>: {e}"),
227 })?;
228 let rhs_doc = parse(rhs_text).map_err(|e| EvalError::TypeMismatch {
229 detail: alloc::format!("invalid JSON on right of @>: {e}"),
230 })?;
231 Ok(Value::Bool(json_contains(&lhs_doc, &rhs_doc)))
232}
233
234fn json_contains(lhs: &JsonValue, rhs: &JsonValue) -> bool {
235 match (lhs, rhs) {
236 (JsonValue::Object(l), JsonValue::Object(r)) => r
237 .iter()
238 .all(|(rk, rv)| l.iter().any(|(lk, lv)| lk == rk && json_contains(lv, rv))),
239 (JsonValue::Array(l), JsonValue::Array(r)) => {
240 r.iter().all(|rv| l.iter().any(|lv| json_contains(lv, rv)))
241 }
242 _ => json_eq(lhs, rhs),
243 }
244}
245
246fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
247 match (a, b) {
248 (JsonValue::Null, JsonValue::Null) => true,
249 (JsonValue::Bool(x), JsonValue::Bool(y)) => x == y,
250 (JsonValue::String(x), JsonValue::String(y)) => x == y,
251 (JsonValue::Number(x), JsonValue::Number(y)) => (x - y).abs() < 1e-12,
252 (JsonValue::NumberText(x), JsonValue::NumberText(y)) => x == y,
253 (JsonValue::NumberText(x), JsonValue::Number(y))
254 | (JsonValue::Number(y), JsonValue::NumberText(x)) => {
255 x.parse::<f64>().is_ok_and(|xn| (xn - y).abs() < 1e-12)
256 }
257 (JsonValue::Array(x), JsonValue::Array(y)) => {
258 x.len() == y.len() && x.iter().zip(y).all(|(a, b)| json_eq(a, b))
259 }
260 (JsonValue::Object(x), JsonValue::Object(y)) => {
261 x.len() == y.len()
262 && x.iter()
263 .all(|(k, v)| y.iter().any(|(k2, v2)| k == k2 && json_eq(v, v2)))
264 }
265 _ => false,
266 }
267}
268
269fn parse_text_array(s: &str) -> Result<Vec<String>, EvalError> {
274 let trimmed = s.trim();
275 let inner = if let Some(stripped) = trimmed.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
276 {
277 stripped
278 } else {
279 return Err(EvalError::TypeMismatch {
280 detail: alloc::format!("path walk: expected PG array literal `{{…}}`, got {s:?}"),
281 });
282 };
283 if inner.trim().is_empty() {
284 return Ok(Vec::new());
285 }
286 let mut out = Vec::new();
287 let mut cur = String::new();
288 let mut in_quotes = false;
289 let mut chars = inner.chars().peekable();
290 while let Some(c) = chars.next() {
291 match c {
292 '"' => in_quotes = !in_quotes,
293 ',' if !in_quotes => {
294 out.push(cur.trim().to_string());
295 cur = String::new();
296 }
297 '\\' => {
298 if let Some(&next) = chars.peek() {
299 cur.push(next);
300 chars.next();
301 }
302 }
303 _ => cur.push(c),
304 }
305 }
306 out.push(cur.trim().to_string());
307 Ok(out)
308}
309
310pub fn path_get(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value, EvalError> {
315 let src = match lhs {
316 Value::Json(s) | Value::Text(s) => s.as_str(),
317 Value::Null => return Ok(Value::Null),
318 other => {
319 return Err(EvalError::TypeMismatch {
320 detail: alloc::format!(
321 "JSON path operator: left side must be JSON or TEXT, got {:?}",
322 other.data_type()
323 ),
324 });
325 }
326 };
327 let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
328 detail: alloc::format!("invalid JSON for path access: {e}"),
329 })?;
330 let inner = match (&doc, rhs) {
331 (JsonValue::Object(entries), Value::Text(k)) => entries
332 .iter()
333 .find(|(name, _)| name == k)
334 .map(|(_, v)| v.clone()),
335 (JsonValue::Array(items), Value::Int(idx)) => {
336 let n = *idx;
337 if n >= 0 {
338 items.get(n as usize).cloned()
339 } else {
340 let from_end = items.len() as i64 + i64::from(n);
341 if from_end >= 0 {
342 items.get(from_end as usize).cloned()
343 } else {
344 None
345 }
346 }
347 }
348 (JsonValue::Array(items), Value::BigInt(idx)) => {
349 let n = *idx;
350 if n >= 0 {
351 items.get(n as usize).cloned()
352 } else {
353 let from_end = items.len() as i64 + n;
354 if from_end >= 0 {
355 items.get(from_end as usize).cloned()
356 } else {
357 None
358 }
359 }
360 }
361 (_, Value::Null) => return Ok(Value::Null),
362 _ => None,
363 };
364 match inner {
365 None | Some(JsonValue::Null) => Ok(Value::Null),
366 Some(v) => {
367 if as_text {
368 Ok(Value::Text(v.as_text()))
369 } else {
370 Ok(Value::Json(v.to_json_text()))
371 }
372 }
373 }
374}
375
376#[derive(Debug)]
379pub enum ParseError {
380 Unexpected(char, usize),
381 Truncated,
382 InvalidEscape(usize),
383 InvalidNumber(usize),
384}
385
386impl core::fmt::Display for ParseError {
387 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
388 match self {
389 Self::Unexpected(c, p) => write!(f, "unexpected {c:?} at offset {p}"),
390 Self::Truncated => f.write_str("unexpected end of JSON input"),
391 Self::InvalidEscape(p) => write!(f, "invalid string escape at offset {p}"),
392 Self::InvalidNumber(p) => write!(f, "invalid number at offset {p}"),
393 }
394 }
395}
396
397pub fn parse(src: &str) -> Result<JsonValue, ParseError> {
398 let bytes = src.as_bytes();
399 let mut p = 0;
400 skip_ws(bytes, &mut p);
401 let value = parse_value(bytes, &mut p)?;
402 skip_ws(bytes, &mut p);
403 if p != bytes.len() {
404 return Err(ParseError::Unexpected(bytes[p] as char, p));
405 }
406 Ok(value)
407}
408
409fn skip_ws(bytes: &[u8], p: &mut usize) {
410 while *p < bytes.len() && matches!(bytes[*p], b' ' | b'\t' | b'\n' | b'\r') {
411 *p += 1;
412 }
413}
414
415fn parse_value(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
416 skip_ws(bytes, p);
417 if *p >= bytes.len() {
418 return Err(ParseError::Truncated);
419 }
420 match bytes[*p] {
421 b'{' => parse_object(bytes, p),
422 b'[' => parse_array(bytes, p),
423 b'"' => parse_string(bytes, p).map(JsonValue::String),
424 b't' | b'f' => parse_bool(bytes, p),
425 b'n' => parse_null(bytes, p),
426 b'-' | b'0'..=b'9' => parse_number(bytes, p),
427 c => Err(ParseError::Unexpected(c as char, *p)),
428 }
429}
430
431fn parse_object(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
432 debug_assert_eq!(bytes[*p], b'{');
433 *p += 1;
434 let mut entries = Vec::new();
435 skip_ws(bytes, p);
436 if *p < bytes.len() && bytes[*p] == b'}' {
437 *p += 1;
438 return Ok(JsonValue::Object(entries));
439 }
440 loop {
441 skip_ws(bytes, p);
442 if *p >= bytes.len() || bytes[*p] != b'"' {
443 return Err(ParseError::Unexpected(
444 bytes.get(*p).copied().unwrap_or(0) as char,
445 *p,
446 ));
447 }
448 let key = parse_string(bytes, p)?;
449 skip_ws(bytes, p);
450 if *p >= bytes.len() || bytes[*p] != b':' {
451 return Err(ParseError::Unexpected(
452 bytes.get(*p).copied().unwrap_or(0) as char,
453 *p,
454 ));
455 }
456 *p += 1;
457 let value = parse_value(bytes, p)?;
458 entries.push((key, value));
459 skip_ws(bytes, p);
460 if *p >= bytes.len() {
461 return Err(ParseError::Truncated);
462 }
463 match bytes[*p] {
464 b',' => {
465 *p += 1;
466 continue;
467 }
468 b'}' => {
469 *p += 1;
470 return Ok(JsonValue::Object(entries));
471 }
472 c => return Err(ParseError::Unexpected(c as char, *p)),
473 }
474 }
475}
476
477fn parse_array(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
478 debug_assert_eq!(bytes[*p], b'[');
479 *p += 1;
480 let mut items = Vec::new();
481 skip_ws(bytes, p);
482 if *p < bytes.len() && bytes[*p] == b']' {
483 *p += 1;
484 return Ok(JsonValue::Array(items));
485 }
486 loop {
487 items.push(parse_value(bytes, p)?);
488 skip_ws(bytes, p);
489 if *p >= bytes.len() {
490 return Err(ParseError::Truncated);
491 }
492 match bytes[*p] {
493 b',' => {
494 *p += 1;
495 continue;
496 }
497 b']' => {
498 *p += 1;
499 return Ok(JsonValue::Array(items));
500 }
501 c => return Err(ParseError::Unexpected(c as char, *p)),
502 }
503 }
504}
505
506fn parse_string(bytes: &[u8], p: &mut usize) -> Result<String, ParseError> {
507 debug_assert_eq!(bytes[*p], b'"');
508 *p += 1;
509 let mut out = String::new();
510 while *p < bytes.len() {
511 match bytes[*p] {
512 b'"' => {
513 *p += 1;
514 return Ok(out);
515 }
516 b'\\' => {
517 let start = *p;
518 *p += 1;
519 if *p >= bytes.len() {
520 return Err(ParseError::Truncated);
521 }
522 match bytes[*p] {
523 b'"' => {
524 out.push('"');
525 *p += 1;
526 }
527 b'\\' => {
528 out.push('\\');
529 *p += 1;
530 }
531 b'/' => {
532 out.push('/');
533 *p += 1;
534 }
535 b'b' => {
536 out.push('\u{08}');
537 *p += 1;
538 }
539 b'f' => {
540 out.push('\u{0c}');
541 *p += 1;
542 }
543 b'n' => {
544 out.push('\n');
545 *p += 1;
546 }
547 b'r' => {
548 out.push('\r');
549 *p += 1;
550 }
551 b't' => {
552 out.push('\t');
553 *p += 1;
554 }
555 b'u' => {
556 if *p + 5 > bytes.len() {
557 return Err(ParseError::Truncated);
558 }
559 let hex = &bytes[*p + 1..*p + 5];
560 let n = u32::from_str_radix(
561 core::str::from_utf8(hex)
562 .map_err(|_| ParseError::InvalidEscape(start))?,
563 16,
564 )
565 .map_err(|_| ParseError::InvalidEscape(start))?;
566 out.push(char::from_u32(n).ok_or(ParseError::InvalidEscape(start))?);
567 *p += 5;
568 }
569 _ => return Err(ParseError::InvalidEscape(start)),
570 }
571 }
572 c if c < 0x20 => return Err(ParseError::Unexpected(c as char, *p)),
573 _ => {
574 let s = core::str::from_utf8(&bytes[*p..])
576 .map_err(|_| ParseError::Unexpected(bytes[*p] as char, *p))?;
577 let c = s.chars().next().unwrap();
578 out.push(c);
579 *p += c.len_utf8();
580 }
581 }
582 }
583 Err(ParseError::Truncated)
584}
585
586fn parse_bool(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
587 if bytes[*p..].starts_with(b"true") {
588 *p += 4;
589 Ok(JsonValue::Bool(true))
590 } else if bytes[*p..].starts_with(b"false") {
591 *p += 5;
592 Ok(JsonValue::Bool(false))
593 } else {
594 Err(ParseError::Unexpected(bytes[*p] as char, *p))
595 }
596}
597
598fn parse_null(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
599 if bytes[*p..].starts_with(b"null") {
600 *p += 4;
601 Ok(JsonValue::Null)
602 } else {
603 Err(ParseError::Unexpected(bytes[*p] as char, *p))
604 }
605}
606
607fn parse_number(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
608 let start = *p;
609 if bytes[*p] == b'-' {
610 *p += 1;
611 }
612 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
613 *p += 1;
614 }
615 if *p < bytes.len() && bytes[*p] == b'.' {
616 *p += 1;
617 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
618 *p += 1;
619 }
620 }
621 if *p < bytes.len() && matches!(bytes[*p], b'e' | b'E') {
622 *p += 1;
623 if *p < bytes.len() && matches!(bytes[*p], b'+' | b'-') {
624 *p += 1;
625 }
626 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
627 *p += 1;
628 }
629 }
630 let text = core::str::from_utf8(&bytes[start..*p])
631 .map_err(|_| ParseError::InvalidNumber(start))?
632 .to_string();
633 if text.parse::<f64>().is_err() {
635 return Err(ParseError::InvalidNumber(start));
636 }
637 Ok(JsonValue::NumberText(text))
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn parse_atoms() {
646 assert_eq!(parse("null").unwrap(), JsonValue::Null);
647 assert_eq!(parse("true").unwrap(), JsonValue::Bool(true));
648 assert_eq!(parse("false").unwrap(), JsonValue::Bool(false));
649 assert_eq!(
650 parse("\"hello\"").unwrap(),
651 JsonValue::String("hello".into())
652 );
653 assert!(matches!(
654 parse("42").unwrap(),
655 JsonValue::NumberText(ref s) if s == "42"
656 ));
657 }
658
659 #[test]
660 fn parse_nested() {
661 let doc = parse(r#"{"a":1,"b":[true,null,"x"]}"#).unwrap();
662 let JsonValue::Object(entries) = doc else {
663 panic!("expected object");
664 };
665 assert_eq!(entries.len(), 2);
666 assert_eq!(entries[0].0, "a");
667 assert_eq!(entries[1].0, "b");
668 }
669
670 #[test]
671 fn parse_string_escapes() {
672 let s = parse(r#""he said \"hi\" and\\then\n""#).unwrap();
673 assert_eq!(s, JsonValue::String("he said \"hi\" and\\then\n".into()));
674 }
675
676 #[test]
677 fn parse_unicode_escape() {
678 assert_eq!(parse(r#""é""#).unwrap(), JsonValue::String("é".into()));
679 }
680
681 #[test]
682 fn path_object_key_returns_value() {
683 let doc = Value::Json(r#"{"name":"alice","age":30}"#.into());
684 let key = Value::Text("name".into());
685 let v = path_get(&doc, &key, true).unwrap();
686 assert_eq!(v, Value::Text("alice".into()));
687 let v = path_get(&doc, &key, false).unwrap();
688 assert_eq!(v, Value::Json("\"alice\"".into()));
689 }
690
691 #[test]
692 fn path_array_index_supports_negative() {
693 let doc = Value::Json("[10,20,30]".into());
694 let v = path_get(&doc, &Value::Int(1), true).unwrap();
695 assert_eq!(v, Value::Text("20".into()));
696 let v = path_get(&doc, &Value::Int(-1), true).unwrap();
697 assert_eq!(v, Value::Text("30".into()));
698 }
699
700 #[test]
701 fn path_missing_key_returns_null() {
702 let doc = Value::Json(r#"{"a":1}"#.into());
703 let v = path_get(&doc, &Value::Text("missing".into()), true).unwrap();
704 assert_eq!(v, Value::Null);
705 }
706
707 #[test]
708 fn path_get_nested_subtree_renders_back() {
709 let doc = Value::Json(r#"{"k":{"x":[1,2]}}"#.into());
710 let v = path_get(&doc, &Value::Text("k".into()), false).unwrap();
711 assert_eq!(v, Value::Json("{\"x\":[1,2]}".into()));
712 }
713}