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