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