1#![allow(dead_code)]
2use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20pub type Path = Vec<PathElement>;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum PathElement {
30 Key(String),
31 Index(usize),
32}
33
34impl PathElement {
35 pub fn key(s: &str) -> Self {
36 PathElement::Key(s.to_string())
37 }
38
39 pub fn index(i: usize) -> Self {
40 PathElement::Index(i)
41 }
42}
43
44impl std::fmt::Display for PathElement {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 PathElement::Key(k) => write!(f, ".{k}"),
48 PathElement::Index(i) => write!(f, "[{i}]"),
49 }
50 }
51}
52
53pub const CMD_SET: &str = "set";
57pub const CMD_DEL: &str = "del";
58pub const CMD_APPEND: &str = "append";
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Command {
63 #[serde(rename = "type")]
65 pub cmd_type: String,
66 pub path: Vec<Value>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub data: Option<Value>,
71}
72
73impl Command {
74 pub fn set(path: Path, data: Value) -> Self {
76 Self {
77 cmd_type: CMD_SET.to_string(),
78 path: path_to_values(&path),
79 data: Some(data),
80 }
81 }
82
83 pub fn del(path: Path) -> Self {
85 Self {
86 cmd_type: CMD_DEL.to_string(),
87 path: path_to_values(&path),
88 data: None,
89 }
90 }
91
92 pub fn append(path: Path, data: Value) -> Self {
94 Self {
95 cmd_type: CMD_APPEND.to_string(),
96 path: path_to_values(&path),
97 data: Some(data),
98 }
99 }
100
101 pub fn parsed_path(&self) -> Result<Path, String> {
103 values_to_path(&self.path)
104 }
105}
106
107#[derive(Debug, Clone, Default)]
111pub struct SetPathOpts {
112 pub budget: i64,
114 pub force: bool,
116 pub remove: bool,
118}
119
120#[derive(Debug, Clone)]
124pub enum IJsonError {
125 PathError(String),
126 TypeError(String),
127 BudgetExceeded,
128 IndexOutOfBounds { index: usize, len: usize },
129}
130
131impl std::fmt::Display for IJsonError {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 IJsonError::PathError(msg) => write!(f, "path error: {msg}"),
135 IJsonError::TypeError(msg) => write!(f, "type error: {msg}"),
136 IJsonError::BudgetExceeded => write!(f, "budget exceeded"),
137 IJsonError::IndexOutOfBounds { index, len } => {
138 write!(f, "index {index} out of bounds (len {len})")
139 }
140 }
141 }
142}
143
144impl std::error::Error for IJsonError {}
145
146impl From<String> for IJsonError {
147 fn from(s: String) -> Self {
148 IJsonError::PathError(s)
149 }
150}
151
152pub fn get_path(data: &Value, path: &Path) -> Result<Option<Value>, IJsonError> {
157 let mut current = data;
158 for elem in path {
159 match elem {
160 PathElement::Key(key) => match current {
161 Value::Object(map) => match map.get(key) {
162 Some(v) => current = v,
163 None => return Ok(None),
164 },
165 _ => return Ok(None),
166 },
167 PathElement::Index(idx) => match current {
168 Value::Array(arr) => match arr.get(*idx) {
169 Some(v) => current = v,
170 None => return Ok(None),
171 },
172 _ => return Ok(None),
173 },
174 }
175 }
176 Ok(Some(current.clone()))
177}
178
179pub fn set_path(
183 data: Value,
184 path: &Path,
185 value: Value,
186 opts: &SetPathOpts,
187) -> Result<Value, IJsonError> {
188 if opts.budget < 0 {
189 return Err(IJsonError::BudgetExceeded);
190 }
191
192 if path.is_empty() {
193 if opts.remove {
194 return Ok(Value::Null);
195 }
196 return Ok(value);
197 }
198
199 set_path_recursive(data, path, 0, value, opts)
200}
201
202fn set_path_recursive(
203 data: Value,
204 path: &Path,
205 depth: usize,
206 value: Value,
207 opts: &SetPathOpts,
208) -> Result<Value, IJsonError> {
209 if depth >= path.len() {
210 if opts.remove {
211 return Ok(Value::Null);
212 }
213 return Ok(value);
214 }
215
216 let elem = &path[depth];
217 let is_last = depth == path.len() - 1;
218
219 match elem {
220 PathElement::Key(key) => {
221 let mut map = match data {
222 Value::Object(m) => m,
223 Value::Null if opts.force || depth > 0 => serde_json::Map::new(),
224 _ if opts.force => serde_json::Map::new(),
225 _ => {
226 return Err(IJsonError::TypeError(format!(
227 "expected object at path depth {depth}, got {}",
228 value_type_name(&data)
229 )));
230 }
231 };
232
233 if is_last && opts.remove {
234 map.remove(key);
235 } else if is_last {
236 map.insert(key.clone(), value);
237 } else {
238 let child = map.remove(key).unwrap_or(Value::Null);
239 let new_child = set_path_recursive(child, path, depth + 1, value, opts)?;
240 map.insert(key.clone(), new_child);
241 }
242
243 Ok(Value::Object(map))
244 }
245 PathElement::Index(idx) => {
246 let mut arr = match data {
247 Value::Array(a) => a,
248 Value::Null if opts.force || depth > 0 => Vec::new(),
249 _ if opts.force => Vec::new(),
250 _ => {
251 return Err(IJsonError::TypeError(format!(
252 "expected array at path depth {depth}, got {}",
253 value_type_name(&data)
254 )));
255 }
256 };
257
258 while arr.len() <= *idx {
260 arr.push(Value::Null);
261 }
262
263 if is_last && opts.remove {
264 if *idx < arr.len() {
265 arr.remove(*idx);
266 }
267 } else if is_last {
268 arr[*idx] = value;
269 } else {
270 let child = std::mem::replace(&mut arr[*idx], Value::Null);
271 let new_child = set_path_recursive(child, path, depth + 1, value, opts)?;
272 arr[*idx] = new_child;
273 }
274
275 Ok(Value::Array(arr))
276 }
277 }
278}
279
280pub fn apply_command(data: Value, cmd: &Command) -> Result<Value, IJsonError> {
282 let path = cmd.parsed_path()?;
283
284 match cmd.cmd_type.as_str() {
285 CMD_SET => {
286 let value = cmd.data.clone().unwrap_or(Value::Null);
287 set_path(
288 data,
289 &path,
290 value,
291 &SetPathOpts {
292 force: true,
293 ..Default::default()
294 },
295 )
296 }
297 CMD_DEL => set_path(
298 data,
299 &path,
300 Value::Null,
301 &SetPathOpts {
302 remove: true,
303 force: true,
304 ..Default::default()
305 },
306 ),
307 CMD_APPEND => {
308 let value = cmd.data.clone().unwrap_or(Value::Null);
309 let current = get_path(&data, &path)?;
311 let mut arr = match current {
312 Some(Value::Array(a)) => a,
313 Some(Value::Null) | None => Vec::new(),
314 Some(_) => {
315 return Err(IJsonError::TypeError(
316 "append target is not an array".to_string(),
317 ))
318 }
319 };
320 arr.push(value);
321 set_path(
322 data,
323 &path,
324 Value::Array(arr),
325 &SetPathOpts {
326 force: true,
327 ..Default::default()
328 },
329 )
330 }
331 other => Err(IJsonError::PathError(format!(
332 "unknown command type: {other}"
333 ))),
334 }
335}
336
337pub fn apply_commands(data: Value, commands: &[Command]) -> Result<Value, IJsonError> {
339 let mut result = data;
340 for cmd in commands {
341 result = apply_command(result, cmd)?;
342 }
343 Ok(result)
344}
345
346pub fn format_path(path: &Path) -> String {
350 let mut result = String::from("$");
351 for elem in path {
352 match elem {
353 PathElement::Key(k) => {
354 if k.chars()
356 .all(|c| c.is_ascii_alphanumeric() || c == '_')
357 && !k.is_empty()
358 && !k.chars().next().unwrap().is_ascii_digit()
359 {
360 result.push('.');
361 result.push_str(k);
362 } else {
363 result.push_str(&format!("[\"{k}\"]"));
364 }
365 }
366 PathElement::Index(i) => {
367 result.push_str(&format!("[{i}]"));
368 }
369 }
370 }
371 result
372}
373
374pub fn parse_simple_path(s: &str) -> Path {
377 if s.is_empty() {
378 return vec![];
379 }
380 s.split('.')
381 .map(|part| {
382 if let Ok(idx) = part.parse::<usize>() {
383 PathElement::Index(idx)
384 } else {
385 PathElement::Key(part.to_string())
386 }
387 })
388 .collect()
389}
390
391fn path_to_values(path: &Path) -> Vec<Value> {
395 path.iter()
396 .map(|elem| match elem {
397 PathElement::Key(k) => Value::String(k.clone()),
398 PathElement::Index(i) => Value::Number((*i as u64).into()),
399 })
400 .collect()
401}
402
403fn values_to_path(values: &[Value]) -> Result<Path, String> {
405 values
406 .iter()
407 .enumerate()
408 .map(|(i, v)| match v {
409 Value::String(s) => Ok(PathElement::Key(s.clone())),
410 Value::Number(n) => {
411 if let Some(idx) = n.as_u64() {
412 Ok(PathElement::Index(idx as usize))
413 } else {
414 Err(format!("path element {i}: expected non-negative integer"))
415 }
416 }
417 _ => Err(format!(
418 "path element {i}: expected string or number, got {}",
419 value_type_name(v)
420 )),
421 })
422 .collect::<Result<Path, String>>()
423 .map_err(|e| e.to_string())
424}
425
426fn value_type_name(v: &Value) -> &'static str {
428 match v {
429 Value::Null => "null",
430 Value::Bool(_) => "bool",
431 Value::Number(_) => "number",
432 Value::String(_) => "string",
433 Value::Array(_) => "array",
434 Value::Object(_) => "object",
435 }
436}
437
438pub fn parse_ijson(data: &str) -> Result<Vec<Command>, String> {
440 let mut commands = Vec::new();
441 for (i, line) in data.lines().enumerate() {
442 let line = line.trim();
443 if line.is_empty() {
444 continue;
445 }
446 let cmd: Command = serde_json::from_str(line)
447 .map_err(|e| format!("line {}: {e}", i + 1))?;
448 commands.push(cmd);
449 }
450 Ok(commands)
451}
452
453pub fn compact_ijson(data: &str) -> Result<String, String> {
455 let commands = parse_ijson(data)?;
456 if commands.is_empty() {
457 return Ok(String::new());
458 }
459 let result = apply_commands(Value::Null, &commands)
460 .map_err(|e| format!("apply error: {e}"))?;
461 let compact_cmd = Command::set(vec![], result);
462 serde_json::to_string(&compact_cmd).map_err(|e| format!("serialize error: {e}"))
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use serde_json::json;
469
470 #[test]
471 fn test_path_element_display() {
472 assert_eq!(PathElement::key("name").to_string(), ".name");
473 assert_eq!(PathElement::index(0).to_string(), "[0]");
474 }
475
476 #[test]
477 fn test_format_path() {
478 let path = vec![
479 PathElement::key("users"),
480 PathElement::index(0),
481 PathElement::key("name"),
482 ];
483 assert_eq!(format_path(&path), "$.users[0].name");
484 }
485
486 #[test]
487 fn test_format_path_empty() {
488 assert_eq!(format_path(&vec![]), "$");
489 }
490
491 #[test]
492 fn test_format_path_special_key() {
493 let path = vec![PathElement::key("my-key")];
494 assert_eq!(format_path(&path), "$[\"my-key\"]");
495 }
496
497 #[test]
498 fn test_parse_simple_path() {
499 let path = parse_simple_path("users.0.name");
500 assert_eq!(
501 path,
502 vec![
503 PathElement::key("users"),
504 PathElement::index(0),
505 PathElement::key("name"),
506 ]
507 );
508 }
509
510 #[test]
511 fn test_parse_simple_path_empty() {
512 assert!(parse_simple_path("").is_empty());
513 }
514
515 #[test]
516 fn test_get_path_object() {
517 let data = json!({"users": [{"name": "Alice"}, {"name": "Bob"}]});
518 let path = vec![
519 PathElement::key("users"),
520 PathElement::index(0),
521 PathElement::key("name"),
522 ];
523 let result = get_path(&data, &path).unwrap();
524 assert_eq!(result, Some(json!("Alice")));
525 }
526
527 #[test]
528 fn test_get_path_missing() {
529 let data = json!({"a": 1});
530 let path = vec![PathElement::key("b")];
531 let result = get_path(&data, &path).unwrap();
532 assert_eq!(result, None);
533 }
534
535 #[test]
536 fn test_get_path_empty() {
537 let data = json!({"a": 1});
538 let result = get_path(&data, &vec![]).unwrap();
539 assert_eq!(result, Some(json!({"a": 1})));
540 }
541
542 #[test]
543 fn test_get_path_deep_missing() {
544 let data = json!({"a": {"b": 1}});
545 let path = vec![PathElement::key("a"), PathElement::key("c")];
546 let result = get_path(&data, &path).unwrap();
547 assert_eq!(result, None);
548 }
549
550 #[test]
551 fn test_set_path_simple() {
552 let data = json!({});
553 let path = vec![PathElement::key("name")];
554 let result = set_path(data, &path, json!("Alice"), &SetPathOpts::default()).unwrap();
555 assert_eq!(result, json!({"name": "Alice"}));
556 }
557
558 #[test]
559 fn test_set_path_nested() {
560 let data = json!({});
561 let path = vec![PathElement::key("a"), PathElement::key("b")];
562 let result = set_path(
563 data,
564 &path,
565 json!(42),
566 &SetPathOpts {
567 force: true,
568 ..Default::default()
569 },
570 )
571 .unwrap();
572 assert_eq!(result, json!({"a": {"b": 42}}));
573 }
574
575 #[test]
576 fn test_set_path_array() {
577 let data = json!({"items": [1, 2, 3]});
578 let path = vec![PathElement::key("items"), PathElement::index(1)];
579 let result = set_path(data, &path, json!(99), &SetPathOpts::default()).unwrap();
580 assert_eq!(result, json!({"items": [1, 99, 3]}));
581 }
582
583 #[test]
584 fn test_set_path_array_extend() {
585 let data = json!({"items": []});
586 let path = vec![PathElement::key("items"), PathElement::index(2)];
587 let result = set_path(data, &path, json!("new"), &SetPathOpts::default()).unwrap();
588 assert_eq!(result, json!({"items": [null, null, "new"]}));
589 }
590
591 #[test]
592 fn test_set_path_remove() {
593 let data = json!({"a": 1, "b": 2});
594 let path = vec![PathElement::key("a")];
595 let result = set_path(
596 data,
597 &path,
598 Value::Null,
599 &SetPathOpts {
600 remove: true,
601 ..Default::default()
602 },
603 )
604 .unwrap();
605 assert_eq!(result, json!({"b": 2}));
606 }
607
608 #[test]
609 fn test_set_path_empty_path() {
610 let data = json!({"old": true});
611 let result =
612 set_path(data, &vec![], json!({"new": true}), &SetPathOpts::default()).unwrap();
613 assert_eq!(result, json!({"new": true}));
614 }
615
616 #[test]
617 fn test_set_path_budget_exceeded() {
618 let data = json!({});
619 let path = vec![PathElement::key("a")];
620 let result = set_path(
621 data,
622 &path,
623 json!(1),
624 &SetPathOpts {
625 budget: -1,
626 ..Default::default()
627 },
628 );
629 assert!(matches!(result, Err(IJsonError::BudgetExceeded)));
630 }
631
632 #[test]
633 fn test_command_set() {
634 let cmd = Command::set(
635 vec![PathElement::key("name")],
636 json!("Alice"),
637 );
638 assert_eq!(cmd.cmd_type, CMD_SET);
639 let json = serde_json::to_string(&cmd).unwrap();
640 assert!(json.contains("\"type\":\"set\""));
641 }
642
643 #[test]
644 fn test_command_del() {
645 let cmd = Command::del(vec![PathElement::key("name")]);
646 assert_eq!(cmd.cmd_type, CMD_DEL);
647 assert!(cmd.data.is_none());
648 }
649
650 #[test]
651 fn test_command_append() {
652 let cmd = Command::append(vec![PathElement::key("items")], json!(42));
653 assert_eq!(cmd.cmd_type, CMD_APPEND);
654 }
655
656 #[test]
657 fn test_apply_command_set() {
658 let data = json!({});
659 let cmd = Command::set(vec![PathElement::key("x")], json!(1));
660 let result = apply_command(data, &cmd).unwrap();
661 assert_eq!(result, json!({"x": 1}));
662 }
663
664 #[test]
665 fn test_apply_command_del() {
666 let data = json!({"x": 1, "y": 2});
667 let cmd = Command::del(vec![PathElement::key("x")]);
668 let result = apply_command(data, &cmd).unwrap();
669 assert_eq!(result, json!({"y": 2}));
670 }
671
672 #[test]
673 fn test_apply_command_append() {
674 let data = json!({"items": [1, 2]});
675 let cmd = Command::append(vec![PathElement::key("items")], json!(3));
676 let result = apply_command(data, &cmd).unwrap();
677 assert_eq!(result, json!({"items": [1, 2, 3]}));
678 }
679
680 #[test]
681 fn test_apply_command_append_create() {
682 let data = json!({});
683 let cmd = Command::append(vec![PathElement::key("items")], json!(1));
684 let result = apply_command(data, &cmd).unwrap();
685 assert_eq!(result, json!({"items": [1]}));
686 }
687
688 #[test]
689 fn test_apply_commands_sequence() {
690 let commands = vec![
691 Command::set(vec![], json!({})),
692 Command::set(vec![PathElement::key("name")], json!("Alice")),
693 Command::set(vec![PathElement::key("age")], json!(30)),
694 ];
695 let result = apply_commands(Value::Null, &commands).unwrap();
696 assert_eq!(result, json!({"name": "Alice", "age": 30}));
697 }
698
699 #[test]
700 fn test_parse_ijson() {
701 let input = r#"{"type":"set","path":[],"data":{}}
702{"type":"set","path":["x"],"data":1}"#;
703 let commands = parse_ijson(input).unwrap();
704 assert_eq!(commands.len(), 2);
705 assert_eq!(commands[0].cmd_type, CMD_SET);
706 assert_eq!(commands[1].cmd_type, CMD_SET);
707 }
708
709 #[test]
710 fn test_parse_ijson_empty_lines() {
711 let input = "\n\n{\"type\":\"set\",\"path\":[],\"data\":1}\n\n";
712 let commands = parse_ijson(input).unwrap();
713 assert_eq!(commands.len(), 1);
714 }
715
716 #[test]
717 fn test_compact_ijson() {
718 let input = r#"{"type":"set","path":[],"data":{}}
719{"type":"set","path":["x"],"data":1}
720{"type":"set","path":["y"],"data":2}"#;
721 let result = compact_ijson(input).unwrap();
722 let cmd: Command = serde_json::from_str(&result).unwrap();
723 assert_eq!(cmd.cmd_type, CMD_SET);
724 let data = cmd.data.unwrap();
725 assert_eq!(data, json!({"x": 1, "y": 2}));
726 }
727
728 #[test]
729 fn test_command_serde_roundtrip() {
730 let cmd = Command::set(
731 vec![PathElement::key("a"), PathElement::index(0)],
732 json!({"nested": true}),
733 );
734 let json = serde_json::to_string(&cmd).unwrap();
735 let parsed: Command = serde_json::from_str(&json).unwrap();
736 assert_eq!(parsed.cmd_type, CMD_SET);
737 assert_eq!(parsed.path.len(), 2);
738 }
739
740 #[test]
741 fn test_path_element_serde() {
742 let path = vec![PathElement::key("users"), PathElement::index(0)];
743 let json = serde_json::to_string(&path).unwrap();
744 assert_eq!(json, r#"["users",0]"#);
745 let parsed: Path = serde_json::from_str(&json).unwrap();
746 assert_eq!(parsed, path);
747 }
748
749 #[test]
750 fn test_ijson_error_display() {
751 assert_eq!(
752 IJsonError::PathError("bad path".to_string()).to_string(),
753 "path error: bad path"
754 );
755 assert_eq!(IJsonError::BudgetExceeded.to_string(), "budget exceeded");
756 assert_eq!(
757 IJsonError::IndexOutOfBounds { index: 5, len: 3 }.to_string(),
758 "index 5 out of bounds (len 3)"
759 );
760 }
761
762 #[test]
763 fn test_set_path_force_clobber() {
764 let data = json!({"a": "string_value"});
766 let path = vec![PathElement::key("a"), PathElement::key("b")];
767 let result = set_path(
768 data,
769 &path,
770 json!(1),
771 &SetPathOpts {
772 force: true,
773 ..Default::default()
774 },
775 )
776 .unwrap();
777 assert_eq!(result, json!({"a": {"b": 1}}));
778 }
779
780 #[test]
781 fn test_value_type_name() {
782 assert_eq!(value_type_name(&Value::Null), "null");
783 assert_eq!(value_type_name(&json!(true)), "bool");
784 assert_eq!(value_type_name(&json!(42)), "number");
785 assert_eq!(value_type_name(&json!("hi")), "string");
786 assert_eq!(value_type_name(&json!([])), "array");
787 assert_eq!(value_type_name(&json!({})), "object");
788 }
789}