1use anyhow::Result;
2use serde::{Serialize, de::DeserializeOwned};
3use serde_json::Value;
4use std::{ops::Range, sync::LazyLock};
5use tree_sitter::{Query, StreamingIterator as _};
6use util::RangeExt;
7
8pub fn update_value_in_json_text<'a>(
9 text: &mut String,
10 key_path: &mut Vec<&'a str>,
11 tab_size: usize,
12 old_value: &'a Value,
13 new_value: &'a Value,
14 edits: &mut Vec<(Range<usize>, String)>,
15) {
16 // If the old and new values are both objects, then compare them key by key,
17 // preserving the comments and formatting of the unchanged parts. Otherwise,
18 // replace the old value with the new value.
19 if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
20 for (key, old_sub_value) in old_object.iter() {
21 key_path.push(key);
22 if let Some(new_sub_value) = new_object.get(key) {
23 // Key exists in both old and new, recursively update
24 update_value_in_json_text(
25 text,
26 key_path,
27 tab_size,
28 old_sub_value,
29 new_sub_value,
30 edits,
31 );
32 } else {
33 // Key was removed from new object, remove the entire key-value pair
34 let (range, replacement) =
35 replace_value_in_json_text(text, key_path, 0, None, None);
36 text.replace_range(range.clone(), &replacement);
37 edits.push((range, replacement));
38 }
39 key_path.pop();
40 }
41 for (key, new_sub_value) in new_object.iter() {
42 key_path.push(key);
43 if !old_object.contains_key(key) {
44 update_value_in_json_text(
45 text,
46 key_path,
47 tab_size,
48 &Value::Null,
49 new_sub_value,
50 edits,
51 );
52 }
53 key_path.pop();
54 }
55 } else if old_value != new_value {
56 let mut new_value = new_value.clone();
57 if let Some(new_object) = new_value.as_object_mut() {
58 new_object.retain(|_, v| !v.is_null());
59 }
60 let (range, replacement) =
61 replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
62 text.replace_range(range.clone(), &replacement);
63 edits.push((range, replacement));
64 }
65}
66
67/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
68pub fn replace_value_in_json_text<T: AsRef<str>>(
69 text: &str,
70 key_path: &[T],
71 tab_size: usize,
72 new_value: Option<&Value>,
73 replace_key: Option<&str>,
74) -> (Range<usize>, String) {
75 static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
76 Query::new(
77 &tree_sitter_json::LANGUAGE.into(),
78 "(pair key: (string) @key value: (_) @value)",
79 )
80 .expect("Failed to create PAIR_QUERY")
81 });
82
83 let mut parser = tree_sitter::Parser::new();
84 parser
85 .set_language(&tree_sitter_json::LANGUAGE.into())
86 .unwrap();
87 let syntax_tree = parser.parse(text, None).unwrap();
88
89 let mut cursor = tree_sitter::QueryCursor::new();
90
91 let mut depth = 0;
92 let mut last_value_range = 0..0;
93 let mut first_key_start = None;
94 let mut existing_value_range = 0..text.len();
95
96 let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
97 while let Some(mat) = matches.next() {
98 if mat.captures.len() != 2 {
99 continue;
100 }
101
102 let key_range = mat.captures[0].node.byte_range();
103 let value_range = mat.captures[1].node.byte_range();
104
105 // Don't enter sub objects until we find an exact
106 // match for the current keypath
107 if last_value_range.contains_inclusive(&value_range) {
108 continue;
109 }
110
111 last_value_range = value_range.clone();
112
113 if key_range.start > existing_value_range.end {
114 break;
115 }
116
117 first_key_start.get_or_insert(key_range.start);
118
119 let found_key = text
120 .get(key_range.clone())
121 .zip(key_path.get(depth))
122 .and_then(|(key_text, key_path_value)| {
123 serde_json::to_string(key_path_value.as_ref())
124 .ok()
125 .map(|key_path| depth < key_path.len() && key_text == key_path)
126 })
127 .unwrap_or(false);
128
129 if found_key {
130 existing_value_range = value_range;
131 // Reset last value range when increasing in depth
132 last_value_range = existing_value_range.start..existing_value_range.start;
133 depth += 1;
134
135 if depth == key_path.len() {
136 break;
137 }
138
139 if let Some(array_replacement) = handle_possible_array_value(
140 &mat.captures[0].node,
141 &mat.captures[1].node,
142 text,
143 &key_path[depth..],
144 new_value,
145 replace_key,
146 tab_size,
147 ) {
148 return array_replacement;
149 }
150
151 first_key_start = None;
152 }
153 }
154
155 // We found the exact key we want
156 if depth == key_path.len() {
157 if let Some(new_value) = new_value {
158 let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
159 if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) {
160 let new_key = format!("{}: ", replace_key);
161 if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
162 if let Some(prev_key_start) = text[..key_start].rfind('"') {
163 existing_value_range.start = prev_key_start;
164 } else {
165 existing_value_range.start = key_start;
166 }
167 }
168 (existing_value_range, new_key + &new_val)
169 } else {
170 (existing_value_range, new_val)
171 }
172 } else {
173 let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
174 let mut removal_end = existing_value_range.end;
175
176 // Find the actual key position by looking for the key in the pair
177 // We need to extend the range to include the key, not just the value
178 if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
179 if let Some(prev_key_start) = text[..key_start].rfind('"') {
180 removal_start = prev_key_start;
181 } else {
182 removal_start = key_start;
183 }
184 }
185
186 let mut removed_comma = false;
187 // Look backward for a preceding comma first
188 let preceding_text = text.get(0..removal_start).unwrap_or("");
189 if let Some(comma_pos) = preceding_text.rfind(',') {
190 // Check if there are only whitespace characters between the comma and our key
191 let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
192 if between_comma_and_key.trim().is_empty() {
193 removal_start = comma_pos;
194 removed_comma = true;
195 }
196 }
197 if let Some(remaining_text) = text.get(existing_value_range.end..)
198 && !removed_comma
199 {
200 let mut chars = remaining_text.char_indices();
201 while let Some((offset, ch)) = chars.next() {
202 if ch == ',' {
203 removal_end = existing_value_range.end + offset + 1;
204 // Also consume whitespace after the comma
205 for (_, next_ch) in chars.by_ref() {
206 if next_ch.is_whitespace() {
207 removal_end += next_ch.len_utf8();
208 } else {
209 break;
210 }
211 }
212 break;
213 } else if !ch.is_whitespace() {
214 break;
215 }
216 }
217 }
218 (removal_start..removal_end, String::new())
219 }
220 } else {
221 if let Some(first_key_start) = first_key_start {
222 // We have key paths, construct the sub objects
223 let new_key = key_path[depth].as_ref();
224 // We don't have the key, construct the nested objects
225 let new_value = construct_json_value(&key_path[(depth + 1)..], new_value);
226
227 let mut row = 0;
228 let mut column = 0;
229 for (ix, char) in text.char_indices() {
230 if ix == first_key_start {
231 break;
232 }
233 if char == '\n' {
234 row += 1;
235 column = 0;
236 } else {
237 column += char.len_utf8();
238 }
239 }
240
241 if row > 0 {
242 // depth is 0 based, but division needs to be 1 based.
243 let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
244 let space = ' ';
245 let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
246 (first_key_start..first_key_start, content)
247 } else {
248 let new_val = serde_json::to_string(&new_value).unwrap();
249 let mut content = format!(r#""{new_key}": {new_val},"#);
250 content.push(' ');
251 (first_key_start..first_key_start, content)
252 }
253 } else {
254 // We don't have the key, construct the nested objects
255 let new_value = construct_json_value(&key_path[depth..], new_value);
256 let indent_prefix_len = tab_size * depth;
257 let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len);
258 if depth == 0 {
259 new_val.push('\n');
260 }
261 // best effort to keep comments with best effort indentation
262 let mut replace_text = &text[existing_value_range.clone()];
263 while let Some(comment_start) = replace_text.rfind("//") {
264 if let Some(comment_end) = replace_text[comment_start..].find('\n') {
265 let mut comment_with_indent_start = replace_text[..comment_start]
266 .rfind('\n')
267 .unwrap_or(comment_start);
268 if !replace_text[comment_with_indent_start..comment_start]
269 .trim()
270 .is_empty()
271 {
272 comment_with_indent_start = comment_start;
273 }
274 new_val.insert_str(
275 1,
276 &replace_text[comment_with_indent_start..comment_start + comment_end],
277 );
278 }
279 replace_text = &replace_text[..comment_start];
280 }
281
282 (existing_value_range, new_val)
283 }
284 }
285}
286
287fn construct_json_value(
288 key_path: &[impl AsRef<str>],
289 new_value: Option<&serde_json::Value>,
290) -> serde_json::Value {
291 let mut new_value =
292 serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
293 for key in key_path.iter().rev() {
294 if parse_index_key(key.as_ref()).is_some() {
295 new_value = serde_json::json!([new_value]);
296 } else {
297 new_value = serde_json::json!({ key.as_ref().to_string(): new_value });
298 }
299 }
300 return new_value;
301}
302
303fn parse_index_key(index_key: &str) -> Option<usize> {
304 index_key.strip_prefix('#')?.parse().ok()
305}
306
307fn handle_possible_array_value(
308 key_node: &tree_sitter::Node,
309 value_node: &tree_sitter::Node,
310 text: &str,
311 remaining_key_path: &[impl AsRef<str>],
312 new_value: Option<&Value>,
313 replace_key: Option<&str>,
314 tab_size: usize,
315) -> Option<(Range<usize>, String)> {
316 if remaining_key_path.is_empty() {
317 return None;
318 }
319 let key_path = remaining_key_path;
320 let index = parse_index_key(key_path[0].as_ref())?;
321
322 let value_is_array = value_node.kind() == TS_ARRAY_KIND;
323
324 let array_str = if value_is_array {
325 &text[value_node.byte_range()]
326 } else {
327 ""
328 };
329
330 let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text(
331 array_str,
332 &key_path[1..],
333 new_value,
334 replace_key,
335 index,
336 tab_size,
337 );
338
339 if value_is_array {
340 replace_range.start += value_node.start_byte();
341 replace_range.end += value_node.start_byte();
342 } else {
343 // replace the full value if it wasn't an array
344 replace_range = value_node.byte_range();
345 }
346 let non_whitespace_char_count = replace_value.len()
347 - replace_value
348 .chars()
349 .filter(char::is_ascii_whitespace)
350 .count();
351 let needs_indent = replace_value.ends_with('\n')
352 || (replace_value
353 .chars()
354 .zip(replace_value.chars().skip(1))
355 .any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace()));
356 let contains_comment = (replace_value.contains("//") && replace_value.contains('\n'))
357 || (replace_value.contains("/*") && replace_value.contains("*/"));
358 if needs_indent {
359 let indent_width = key_node.start_position().column;
360 let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
361 replace_value = replace_value.replace('\n', &increased_indent);
362 } else if non_whitespace_char_count < 32 && !contains_comment {
363 // remove indentation
364 while let Some(idx) = replace_value.find("\n ") {
365 replace_value.remove(idx);
366 }
367 while let Some(idx) = replace_value.find(" ") {
368 replace_value.remove(idx);
369 }
370 }
371 return Some((replace_range, replace_value));
372}
373
374const TS_DOCUMENT_KIND: &str = "document";
375const TS_ARRAY_KIND: &str = "array";
376const TS_COMMENT_KIND: &str = "comment";
377
378pub fn replace_top_level_array_value_in_json_text(
379 text: &str,
380 key_path: &[impl AsRef<str>],
381 new_value: Option<&Value>,
382 replace_key: Option<&str>,
383 array_index: usize,
384 tab_size: usize,
385) -> (Range<usize>, String) {
386 let mut parser = tree_sitter::Parser::new();
387 parser
388 .set_language(&tree_sitter_json::LANGUAGE.into())
389 .unwrap();
390
391 let syntax_tree = parser.parse(text, None).unwrap();
392
393 let mut cursor = syntax_tree.walk();
394
395 if cursor.node().kind() == TS_DOCUMENT_KIND {
396 cursor.goto_first_child();
397 }
398
399 while cursor.node().kind() != TS_ARRAY_KIND {
400 if !cursor.goto_next_sibling() {
401 let json_value = construct_json_value(key_path, new_value);
402 let json_value = serde_json::json!([json_value]);
403 return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
404 }
405 }
406
407 // false if no children
408 //
409 cursor.goto_first_child();
410 debug_assert_eq!(cursor.node().kind(), "[");
411
412 let mut index = 0;
413
414 while index <= array_index {
415 let node = cursor.node();
416 if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
417 && !node.is_extra()
418 && !node.is_missing()
419 {
420 if index == array_index {
421 break;
422 }
423 index += 1;
424 }
425 if !cursor.goto_next_sibling() {
426 if let Some(new_value) = new_value {
427 return append_top_level_array_value_in_json_text(text, new_value, tab_size);
428 } else {
429 return (0..0, String::new());
430 }
431 }
432 }
433
434 let range = cursor.node().range();
435 let indent_width = range.start_point.column;
436 let offset = range.start_byte;
437 let text_range = range.start_byte..range.end_byte;
438 let value_str = &text[text_range.clone()];
439 let needs_indent = range.start_point.row > 0;
440
441 if new_value.is_none() && key_path.is_empty() {
442 let mut remove_range = text_range;
443 if index == 0 {
444 while cursor.goto_next_sibling()
445 && (cursor.node().is_extra() || cursor.node().is_missing())
446 {}
447 if cursor.node().kind() == "," {
448 remove_range.end = cursor.node().range().end_byte;
449 }
450 if let Some(next_newline) = &text[remove_range.end + 1..].find('\n')
451 && text[remove_range.end + 1..remove_range.end + next_newline]
452 .chars()
453 .all(|c| c.is_ascii_whitespace())
454 {
455 remove_range.end = remove_range.end + next_newline;
456 }
457 } else {
458 while cursor.goto_previous_sibling()
459 && (cursor.node().is_extra() || cursor.node().is_missing())
460 {}
461 if cursor.node().kind() == "," {
462 remove_range.start = cursor.node().range().start_byte;
463 }
464 }
465 (remove_range, String::new())
466 } else {
467 if let Some(array_replacement) = handle_possible_array_value(
468 &cursor.node(),
469 &cursor.node(),
470 text,
471 key_path,
472 new_value,
473 replace_key,
474 tab_size,
475 ) {
476 return array_replacement;
477 }
478 let (mut replace_range, mut replace_value) =
479 replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
480
481 replace_range.start += offset;
482 replace_range.end += offset;
483
484 if needs_indent {
485 let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
486 replace_value = replace_value.replace('\n', &increased_indent);
487 } else {
488 while let Some(idx) = replace_value.find("\n ") {
489 replace_value.remove(idx + 1);
490 }
491 while let Some(idx) = replace_value.find("\n") {
492 replace_value.replace_range(idx..idx + 1, " ");
493 }
494 }
495
496 (replace_range, replace_value)
497 }
498}
499
500pub fn append_top_level_array_value_in_json_text(
501 text: &str,
502 new_value: &Value,
503 tab_size: usize,
504) -> (Range<usize>, String) {
505 let mut parser = tree_sitter::Parser::new();
506 parser
507 .set_language(&tree_sitter_json::LANGUAGE.into())
508 .unwrap();
509 let syntax_tree = parser.parse(text, None).unwrap();
510
511 let mut cursor = syntax_tree.walk();
512
513 if cursor.node().kind() == TS_DOCUMENT_KIND {
514 cursor.goto_first_child();
515 }
516
517 while cursor.node().kind() != TS_ARRAY_KIND {
518 if !cursor.goto_next_sibling() {
519 let json_value = serde_json::json!([new_value]);
520 return (0..text.len(), to_pretty_json(&json_value, tab_size, 0));
521 }
522 }
523
524 let went_to_last_child = cursor.goto_last_child();
525 debug_assert!(
526 went_to_last_child && cursor.node().kind() == "]",
527 "Malformed JSON syntax tree, expected `]` at end of array"
528 );
529 let close_bracket_start = cursor.node().start_byte();
530 while cursor.goto_previous_sibling()
531 && (cursor.node().is_extra() || cursor.node().is_missing())
532 && !cursor.node().is_error()
533 {}
534
535 let mut comma_range = None;
536 let mut prev_item_range = None;
537
538 if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") {
539 comma_range = Some(cursor.node().byte_range());
540 while cursor.goto_previous_sibling()
541 && (cursor.node().is_extra() || cursor.node().is_missing())
542 {}
543
544 debug_assert_ne!(cursor.node().kind(), "[");
545 prev_item_range = Some(cursor.node().range());
546 } else {
547 while (cursor.node().is_extra() || cursor.node().is_missing())
548 && cursor.goto_previous_sibling()
549 {}
550 if cursor.node().kind() != "[" {
551 prev_item_range = Some(cursor.node().range());
552 }
553 }
554
555 let (mut replace_range, mut replace_value) =
556 replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None);
557
558 replace_range.start = close_bracket_start;
559 replace_range.end = close_bracket_start;
560
561 let space = ' ';
562 if let Some(prev_item_range) = prev_item_range {
563 let needs_newline = prev_item_range.start_point.row > 0;
564 let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
565 prev_item_range.start_point.column,
566 |idx| {
567 prev_item_range.start_point.column
568 - text[idx + 1..prev_item_range.start_byte].trim_start().len()
569 },
570 );
571
572 let prev_item_end = comma_range
573 .as_ref()
574 .map_or(prev_item_range.end_byte, |range| range.end);
575 if text[prev_item_end..replace_range.start].trim().is_empty() {
576 replace_range.start = prev_item_end;
577 }
578
579 if needs_newline {
580 let increased_indent = format!("\n{space:width$}", width = indent_width);
581 replace_value = replace_value.replace('\n', &increased_indent);
582 replace_value.push('\n');
583 replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
584 } else {
585 while let Some(idx) = replace_value.find("\n ") {
586 replace_value.remove(idx + 1);
587 }
588 while let Some(idx) = replace_value.find('\n') {
589 replace_value.replace_range(idx..idx + 1, " ");
590 }
591 replace_value.insert(0, ' ');
592 }
593
594 if comma_range.is_none() {
595 replace_value.insert(0, ',');
596 }
597 } else if replace_value.contains('\n') || text.contains('\n') {
598 if let Some(prev_newline) = text[..replace_range.start].rfind('\n')
599 && text[prev_newline..replace_range.start].trim().is_empty()
600 {
601 replace_range.start = prev_newline;
602 }
603 let indent = format!("\n{space:width$}", width = tab_size);
604 replace_value = replace_value.replace('\n', &indent);
605 replace_value.insert_str(0, &indent);
606 replace_value.push('\n');
607 }
608 return (replace_range, replace_value);
609
610 fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool {
611 if cursor.node().kind() != "ERROR" {
612 return false;
613 }
614
615 let descendant_index = cursor.descendant_index();
616 let res = cursor.goto_first_child() && cursor.node().kind() == kind;
617 cursor.goto_descendant(descendant_index);
618 res
619 }
620}
621
622/// Infers the indentation size used in JSON text by analyzing the tree structure.
623/// Returns the detected indent size, or a default of 2 if no indentation is found.
624pub fn infer_json_indent_size(text: &str) -> usize {
625 const MAX_INDENT_SIZE: usize = 64;
626
627 let mut parser = tree_sitter::Parser::new();
628 parser
629 .set_language(&tree_sitter_json::LANGUAGE.into())
630 .unwrap();
631
632 let Some(syntax_tree) = parser.parse(text, None) else {
633 return 4;
634 };
635
636 let mut cursor = syntax_tree.walk();
637 let mut indent_counts = [0u32; MAX_INDENT_SIZE];
638
639 // Traverse the tree to find indentation patterns
640 fn visit_node(
641 cursor: &mut tree_sitter::TreeCursor,
642 indent_counts: &mut [u32; MAX_INDENT_SIZE],
643 depth: usize,
644 ) {
645 if depth >= 3 {
646 return;
647 }
648 let node = cursor.node();
649 let node_kind = node.kind();
650
651 // For objects and arrays, check the indentation of their first content child
652 if matches!(node_kind, "object" | "array") {
653 let container_column = node.start_position().column;
654 let container_row = node.start_position().row;
655
656 if cursor.goto_first_child() {
657 // Skip the opening bracket
658 loop {
659 let child = cursor.node();
660 let child_kind = child.kind();
661
662 // Look for the first actual content (pair for objects, value for arrays)
663 if (node_kind == "object" && child_kind == "pair")
664 || (node_kind == "array"
665 && !matches!(child_kind, "[" | "]" | "," | "comment"))
666 {
667 let child_column = child.start_position().column;
668 let child_row = child.start_position().row;
669
670 // Only count if the child is on a different line
671 if child_row > container_row && child_column > container_column {
672 let indent = child_column - container_column;
673 if indent > 0 && indent < MAX_INDENT_SIZE {
674 indent_counts[indent] += 1;
675 }
676 }
677 break;
678 }
679
680 if !cursor.goto_next_sibling() {
681 break;
682 }
683 }
684 cursor.goto_parent();
685 }
686 }
687
688 // Recurse to children
689 if cursor.goto_first_child() {
690 loop {
691 visit_node(cursor, indent_counts, depth + 1);
692 if !cursor.goto_next_sibling() {
693 break;
694 }
695 }
696 cursor.goto_parent();
697 }
698 }
699
700 visit_node(&mut cursor, &mut indent_counts, 0);
701
702 // Find the indent size with the highest count
703 let mut max_count = 0;
704 let mut max_indent = 4;
705
706 for (indent, &count) in indent_counts.iter().enumerate() {
707 if count > max_count {
708 max_count = count;
709 max_indent = indent;
710 }
711 }
712
713 if max_count == 0 { 2 } else { max_indent }
714}
715
716pub fn to_pretty_json(
717 value: &impl Serialize,
718 indent_size: usize,
719 indent_prefix_len: usize,
720) -> String {
721 let mut output = Vec::new();
722 let indent = " ".repeat(indent_size);
723 let mut ser = serde_json::Serializer::with_formatter(
724 &mut output,
725 serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()),
726 );
727
728 value.serialize(&mut ser).unwrap();
729 let text = String::from_utf8(output).unwrap();
730
731 let mut adjusted_text = String::new();
732 for (i, line) in text.split('\n').enumerate() {
733 if i > 0 {
734 adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len));
735 }
736 adjusted_text.push_str(line);
737 adjusted_text.push('\n');
738 }
739 adjusted_text.pop();
740 adjusted_text
741}
742
743pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
744 let mut deserializer = serde_json_lenient::Deserializer::from_str(content);
745 Ok(serde_path_to_error::deserialize(&mut deserializer)?)
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use serde_json::{Value, json};
752 use unindent::Unindent;
753
754 #[test]
755 fn object_replace() {
756 #[track_caller]
757 fn check_object_replace(
758 input: String,
759 key_path: &[&str],
760 value: Option<Value>,
761 expected: String,
762 ) {
763 let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
764 let mut result_str = input;
765 result_str.replace_range(result.0, &result.1);
766 pretty_assertions::assert_eq!(expected, result_str);
767 }
768 check_object_replace(
769 r#"{
770 "a": 1,
771 "b": 2
772 }"#
773 .unindent(),
774 &["b"],
775 Some(json!(3)),
776 r#"{
777 "a": 1,
778 "b": 3
779 }"#
780 .unindent(),
781 );
782 check_object_replace(
783 r#"{
784 "a": 1,
785 "b": 2
786 }"#
787 .unindent(),
788 &["b"],
789 None,
790 r#"{
791 "a": 1
792 }"#
793 .unindent(),
794 );
795 check_object_replace(
796 r#"{
797 "a": 1,
798 "b": 2
799 }"#
800 .unindent(),
801 &["c"],
802 Some(json!(3)),
803 r#"{
804 "c": 3,
805 "a": 1,
806 "b": 2
807 }"#
808 .unindent(),
809 );
810 check_object_replace(
811 r#"{
812 "a": 1,
813 "b": {
814 "c": 2,
815 "d": 3,
816 }
817 }"#
818 .unindent(),
819 &["b", "c"],
820 Some(json!([1, 2, 3])),
821 r#"{
822 "a": 1,
823 "b": {
824 "c": [
825 1,
826 2,
827 3
828 ],
829 "d": 3,
830 }
831 }"#
832 .unindent(),
833 );
834
835 check_object_replace(
836 r#"{
837 "name": "old_name",
838 "id": 123
839 }"#
840 .unindent(),
841 &["name"],
842 Some(json!("new_name")),
843 r#"{
844 "name": "new_name",
845 "id": 123
846 }"#
847 .unindent(),
848 );
849
850 check_object_replace(
851 r#"{
852 "enabled": false,
853 "count": 5
854 }"#
855 .unindent(),
856 &["enabled"],
857 Some(json!(true)),
858 r#"{
859 "enabled": true,
860 "count": 5
861 }"#
862 .unindent(),
863 );
864
865 check_object_replace(
866 r#"{
867 "value": null,
868 "other": "test"
869 }"#
870 .unindent(),
871 &["value"],
872 Some(json!(42)),
873 r#"{
874 "value": 42,
875 "other": "test"
876 }"#
877 .unindent(),
878 );
879
880 check_object_replace(
881 r#"{
882 "config": {
883 "old": true
884 },
885 "name": "test"
886 }"#
887 .unindent(),
888 &["config"],
889 Some(json!({"new": false, "count": 3})),
890 r#"{
891 "config": {
892 "new": false,
893 "count": 3
894 },
895 "name": "test"
896 }"#
897 .unindent(),
898 );
899
900 check_object_replace(
901 r#"{
902 // This is a comment
903 "a": 1,
904 "b": 2 // Another comment
905 }"#
906 .unindent(),
907 &["b"],
908 Some(json!({"foo": "bar"})),
909 r#"{
910 // This is a comment
911 "a": 1,
912 "b": {
913 "foo": "bar"
914 } // Another comment
915 }"#
916 .unindent(),
917 );
918
919 check_object_replace(
920 r#"{}"#.to_string(),
921 &["new_key"],
922 Some(json!("value")),
923 r#"{
924 "new_key": "value"
925 }
926 "#
927 .unindent(),
928 );
929
930 check_object_replace(
931 r#"{
932 "only_key": 123
933 }"#
934 .unindent(),
935 &["only_key"],
936 None,
937 "{\n \n}".to_string(),
938 );
939
940 check_object_replace(
941 r#"{
942 "level1": {
943 "level2": {
944 "level3": {
945 "target": "old"
946 }
947 }
948 }
949 }"#
950 .unindent(),
951 &["level1", "level2", "level3", "target"],
952 Some(json!("new")),
953 r#"{
954 "level1": {
955 "level2": {
956 "level3": {
957 "target": "new"
958 }
959 }
960 }
961 }"#
962 .unindent(),
963 );
964
965 check_object_replace(
966 r#"{
967 "parent": {}
968 }"#
969 .unindent(),
970 &["parent", "child"],
971 Some(json!("value")),
972 r#"{
973 "parent": {
974 "child": "value"
975 }
976 }"#
977 .unindent(),
978 );
979
980 check_object_replace(
981 r#"{
982 "a": 1,
983 "b": 2,
984 }"#
985 .unindent(),
986 &["b"],
987 Some(json!(3)),
988 r#"{
989 "a": 1,
990 "b": 3,
991 }"#
992 .unindent(),
993 );
994
995 check_object_replace(
996 r#"{
997 "items": [1, 2, 3],
998 "count": 3
999 }"#
1000 .unindent(),
1001 &["items", "1"],
1002 Some(json!(5)),
1003 r#"{
1004 "items": {
1005 "1": 5
1006 },
1007 "count": 3
1008 }"#
1009 .unindent(),
1010 );
1011
1012 check_object_replace(
1013 r#"{
1014 "items": [1, 2, 3],
1015 "count": 3
1016 }"#
1017 .unindent(),
1018 &["items", "1"],
1019 None,
1020 r#"{
1021 "items": {
1022 "1": null
1023 },
1024 "count": 3
1025 }"#
1026 .unindent(),
1027 );
1028
1029 check_object_replace(
1030 r#"{
1031 "items": [1, 2, 3],
1032 "count": 3
1033 }"#
1034 .unindent(),
1035 &["items"],
1036 Some(json!(["a", "b", "c", "d"])),
1037 r#"{
1038 "items": [
1039 "a",
1040 "b",
1041 "c",
1042 "d"
1043 ],
1044 "count": 3
1045 }"#
1046 .unindent(),
1047 );
1048
1049 check_object_replace(
1050 r#"{
1051 "0": "zero",
1052 "1": "one"
1053 }"#
1054 .unindent(),
1055 &["1"],
1056 Some(json!("ONE")),
1057 r#"{
1058 "0": "zero",
1059 "1": "ONE"
1060 }"#
1061 .unindent(),
1062 );
1063 // Test with comments between object members
1064 check_object_replace(
1065 r#"{
1066 "a": 1,
1067 // Comment between members
1068 "b": 2,
1069 /* Block comment */
1070 "c": 3
1071 }"#
1072 .unindent(),
1073 &["b"],
1074 Some(json!({"nested": true})),
1075 r#"{
1076 "a": 1,
1077 // Comment between members
1078 "b": {
1079 "nested": true
1080 },
1081 /* Block comment */
1082 "c": 3
1083 }"#
1084 .unindent(),
1085 );
1086
1087 // Test with trailing comments on replaced value
1088 check_object_replace(
1089 r#"{
1090 "a": 1, // keep this comment
1091 "b": 2 // this should stay
1092 }"#
1093 .unindent(),
1094 &["a"],
1095 Some(json!("changed")),
1096 r#"{
1097 "a": "changed", // keep this comment
1098 "b": 2 // this should stay
1099 }"#
1100 .unindent(),
1101 );
1102
1103 // Test with deep indentation
1104 check_object_replace(
1105 r#"{
1106 "deeply": {
1107 "nested": {
1108 "value": "old"
1109 }
1110 }
1111 }"#
1112 .unindent(),
1113 &["deeply", "nested", "value"],
1114 Some(json!("new")),
1115 r#"{
1116 "deeply": {
1117 "nested": {
1118 "value": "new"
1119 }
1120 }
1121 }"#
1122 .unindent(),
1123 );
1124
1125 // Test removing value with comment preservation
1126 check_object_replace(
1127 r#"{
1128 // Header comment
1129 "a": 1,
1130 // This comment belongs to b
1131 "b": 2,
1132 // This comment belongs to c
1133 "c": 3
1134 }"#
1135 .unindent(),
1136 &["b"],
1137 None,
1138 r#"{
1139 // Header comment
1140 "a": 1,
1141 // This comment belongs to b
1142 // This comment belongs to c
1143 "c": 3
1144 }"#
1145 .unindent(),
1146 );
1147
1148 // Test with multiline block comments
1149 check_object_replace(
1150 r#"{
1151 /*
1152 * This is a multiline
1153 * block comment
1154 */
1155 "value": "old",
1156 /* Another block */ "other": 123
1157 }"#
1158 .unindent(),
1159 &["value"],
1160 Some(json!("new")),
1161 r#"{
1162 /*
1163 * This is a multiline
1164 * block comment
1165 */
1166 "value": "new",
1167 /* Another block */ "other": 123
1168 }"#
1169 .unindent(),
1170 );
1171
1172 check_object_replace(
1173 r#"{
1174 // This object is empty
1175 }"#
1176 .unindent(),
1177 &["key"],
1178 Some(json!("value")),
1179 r#"{
1180 // This object is empty
1181 "key": "value"
1182 }
1183 "#
1184 .unindent(),
1185 );
1186
1187 // Test replacing in object with only comments
1188 check_object_replace(
1189 r#"{
1190 // Comment 1
1191 // Comment 2
1192 }"#
1193 .unindent(),
1194 &["new"],
1195 Some(json!(42)),
1196 r#"{
1197 // Comment 1
1198 // Comment 2
1199 "new": 42
1200 }
1201 "#
1202 .unindent(),
1203 );
1204
1205 // Test with inconsistent spacing
1206 check_object_replace(
1207 r#"{
1208 "a":1,
1209 "b" : 2 ,
1210 "c": 3
1211 }"#
1212 .unindent(),
1213 &["b"],
1214 Some(json!("spaced")),
1215 r#"{
1216 "a":1,
1217 "b" : "spaced" ,
1218 "c": 3
1219 }"#
1220 .unindent(),
1221 );
1222 }
1223
1224 #[test]
1225 fn object_replace_array() {
1226 // Tests replacing values within arrays that are nested inside objects.
1227 // Uses "#N" syntax in key paths to indicate array indices.
1228 #[track_caller]
1229 fn check_object_replace_array(
1230 input: String,
1231 key_path: &[&str],
1232 value: Option<Value>,
1233 expected: String,
1234 ) {
1235 let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
1236 let mut result_str = input;
1237 result_str.replace_range(result.0, &result.1);
1238 pretty_assertions::assert_eq!(expected, result_str);
1239 }
1240
1241 // Basic array element replacement
1242 check_object_replace_array(
1243 r#"{
1244 "a": [1, 3],
1245 }"#
1246 .unindent(),
1247 &["a", "#1"],
1248 Some(json!(2)),
1249 r#"{
1250 "a": [1, 2],
1251 }"#
1252 .unindent(),
1253 );
1254
1255 // Replace first element
1256 check_object_replace_array(
1257 r#"{
1258 "items": [1, 2, 3]
1259 }"#
1260 .unindent(),
1261 &["items", "#0"],
1262 Some(json!(10)),
1263 r#"{
1264 "items": [10, 2, 3]
1265 }"#
1266 .unindent(),
1267 );
1268
1269 // Replace last element
1270 check_object_replace_array(
1271 r#"{
1272 "items": [1, 2, 3]
1273 }"#
1274 .unindent(),
1275 &["items", "#2"],
1276 Some(json!(30)),
1277 r#"{
1278 "items": [1, 2, 30]
1279 }"#
1280 .unindent(),
1281 );
1282
1283 // Replace string in array
1284 check_object_replace_array(
1285 r#"{
1286 "names": ["alice", "bob", "charlie"]
1287 }"#
1288 .unindent(),
1289 &["names", "#1"],
1290 Some(json!("robert")),
1291 r#"{
1292 "names": ["alice", "robert", "charlie"]
1293 }"#
1294 .unindent(),
1295 );
1296
1297 // Replace boolean
1298 check_object_replace_array(
1299 r#"{
1300 "flags": [true, false, true]
1301 }"#
1302 .unindent(),
1303 &["flags", "#0"],
1304 Some(json!(false)),
1305 r#"{
1306 "flags": [false, false, true]
1307 }"#
1308 .unindent(),
1309 );
1310
1311 // Replace null with value
1312 check_object_replace_array(
1313 r#"{
1314 "values": [null, 2, null]
1315 }"#
1316 .unindent(),
1317 &["values", "#0"],
1318 Some(json!(1)),
1319 r#"{
1320 "values": [1, 2, null]
1321 }"#
1322 .unindent(),
1323 );
1324
1325 // Replace value with null
1326 check_object_replace_array(
1327 r#"{
1328 "data": [1, 2, 3]
1329 }"#
1330 .unindent(),
1331 &["data", "#1"],
1332 Some(json!(null)),
1333 r#"{
1334 "data": [1, null, 3]
1335 }"#
1336 .unindent(),
1337 );
1338
1339 // Replace simple value with object
1340 check_object_replace_array(
1341 r#"{
1342 "list": [1, 2, 3]
1343 }"#
1344 .unindent(),
1345 &["list", "#1"],
1346 Some(json!({"value": 2, "label": "two"})),
1347 r#"{
1348 "list": [1, { "value": 2, "label": "two" }, 3]
1349 }"#
1350 .unindent(),
1351 );
1352
1353 // Replace simple value with nested array
1354 check_object_replace_array(
1355 r#"{
1356 "matrix": [1, 2, 3]
1357 }"#
1358 .unindent(),
1359 &["matrix", "#1"],
1360 Some(json!([20, 21, 22])),
1361 r#"{
1362 "matrix": [1, [ 20, 21, 22 ], 3]
1363 }"#
1364 .unindent(),
1365 );
1366
1367 // Replace object in array
1368 check_object_replace_array(
1369 r#"{
1370 "users": [
1371 {"name": "alice"},
1372 {"name": "bob"},
1373 {"name": "charlie"}
1374 ]
1375 }"#
1376 .unindent(),
1377 &["users", "#1"],
1378 Some(json!({"name": "robert", "age": 30})),
1379 r#"{
1380 "users": [
1381 {"name": "alice"},
1382 { "name": "robert", "age": 30 },
1383 {"name": "charlie"}
1384 ]
1385 }"#
1386 .unindent(),
1387 );
1388
1389 // Replace property within object in array
1390 check_object_replace_array(
1391 r#"{
1392 "users": [
1393 {"name": "alice", "age": 25},
1394 {"name": "bob", "age": 30},
1395 {"name": "charlie", "age": 35}
1396 ]
1397 }"#
1398 .unindent(),
1399 &["users", "#1", "age"],
1400 Some(json!(31)),
1401 r#"{
1402 "users": [
1403 {"name": "alice", "age": 25},
1404 {"name": "bob", "age": 31},
1405 {"name": "charlie", "age": 35}
1406 ]
1407 }"#
1408 .unindent(),
1409 );
1410
1411 // Add new property to object in array
1412 check_object_replace_array(
1413 r#"{
1414 "items": [
1415 {"id": 1},
1416 {"id": 2},
1417 {"id": 3}
1418 ]
1419 }"#
1420 .unindent(),
1421 &["items", "#1", "name"],
1422 Some(json!("Item Two")),
1423 r#"{
1424 "items": [
1425 {"id": 1},
1426 {"name": "Item Two", "id": 2},
1427 {"id": 3}
1428 ]
1429 }"#
1430 .unindent(),
1431 );
1432
1433 // Remove property from object in array
1434 check_object_replace_array(
1435 r#"{
1436 "items": [
1437 {"id": 1, "name": "one"},
1438 {"id": 2, "name": "two"},
1439 {"id": 3, "name": "three"}
1440 ]
1441 }"#
1442 .unindent(),
1443 &["items", "#1", "name"],
1444 None,
1445 r#"{
1446 "items": [
1447 {"id": 1, "name": "one"},
1448 {"id": 2},
1449 {"id": 3, "name": "three"}
1450 ]
1451 }"#
1452 .unindent(),
1453 );
1454
1455 // Deeply nested: array in object in array
1456 check_object_replace_array(
1457 r#"{
1458 "data": [
1459 {
1460 "values": [1, 2, 3]
1461 },
1462 {
1463 "values": [4, 5, 6]
1464 }
1465 ]
1466 }"#
1467 .unindent(),
1468 &["data", "#0", "values", "#1"],
1469 Some(json!(20)),
1470 r#"{
1471 "data": [
1472 {
1473 "values": [1, 20, 3]
1474 },
1475 {
1476 "values": [4, 5, 6]
1477 }
1478 ]
1479 }"#
1480 .unindent(),
1481 );
1482
1483 // Multiple levels of nesting
1484 check_object_replace_array(
1485 r#"{
1486 "root": {
1487 "level1": [
1488 {
1489 "level2": {
1490 "level3": [10, 20, 30]
1491 }
1492 }
1493 ]
1494 }
1495 }"#
1496 .unindent(),
1497 &["root", "level1", "#0", "level2", "level3", "#2"],
1498 Some(json!(300)),
1499 r#"{
1500 "root": {
1501 "level1": [
1502 {
1503 "level2": {
1504 "level3": [10, 20, 300]
1505 }
1506 }
1507 ]
1508 }
1509 }"#
1510 .unindent(),
1511 );
1512
1513 // Array with mixed types
1514 check_object_replace_array(
1515 r#"{
1516 "mixed": [1, "two", true, null, {"five": 5}]
1517 }"#
1518 .unindent(),
1519 &["mixed", "#3"],
1520 Some(json!({"four": 4})),
1521 r#"{
1522 "mixed": [1, "two", true, { "four": 4 }, {"five": 5}]
1523 }"#
1524 .unindent(),
1525 );
1526
1527 // Replace with complex object
1528 check_object_replace_array(
1529 r#"{
1530 "config": [
1531 "simple",
1532 "values"
1533 ]
1534 }"#
1535 .unindent(),
1536 &["config", "#0"],
1537 Some(json!({
1538 "type": "complex",
1539 "settings": {
1540 "enabled": true,
1541 "level": 5
1542 }
1543 })),
1544 r#"{
1545 "config": [
1546 {
1547 "type": "complex",
1548 "settings": {
1549 "enabled": true,
1550 "level": 5
1551 }
1552 },
1553 "values"
1554 ]
1555 }"#
1556 .unindent(),
1557 );
1558
1559 // Array with trailing comma
1560 check_object_replace_array(
1561 r#"{
1562 "items": [
1563 1,
1564 2,
1565 3,
1566 ]
1567 }"#
1568 .unindent(),
1569 &["items", "#1"],
1570 Some(json!(20)),
1571 r#"{
1572 "items": [
1573 1,
1574 20,
1575 3,
1576 ]
1577 }"#
1578 .unindent(),
1579 );
1580
1581 // Array with comments
1582 check_object_replace_array(
1583 r#"{
1584 "items": [
1585 1, // first item
1586 2, // second item
1587 3 // third item
1588 ]
1589 }"#
1590 .unindent(),
1591 &["items", "#1"],
1592 Some(json!(20)),
1593 r#"{
1594 "items": [
1595 1, // first item
1596 20, // second item
1597 3 // third item
1598 ]
1599 }"#
1600 .unindent(),
1601 );
1602
1603 // Multiple arrays in object
1604 check_object_replace_array(
1605 r#"{
1606 "first": [1, 2, 3],
1607 "second": [4, 5, 6],
1608 "third": [7, 8, 9]
1609 }"#
1610 .unindent(),
1611 &["second", "#1"],
1612 Some(json!(50)),
1613 r#"{
1614 "first": [1, 2, 3],
1615 "second": [4, 50, 6],
1616 "third": [7, 8, 9]
1617 }"#
1618 .unindent(),
1619 );
1620
1621 // Empty array - add first element
1622 check_object_replace_array(
1623 r#"{
1624 "empty": []
1625 }"#
1626 .unindent(),
1627 &["empty", "#0"],
1628 Some(json!("first")),
1629 r#"{
1630 "empty": ["first"]
1631 }"#
1632 .unindent(),
1633 );
1634
1635 // Array of arrays
1636 check_object_replace_array(
1637 r#"{
1638 "matrix": [
1639 [1, 2],
1640 [3, 4],
1641 [5, 6]
1642 ]
1643 }"#
1644 .unindent(),
1645 &["matrix", "#1", "#0"],
1646 Some(json!(30)),
1647 r#"{
1648 "matrix": [
1649 [1, 2],
1650 [30, 4],
1651 [5, 6]
1652 ]
1653 }"#
1654 .unindent(),
1655 );
1656
1657 // Replace nested object property in array element
1658 check_object_replace_array(
1659 r#"{
1660 "users": [
1661 {
1662 "name": "alice",
1663 "address": {
1664 "city": "NYC",
1665 "zip": "10001"
1666 }
1667 }
1668 ]
1669 }"#
1670 .unindent(),
1671 &["users", "#0", "address", "city"],
1672 Some(json!("Boston")),
1673 r#"{
1674 "users": [
1675 {
1676 "name": "alice",
1677 "address": {
1678 "city": "Boston",
1679 "zip": "10001"
1680 }
1681 }
1682 ]
1683 }"#
1684 .unindent(),
1685 );
1686
1687 // Add element past end of array
1688 check_object_replace_array(
1689 r#"{
1690 "items": [1, 2]
1691 }"#
1692 .unindent(),
1693 &["items", "#5"],
1694 Some(json!(6)),
1695 r#"{
1696 "items": [1, 2, 6]
1697 }"#
1698 .unindent(),
1699 );
1700
1701 // Complex nested structure
1702 check_object_replace_array(
1703 r#"{
1704 "app": {
1705 "modules": [
1706 {
1707 "name": "auth",
1708 "routes": [
1709 {"path": "/login", "method": "POST"},
1710 {"path": "/logout", "method": "POST"}
1711 ]
1712 },
1713 {
1714 "name": "api",
1715 "routes": [
1716 {"path": "/users", "method": "GET"},
1717 {"path": "/users", "method": "POST"}
1718 ]
1719 }
1720 ]
1721 }
1722 }"#
1723 .unindent(),
1724 &["app", "modules", "#1", "routes", "#0", "method"],
1725 Some(json!("PUT")),
1726 r#"{
1727 "app": {
1728 "modules": [
1729 {
1730 "name": "auth",
1731 "routes": [
1732 {"path": "/login", "method": "POST"},
1733 {"path": "/logout", "method": "POST"}
1734 ]
1735 },
1736 {
1737 "name": "api",
1738 "routes": [
1739 {"path": "/users", "method": "PUT"},
1740 {"path": "/users", "method": "POST"}
1741 ]
1742 }
1743 ]
1744 }
1745 }"#
1746 .unindent(),
1747 );
1748
1749 // Escaped strings in array
1750 check_object_replace_array(
1751 r#"{
1752 "messages": ["hello", "world"]
1753 }"#
1754 .unindent(),
1755 &["messages", "#0"],
1756 Some(json!("hello \"quoted\" world")),
1757 r#"{
1758 "messages": ["hello \"quoted\" world", "world"]
1759 }"#
1760 .unindent(),
1761 );
1762
1763 // Block comments
1764 check_object_replace_array(
1765 r#"{
1766 "data": [
1767 /* first */ 1,
1768 /* second */ 2,
1769 /* third */ 3
1770 ]
1771 }"#
1772 .unindent(),
1773 &["data", "#1"],
1774 Some(json!(20)),
1775 r#"{
1776 "data": [
1777 /* first */ 1,
1778 /* second */ 20,
1779 /* third */ 3
1780 ]
1781 }"#
1782 .unindent(),
1783 );
1784
1785 // Inline array
1786 check_object_replace_array(
1787 r#"{"items": [1, 2, 3], "count": 3}"#.to_string(),
1788 &["items", "#1"],
1789 Some(json!(20)),
1790 r#"{"items": [1, 20, 3], "count": 3}"#.to_string(),
1791 );
1792
1793 // Single element array
1794 check_object_replace_array(
1795 r#"{
1796 "single": [42]
1797 }"#
1798 .unindent(),
1799 &["single", "#0"],
1800 Some(json!(100)),
1801 r#"{
1802 "single": [100]
1803 }"#
1804 .unindent(),
1805 );
1806
1807 // Inconsistent formatting
1808 check_object_replace_array(
1809 r#"{
1810 "messy": [1,
1811 2,
1812 3,
1813 4]
1814 }"#
1815 .unindent(),
1816 &["messy", "#2"],
1817 Some(json!(30)),
1818 r#"{
1819 "messy": [1,
1820 2,
1821 30,
1822 4]
1823 }"#
1824 .unindent(),
1825 );
1826
1827 // Creates array if has numbered key
1828 check_object_replace_array(
1829 r#"{
1830 "array": {"foo": "bar"}
1831 }"#
1832 .unindent(),
1833 &["array", "#3"],
1834 Some(json!(4)),
1835 r#"{
1836 "array": [
1837 4
1838 ]
1839 }"#
1840 .unindent(),
1841 );
1842
1843 // Replace non-array element within array with array
1844 check_object_replace_array(
1845 r#"{
1846 "matrix": [
1847 [1, 2],
1848 [3, 4],
1849 [5, 6]
1850 ]
1851 }"#
1852 .unindent(),
1853 &["matrix", "#1", "#0"],
1854 Some(json!(["foo", "bar"])),
1855 r#"{
1856 "matrix": [
1857 [1, 2],
1858 [[ "foo", "bar" ], 4],
1859 [5, 6]
1860 ]
1861 }"#
1862 .unindent(),
1863 );
1864 // Replace non-array element within array with array
1865 check_object_replace_array(
1866 r#"{
1867 "matrix": [
1868 [1, 2],
1869 [3, 4],
1870 [5, 6]
1871 ]
1872 }"#
1873 .unindent(),
1874 &["matrix", "#1", "#0", "#3"],
1875 Some(json!(["foo", "bar"])),
1876 r#"{
1877 "matrix": [
1878 [1, 2],
1879 [[ [ "foo", "bar" ] ], 4],
1880 [5, 6]
1881 ]
1882 }"#
1883 .unindent(),
1884 );
1885
1886 // Create array in key that doesn't exist
1887 check_object_replace_array(
1888 r#"{
1889 "foo": {}
1890 }"#
1891 .unindent(),
1892 &["foo", "bar", "#0"],
1893 Some(json!({"is_object": true})),
1894 r#"{
1895 "foo": {
1896 "bar": [
1897 {
1898 "is_object": true
1899 }
1900 ]
1901 }
1902 }"#
1903 .unindent(),
1904 );
1905 }
1906
1907 #[test]
1908 fn array_replace() {
1909 #[track_caller]
1910 fn check_array_replace(
1911 input: impl ToString,
1912 index: usize,
1913 key_path: &[&str],
1914 value: Option<Value>,
1915 expected: impl ToString,
1916 ) {
1917 let input = input.to_string();
1918 let result = replace_top_level_array_value_in_json_text(
1919 &input,
1920 key_path,
1921 value.as_ref(),
1922 None,
1923 index,
1924 4,
1925 );
1926 let mut result_str = input;
1927 result_str.replace_range(result.0, &result.1);
1928 pretty_assertions::assert_eq!(expected.to_string(), result_str);
1929 }
1930
1931 check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#);
1932 check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#);
1933 check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1934 check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#);
1935 check_array_replace(
1936 r#"[
1937 1,
1938 2,
1939 3,
1940 ]"#
1941 .unindent(),
1942 1,
1943 &[],
1944 Some(json!({"foo": "bar", "baz": "qux"})),
1945 r#"[
1946 1,
1947 {
1948 "foo": "bar",
1949 "baz": "qux"
1950 },
1951 3,
1952 ]"#
1953 .unindent(),
1954 );
1955 check_array_replace(
1956 r#"[1, 3, 3,]"#,
1957 1,
1958 &[],
1959 Some(json!({"foo": "bar", "baz": "qux"})),
1960 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1961 );
1962
1963 check_array_replace(
1964 r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
1965 1,
1966 &["baz"],
1967 Some(json!({"qux": "quz"})),
1968 r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
1969 );
1970
1971 check_array_replace(
1972 r#"[
1973 1,
1974 {
1975 "foo": "bar",
1976 "baz": "qux"
1977 },
1978 3
1979 ]"#,
1980 1,
1981 &["baz"],
1982 Some(json!({"qux": "quz"})),
1983 r#"[
1984 1,
1985 {
1986 "foo": "bar",
1987 "baz": {
1988 "qux": "quz"
1989 }
1990 },
1991 3
1992 ]"#,
1993 );
1994
1995 check_array_replace(
1996 r#"[
1997 1,
1998 {
1999 "foo": "bar",
2000 "baz": {
2001 "qux": "quz"
2002 }
2003 },
2004 3
2005 ]"#,
2006 1,
2007 &["baz"],
2008 Some(json!("qux")),
2009 r#"[
2010 1,
2011 {
2012 "foo": "bar",
2013 "baz": "qux"
2014 },
2015 3
2016 ]"#,
2017 );
2018
2019 check_array_replace(
2020 r#"[
2021 1,
2022 {
2023 "foo": "bar",
2024 // some comment to keep
2025 "baz": {
2026 // some comment to remove
2027 "qux": "quz"
2028 }
2029 // some other comment to keep
2030 },
2031 3
2032 ]"#,
2033 1,
2034 &["baz"],
2035 Some(json!("qux")),
2036 r#"[
2037 1,
2038 {
2039 "foo": "bar",
2040 // some comment to keep
2041 "baz": "qux"
2042 // some other comment to keep
2043 },
2044 3
2045 ]"#,
2046 );
2047
2048 // Test with comments between array elements
2049 check_array_replace(
2050 r#"[
2051 1,
2052 // This is element 2
2053 2,
2054 /* Block comment */ 3,
2055 4 // Trailing comment
2056 ]"#,
2057 2,
2058 &[],
2059 Some(json!("replaced")),
2060 r#"[
2061 1,
2062 // This is element 2
2063 2,
2064 /* Block comment */ "replaced",
2065 4 // Trailing comment
2066 ]"#,
2067 );
2068
2069 // Test empty array with comments
2070 check_array_replace(
2071 r#"[
2072 // Empty array with comment
2073 ]"#
2074 .unindent(),
2075 0,
2076 &[],
2077 Some(json!("first")),
2078 r#"[
2079 // Empty array with comment
2080 "first"
2081 ]"#
2082 .unindent(),
2083 );
2084 check_array_replace(
2085 r#"[]"#.unindent(),
2086 0,
2087 &[],
2088 Some(json!("first")),
2089 r#"["first"]"#.unindent(),
2090 );
2091
2092 // Test array with leading comments
2093 check_array_replace(
2094 r#"[
2095 // Leading comment
2096 // Another leading comment
2097 1,
2098 2
2099 ]"#,
2100 0,
2101 &[],
2102 Some(json!({"new": "object"})),
2103 r#"[
2104 // Leading comment
2105 // Another leading comment
2106 {
2107 "new": "object"
2108 },
2109 2
2110 ]"#,
2111 );
2112
2113 // Test with deep indentation
2114 check_array_replace(
2115 r#"[
2116 1,
2117 2,
2118 3
2119 ]"#,
2120 1,
2121 &[],
2122 Some(json!("deep")),
2123 r#"[
2124 1,
2125 "deep",
2126 3
2127 ]"#,
2128 );
2129
2130 // Test with mixed spacing
2131 check_array_replace(
2132 r#"[1,2, 3, 4]"#,
2133 2,
2134 &[],
2135 Some(json!("spaced")),
2136 r#"[1,2, "spaced", 4]"#,
2137 );
2138
2139 // Test replacing nested array element
2140 check_array_replace(
2141 r#"[
2142 [1, 2, 3],
2143 [4, 5, 6],
2144 [7, 8, 9]
2145 ]"#,
2146 1,
2147 &[],
2148 Some(json!(["a", "b", "c", "d"])),
2149 r#"[
2150 [1, 2, 3],
2151 [
2152 "a",
2153 "b",
2154 "c",
2155 "d"
2156 ],
2157 [7, 8, 9]
2158 ]"#,
2159 );
2160
2161 // Test with multiline block comments
2162 check_array_replace(
2163 r#"[
2164 /*
2165 * This is a
2166 * multiline comment
2167 */
2168 "first",
2169 "second"
2170 ]"#,
2171 0,
2172 &[],
2173 Some(json!("updated")),
2174 r#"[
2175 /*
2176 * This is a
2177 * multiline comment
2178 */
2179 "updated",
2180 "second"
2181 ]"#,
2182 );
2183
2184 // Test replacing with null
2185 check_array_replace(
2186 r#"[true, false, true]"#,
2187 1,
2188 &[],
2189 Some(json!(null)),
2190 r#"[true, null, true]"#,
2191 );
2192
2193 // Test single element array
2194 check_array_replace(
2195 r#"[42]"#,
2196 0,
2197 &[],
2198 Some(json!({"answer": 42})),
2199 r#"[{ "answer": 42 }]"#,
2200 );
2201
2202 // Test array with only comments
2203 check_array_replace(
2204 r#"[
2205 // Comment 1
2206 // Comment 2
2207 // Comment 3
2208 ]"#
2209 .unindent(),
2210 10,
2211 &[],
2212 Some(json!(123)),
2213 r#"[
2214 // Comment 1
2215 // Comment 2
2216 // Comment 3
2217 123
2218 ]"#
2219 .unindent(),
2220 );
2221
2222 check_array_replace(
2223 r#"[
2224 {
2225 "key": "value"
2226 },
2227 {
2228 "key": "value2"
2229 }
2230 ]"#
2231 .unindent(),
2232 0,
2233 &[],
2234 None,
2235 r#"[
2236 {
2237 "key": "value2"
2238 }
2239 ]"#
2240 .unindent(),
2241 );
2242
2243 check_array_replace(
2244 r#"[
2245 {
2246 "key": "value"
2247 },
2248 {
2249 "key": "value2"
2250 },
2251 {
2252 "key": "value3"
2253 },
2254 ]"#
2255 .unindent(),
2256 1,
2257 &[],
2258 None,
2259 r#"[
2260 {
2261 "key": "value"
2262 },
2263 {
2264 "key": "value3"
2265 },
2266 ]"#
2267 .unindent(),
2268 );
2269
2270 check_array_replace(
2271 r#""#,
2272 2,
2273 &[],
2274 Some(json!(42)),
2275 r#"[
2276 42
2277 ]"#
2278 .unindent(),
2279 );
2280
2281 check_array_replace(
2282 r#""#,
2283 2,
2284 &["foo", "bar"],
2285 Some(json!(42)),
2286 r#"[
2287 {
2288 "foo": {
2289 "bar": 42
2290 }
2291 }
2292 ]"#
2293 .unindent(),
2294 );
2295 }
2296
2297 #[test]
2298 fn array_append() {
2299 #[track_caller]
2300 fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
2301 let input = input.to_string();
2302 let result = append_top_level_array_value_in_json_text(&input, &value, 4);
2303 let mut result_str = input;
2304 result_str.replace_range(result.0, &result.1);
2305 pretty_assertions::assert_eq!(expected.to_string(), result_str);
2306 }
2307 check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
2308 check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
2309 check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2310 check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#);
2311 check_array_append(
2312 r#"[
2313 1,
2314 2,
2315 3
2316 ]"#
2317 .unindent(),
2318 json!(4),
2319 r#"[
2320 1,
2321 2,
2322 3,
2323 4
2324 ]"#
2325 .unindent(),
2326 );
2327 check_array_append(
2328 r#"[
2329 1,
2330 2,
2331 3,
2332 ]"#
2333 .unindent(),
2334 json!(4),
2335 r#"[
2336 1,
2337 2,
2338 3,
2339 4
2340 ]"#
2341 .unindent(),
2342 );
2343 check_array_append(
2344 r#"[
2345 1,
2346 2,
2347 3,
2348 ]"#
2349 .unindent(),
2350 json!({"foo": "bar", "baz": "qux"}),
2351 r#"[
2352 1,
2353 2,
2354 3,
2355 {
2356 "foo": "bar",
2357 "baz": "qux"
2358 }
2359 ]"#
2360 .unindent(),
2361 );
2362 check_array_append(
2363 r#"[ 1, 2, 3, ]"#.unindent(),
2364 json!({"foo": "bar", "baz": "qux"}),
2365 r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
2366 );
2367 check_array_append(
2368 r#"[]"#,
2369 json!({"foo": "bar"}),
2370 r#"[
2371 {
2372 "foo": "bar"
2373 }
2374 ]"#
2375 .unindent(),
2376 );
2377
2378 // Test with comments between array elements
2379 check_array_append(
2380 r#"[
2381 1,
2382 // Comment between elements
2383 2,
2384 /* Block comment */ 3
2385 ]"#
2386 .unindent(),
2387 json!(4),
2388 r#"[
2389 1,
2390 // Comment between elements
2391 2,
2392 /* Block comment */ 3,
2393 4
2394 ]"#
2395 .unindent(),
2396 );
2397
2398 // Test with trailing comment on last element
2399 check_array_append(
2400 r#"[
2401 1,
2402 2,
2403 3 // Trailing comment
2404 ]"#
2405 .unindent(),
2406 json!("new"),
2407 r#"[
2408 1,
2409 2,
2410 3 // Trailing comment
2411 ,
2412 "new"
2413 ]"#
2414 .unindent(),
2415 );
2416
2417 // Test empty array with comments
2418 check_array_append(
2419 r#"[
2420 // Empty array with comment
2421 ]"#
2422 .unindent(),
2423 json!("first"),
2424 r#"[
2425 // Empty array with comment
2426 "first"
2427 ]"#
2428 .unindent(),
2429 );
2430
2431 // Test with multiline block comment at end
2432 check_array_append(
2433 r#"[
2434 1,
2435 2
2436 /*
2437 * This is a
2438 * multiline comment
2439 */
2440 ]"#
2441 .unindent(),
2442 json!(3),
2443 r#"[
2444 1,
2445 2
2446 /*
2447 * This is a
2448 * multiline comment
2449 */
2450 ,
2451 3
2452 ]"#
2453 .unindent(),
2454 );
2455
2456 // Test with deep indentation
2457 check_array_append(
2458 r#"[
2459 1,
2460 2,
2461 3
2462 ]"#
2463 .unindent(),
2464 json!("deep"),
2465 r#"[
2466 1,
2467 2,
2468 3,
2469 "deep"
2470 ]"#
2471 .unindent(),
2472 );
2473
2474 // Test with no spacing
2475 check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
2476
2477 // Test appending complex nested structure
2478 check_array_append(
2479 r#"[
2480 {"a": 1},
2481 {"b": 2}
2482 ]"#
2483 .unindent(),
2484 json!({"c": {"nested": [1, 2, 3]}}),
2485 r#"[
2486 {"a": 1},
2487 {"b": 2},
2488 {
2489 "c": {
2490 "nested": [
2491 1,
2492 2,
2493 3
2494 ]
2495 }
2496 }
2497 ]"#
2498 .unindent(),
2499 );
2500
2501 // Test array ending with comment after bracket
2502 check_array_append(
2503 r#"[
2504 1,
2505 2,
2506 3
2507 ] // Comment after array"#
2508 .unindent(),
2509 json!(4),
2510 r#"[
2511 1,
2512 2,
2513 3,
2514 4
2515 ] // Comment after array"#
2516 .unindent(),
2517 );
2518
2519 // Test with inconsistent element formatting
2520 check_array_append(
2521 r#"[1,
2522 2,
2523 3,
2524 ]"#
2525 .unindent(),
2526 json!(4),
2527 r#"[1,
2528 2,
2529 3,
2530 4
2531 ]"#
2532 .unindent(),
2533 );
2534
2535 // Test appending to single-line array with trailing comma
2536 check_array_append(
2537 r#"[1, 2, 3,]"#,
2538 json!({"key": "value"}),
2539 r#"[1, 2, 3, { "key": "value" }]"#,
2540 );
2541
2542 // Test appending null value
2543 check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
2544
2545 // Test appending to array with only comments
2546 check_array_append(
2547 r#"[
2548 // Just comments here
2549 // More comments
2550 ]"#
2551 .unindent(),
2552 json!(42),
2553 r#"[
2554 // Just comments here
2555 // More comments
2556 42
2557 ]"#
2558 .unindent(),
2559 );
2560
2561 check_array_append(
2562 r#""#,
2563 json!(42),
2564 r#"[
2565 42
2566 ]"#
2567 .unindent(),
2568 )
2569 }
2570
2571 #[test]
2572 fn test_infer_json_indent_size() {
2573 let json_2_spaces = r#"{
2574 "key1": "value1",
2575 "nested": {
2576 "key2": "value2",
2577 "array": [
2578 1,
2579 2,
2580 3
2581 ]
2582 }
2583}"#;
2584 assert_eq!(infer_json_indent_size(json_2_spaces), 2);
2585
2586 let json_4_spaces = r#"{
2587 "key1": "value1",
2588 "nested": {
2589 "key2": "value2",
2590 "array": [
2591 1,
2592 2,
2593 3
2594 ]
2595 }
2596}"#;
2597 assert_eq!(infer_json_indent_size(json_4_spaces), 4);
2598
2599 let json_8_spaces = r#"{
2600 "key1": "value1",
2601 "nested": {
2602 "key2": "value2"
2603 }
2604}"#;
2605 assert_eq!(infer_json_indent_size(json_8_spaces), 8);
2606
2607 let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
2608 assert_eq!(infer_json_indent_size(json_single_line), 2);
2609
2610 let json_empty = r#"{}"#;
2611 assert_eq!(infer_json_indent_size(json_empty), 2);
2612
2613 let json_array = r#"[
2614 {
2615 "id": 1,
2616 "name": "first"
2617 },
2618 {
2619 "id": 2,
2620 "name": "second"
2621 }
2622]"#;
2623 assert_eq!(infer_json_indent_size(json_array), 2);
2624
2625 let json_mixed = r#"{
2626 "a": {
2627 "b": {
2628 "c": "value"
2629 }
2630 },
2631 "d": "value2"
2632}"#;
2633 assert_eq!(infer_json_indent_size(json_mixed), 2);
2634 }
2635}