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.iter().all(|(rk, rv)| {
237 l.iter()
238 .any(|(lk, lv)| lk == rk && json_contains(lv, rv))
239 }),
240 (JsonValue::Array(l), JsonValue::Array(r)) => r
241 .iter()
242 .all(|rv| l.iter().any(|lv| json_contains(lv, rv))),
243 _ => json_eq(lhs, rhs),
244 }
245}
246
247fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
248 match (a, b) {
249 (JsonValue::Null, JsonValue::Null) => true,
250 (JsonValue::Bool(x), JsonValue::Bool(y)) => x == y,
251 (JsonValue::String(x), JsonValue::String(y)) => x == y,
252 (JsonValue::Number(x), JsonValue::Number(y)) => (x - y).abs() < 1e-12,
253 (JsonValue::NumberText(x), JsonValue::NumberText(y)) => x == y,
254 (JsonValue::NumberText(x), JsonValue::Number(y))
255 | (JsonValue::Number(y), JsonValue::NumberText(x)) => {
256 x.parse::<f64>().is_ok_and(|xn| (xn - y).abs() < 1e-12)
257 }
258 (JsonValue::Array(x), JsonValue::Array(y)) => {
259 x.len() == y.len() && x.iter().zip(y).all(|(a, b)| json_eq(a, b))
260 }
261 (JsonValue::Object(x), JsonValue::Object(y)) => {
262 x.len() == y.len()
263 && x.iter().all(|(k, v)| {
264 y.iter().any(|(k2, v2)| k == k2 && json_eq(v, v2))
265 })
266 }
267 _ => false,
268 }
269}
270
271fn parse_text_array(s: &str) -> Result<Vec<String>, EvalError> {
276 let trimmed = s.trim();
277 let inner = if let Some(stripped) = trimmed.strip_prefix('{').and_then(|s| s.strip_suffix('}'))
278 {
279 stripped
280 } else {
281 return Err(EvalError::TypeMismatch {
282 detail: alloc::format!("path walk: expected PG array literal `{{…}}`, got {s:?}"),
283 });
284 };
285 if inner.trim().is_empty() {
286 return Ok(Vec::new());
287 }
288 let mut out = Vec::new();
289 let mut cur = String::new();
290 let mut in_quotes = false;
291 let mut chars = inner.chars().peekable();
292 while let Some(c) = chars.next() {
293 match c {
294 '"' => in_quotes = !in_quotes,
295 ',' if !in_quotes => {
296 out.push(cur.trim().to_string());
297 cur = String::new();
298 }
299 '\\' => {
300 if let Some(&next) = chars.peek() {
301 cur.push(next);
302 chars.next();
303 }
304 }
305 _ => cur.push(c),
306 }
307 }
308 out.push(cur.trim().to_string());
309 Ok(out)
310}
311
312pub fn path_get(lhs: &Value, rhs: &Value, as_text: bool) -> Result<Value, EvalError> {
317 let src = match lhs {
318 Value::Json(s) | Value::Text(s) => s.as_str(),
319 Value::Null => return Ok(Value::Null),
320 other => {
321 return Err(EvalError::TypeMismatch {
322 detail: alloc::format!(
323 "JSON path operator: left side must be JSON or TEXT, got {:?}",
324 other.data_type()
325 ),
326 });
327 }
328 };
329 let doc = parse(src).map_err(|e| EvalError::TypeMismatch {
330 detail: alloc::format!("invalid JSON for path access: {e}"),
331 })?;
332 let inner = match (&doc, rhs) {
333 (JsonValue::Object(entries), Value::Text(k)) => entries
334 .iter()
335 .find(|(name, _)| name == k)
336 .map(|(_, v)| v.clone()),
337 (JsonValue::Array(items), Value::Int(idx)) => {
338 let n = *idx;
339 if n >= 0 {
340 items.get(n as usize).cloned()
341 } else {
342 let from_end = items.len() as i64 + i64::from(n);
343 if from_end >= 0 {
344 items.get(from_end as usize).cloned()
345 } else {
346 None
347 }
348 }
349 }
350 (JsonValue::Array(items), Value::BigInt(idx)) => {
351 let n = *idx;
352 if n >= 0 {
353 items.get(n as usize).cloned()
354 } else {
355 let from_end = items.len() as i64 + n;
356 if from_end >= 0 {
357 items.get(from_end as usize).cloned()
358 } else {
359 None
360 }
361 }
362 }
363 (_, Value::Null) => return Ok(Value::Null),
364 _ => None,
365 };
366 match inner {
367 None | Some(JsonValue::Null) => Ok(Value::Null),
368 Some(v) => {
369 if as_text {
370 Ok(Value::Text(v.as_text()))
371 } else {
372 Ok(Value::Json(v.to_json_text()))
373 }
374 }
375 }
376}
377
378#[derive(Debug)]
381pub enum ParseError {
382 Unexpected(char, usize),
383 Truncated,
384 InvalidEscape(usize),
385 InvalidNumber(usize),
386}
387
388impl core::fmt::Display for ParseError {
389 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
390 match self {
391 Self::Unexpected(c, p) => write!(f, "unexpected {c:?} at offset {p}"),
392 Self::Truncated => f.write_str("unexpected end of JSON input"),
393 Self::InvalidEscape(p) => write!(f, "invalid string escape at offset {p}"),
394 Self::InvalidNumber(p) => write!(f, "invalid number at offset {p}"),
395 }
396 }
397}
398
399pub fn parse(src: &str) -> Result<JsonValue, ParseError> {
400 let bytes = src.as_bytes();
401 let mut p = 0;
402 skip_ws(bytes, &mut p);
403 let value = parse_value(bytes, &mut p)?;
404 skip_ws(bytes, &mut p);
405 if p != bytes.len() {
406 return Err(ParseError::Unexpected(bytes[p] as char, p));
407 }
408 Ok(value)
409}
410
411fn skip_ws(bytes: &[u8], p: &mut usize) {
412 while *p < bytes.len() && matches!(bytes[*p], b' ' | b'\t' | b'\n' | b'\r') {
413 *p += 1;
414 }
415}
416
417fn parse_value(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
418 skip_ws(bytes, p);
419 if *p >= bytes.len() {
420 return Err(ParseError::Truncated);
421 }
422 match bytes[*p] {
423 b'{' => parse_object(bytes, p),
424 b'[' => parse_array(bytes, p),
425 b'"' => parse_string(bytes, p).map(JsonValue::String),
426 b't' | b'f' => parse_bool(bytes, p),
427 b'n' => parse_null(bytes, p),
428 b'-' | b'0'..=b'9' => parse_number(bytes, p),
429 c => Err(ParseError::Unexpected(c as char, *p)),
430 }
431}
432
433fn parse_object(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
434 debug_assert_eq!(bytes[*p], b'{');
435 *p += 1;
436 let mut entries = Vec::new();
437 skip_ws(bytes, p);
438 if *p < bytes.len() && bytes[*p] == b'}' {
439 *p += 1;
440 return Ok(JsonValue::Object(entries));
441 }
442 loop {
443 skip_ws(bytes, p);
444 if *p >= bytes.len() || bytes[*p] != b'"' {
445 return Err(ParseError::Unexpected(
446 bytes.get(*p).copied().unwrap_or(0) as char,
447 *p,
448 ));
449 }
450 let key = parse_string(bytes, p)?;
451 skip_ws(bytes, p);
452 if *p >= bytes.len() || bytes[*p] != b':' {
453 return Err(ParseError::Unexpected(
454 bytes.get(*p).copied().unwrap_or(0) as char,
455 *p,
456 ));
457 }
458 *p += 1;
459 let value = parse_value(bytes, p)?;
460 entries.push((key, value));
461 skip_ws(bytes, p);
462 if *p >= bytes.len() {
463 return Err(ParseError::Truncated);
464 }
465 match bytes[*p] {
466 b',' => {
467 *p += 1;
468 continue;
469 }
470 b'}' => {
471 *p += 1;
472 return Ok(JsonValue::Object(entries));
473 }
474 c => return Err(ParseError::Unexpected(c as char, *p)),
475 }
476 }
477}
478
479fn parse_array(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
480 debug_assert_eq!(bytes[*p], b'[');
481 *p += 1;
482 let mut items = Vec::new();
483 skip_ws(bytes, p);
484 if *p < bytes.len() && bytes[*p] == b']' {
485 *p += 1;
486 return Ok(JsonValue::Array(items));
487 }
488 loop {
489 items.push(parse_value(bytes, p)?);
490 skip_ws(bytes, p);
491 if *p >= bytes.len() {
492 return Err(ParseError::Truncated);
493 }
494 match bytes[*p] {
495 b',' => {
496 *p += 1;
497 continue;
498 }
499 b']' => {
500 *p += 1;
501 return Ok(JsonValue::Array(items));
502 }
503 c => return Err(ParseError::Unexpected(c as char, *p)),
504 }
505 }
506}
507
508fn parse_string(bytes: &[u8], p: &mut usize) -> Result<String, ParseError> {
509 debug_assert_eq!(bytes[*p], b'"');
510 *p += 1;
511 let mut out = String::new();
512 while *p < bytes.len() {
513 match bytes[*p] {
514 b'"' => {
515 *p += 1;
516 return Ok(out);
517 }
518 b'\\' => {
519 let start = *p;
520 *p += 1;
521 if *p >= bytes.len() {
522 return Err(ParseError::Truncated);
523 }
524 match bytes[*p] {
525 b'"' => {
526 out.push('"');
527 *p += 1;
528 }
529 b'\\' => {
530 out.push('\\');
531 *p += 1;
532 }
533 b'/' => {
534 out.push('/');
535 *p += 1;
536 }
537 b'b' => {
538 out.push('\u{08}');
539 *p += 1;
540 }
541 b'f' => {
542 out.push('\u{0c}');
543 *p += 1;
544 }
545 b'n' => {
546 out.push('\n');
547 *p += 1;
548 }
549 b'r' => {
550 out.push('\r');
551 *p += 1;
552 }
553 b't' => {
554 out.push('\t');
555 *p += 1;
556 }
557 b'u' => {
558 if *p + 5 > bytes.len() {
559 return Err(ParseError::Truncated);
560 }
561 let hex = &bytes[*p + 1..*p + 5];
562 let n = u32::from_str_radix(
563 core::str::from_utf8(hex)
564 .map_err(|_| ParseError::InvalidEscape(start))?,
565 16,
566 )
567 .map_err(|_| ParseError::InvalidEscape(start))?;
568 out.push(char::from_u32(n).ok_or(ParseError::InvalidEscape(start))?);
569 *p += 5;
570 }
571 _ => return Err(ParseError::InvalidEscape(start)),
572 }
573 }
574 c if c < 0x20 => return Err(ParseError::Unexpected(c as char, *p)),
575 _ => {
576 let s = core::str::from_utf8(&bytes[*p..])
578 .map_err(|_| ParseError::Unexpected(bytes[*p] as char, *p))?;
579 let c = s.chars().next().unwrap();
580 out.push(c);
581 *p += c.len_utf8();
582 }
583 }
584 }
585 Err(ParseError::Truncated)
586}
587
588fn parse_bool(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
589 if bytes[*p..].starts_with(b"true") {
590 *p += 4;
591 Ok(JsonValue::Bool(true))
592 } else if bytes[*p..].starts_with(b"false") {
593 *p += 5;
594 Ok(JsonValue::Bool(false))
595 } else {
596 Err(ParseError::Unexpected(bytes[*p] as char, *p))
597 }
598}
599
600fn parse_null(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
601 if bytes[*p..].starts_with(b"null") {
602 *p += 4;
603 Ok(JsonValue::Null)
604 } else {
605 Err(ParseError::Unexpected(bytes[*p] as char, *p))
606 }
607}
608
609fn parse_number(bytes: &[u8], p: &mut usize) -> Result<JsonValue, ParseError> {
610 let start = *p;
611 if bytes[*p] == b'-' {
612 *p += 1;
613 }
614 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
615 *p += 1;
616 }
617 if *p < bytes.len() && bytes[*p] == b'.' {
618 *p += 1;
619 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
620 *p += 1;
621 }
622 }
623 if *p < bytes.len() && matches!(bytes[*p], b'e' | b'E') {
624 *p += 1;
625 if *p < bytes.len() && matches!(bytes[*p], b'+' | b'-') {
626 *p += 1;
627 }
628 while *p < bytes.len() && bytes[*p].is_ascii_digit() {
629 *p += 1;
630 }
631 }
632 let text = core::str::from_utf8(&bytes[start..*p])
633 .map_err(|_| ParseError::InvalidNumber(start))?
634 .to_string();
635 if text.parse::<f64>().is_err() {
637 return Err(ParseError::InvalidNumber(start));
638 }
639 Ok(JsonValue::NumberText(text))
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn parse_atoms() {
648 assert_eq!(parse("null").unwrap(), JsonValue::Null);
649 assert_eq!(parse("true").unwrap(), JsonValue::Bool(true));
650 assert_eq!(parse("false").unwrap(), JsonValue::Bool(false));
651 assert_eq!(
652 parse("\"hello\"").unwrap(),
653 JsonValue::String("hello".into())
654 );
655 assert!(matches!(
656 parse("42").unwrap(),
657 JsonValue::NumberText(ref s) if s == "42"
658 ));
659 }
660
661 #[test]
662 fn parse_nested() {
663 let doc = parse(r#"{"a":1,"b":[true,null,"x"]}"#).unwrap();
664 let JsonValue::Object(entries) = doc else {
665 panic!("expected object");
666 };
667 assert_eq!(entries.len(), 2);
668 assert_eq!(entries[0].0, "a");
669 assert_eq!(entries[1].0, "b");
670 }
671
672 #[test]
673 fn parse_string_escapes() {
674 let s = parse(r#""he said \"hi\" and\\then\n""#).unwrap();
675 assert_eq!(s, JsonValue::String("he said \"hi\" and\\then\n".into()));
676 }
677
678 #[test]
679 fn parse_unicode_escape() {
680 assert_eq!(parse(r#""é""#).unwrap(), JsonValue::String("é".into()));
681 }
682
683 #[test]
684 fn path_object_key_returns_value() {
685 let doc = Value::Json(r#"{"name":"alice","age":30}"#.into());
686 let key = Value::Text("name".into());
687 let v = path_get(&doc, &key, true).unwrap();
688 assert_eq!(v, Value::Text("alice".into()));
689 let v = path_get(&doc, &key, false).unwrap();
690 assert_eq!(v, Value::Json("\"alice\"".into()));
691 }
692
693 #[test]
694 fn path_array_index_supports_negative() {
695 let doc = Value::Json("[10,20,30]".into());
696 let v = path_get(&doc, &Value::Int(1), true).unwrap();
697 assert_eq!(v, Value::Text("20".into()));
698 let v = path_get(&doc, &Value::Int(-1), true).unwrap();
699 assert_eq!(v, Value::Text("30".into()));
700 }
701
702 #[test]
703 fn path_missing_key_returns_null() {
704 let doc = Value::Json(r#"{"a":1}"#.into());
705 let v = path_get(&doc, &Value::Text("missing".into()), true).unwrap();
706 assert_eq!(v, Value::Null);
707 }
708
709 #[test]
710 fn path_get_nested_subtree_renders_back() {
711 let doc = Value::Json(r#"{"k":{"x":[1,2]}}"#.into());
712 let v = path_get(&doc, &Value::Text("k".into()), false).unwrap();
713 assert_eq!(v, Value::Json("{\"x\":[1,2]}".into()));
714 }
715}