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