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