|
1 | 1 | use std::{collections::BTreeMap, ops::Range}; |
2 | 2 |
|
| 3 | +use itertools::Itertools; |
3 | 4 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; |
4 | 5 |
|
5 | 6 | use crate::{document::Document, value::Indent, Error, Path, Value}; |
@@ -376,6 +377,62 @@ impl JsonDocument { |
376 | 377 | self.insert_into_empty(offset, new_key, indent, value) |
377 | 378 | } |
378 | 379 |
|
| 380 | + pub fn sort_object_keys(&mut self, parent_path: &Path) -> Result<bool, Error> { |
| 381 | + let mut keys_by_position |
| 382 | + = self.paths.iter() |
| 383 | + .filter(|(path, _)| path.is_direct_child_of(parent_path)) |
| 384 | + .map(|(path, &offset)| (path.last().unwrap(), offset)) |
| 385 | + .collect_vec(); |
| 386 | + |
| 387 | + if keys_by_position.len() <= 1 { |
| 388 | + return Ok(false); |
| 389 | + } |
| 390 | + |
| 391 | + keys_by_position.sort_by_key(|(_, offset)| *offset); |
| 392 | + |
| 393 | + // Check if already sorted alphabetically |
| 394 | + if keys_by_position.windows(2).all(|w| w[0].0 <= w[1].0) { |
| 395 | + return Ok(false); |
| 396 | + } |
| 397 | + |
| 398 | + // Extract each "key": value as raw bytes |
| 399 | + let mut key_value_pairs: Vec<(&str, Vec<u8>)> = vec![]; |
| 400 | + let mut content_end_offset = 0usize; |
| 401 | + |
| 402 | + for (key_name, offset) in &keys_by_position { |
| 403 | + let mut scanner |
| 404 | + = Scanner::new(&self.input, *offset); |
| 405 | + |
| 406 | + scanner.skip_string()?; |
| 407 | + scanner.skip_whitespace(); |
| 408 | + scanner.skip_char(b':')?; |
| 409 | + scanner.skip_whitespace(); |
| 410 | + scanner.skip_value()?; |
| 411 | + |
| 412 | + key_value_pairs.push((key_name, self.input[*offset..scanner.offset].to_vec())); |
| 413 | + content_end_offset = scanner.offset; |
| 414 | + } |
| 415 | + |
| 416 | + // Detect separator pattern (e.g., ", " or ",\n ") |
| 417 | + let separator |
| 418 | + = self.input[key_value_pairs[0].1.len() + keys_by_position[0].1..keys_by_position[1].1].to_vec(); |
| 419 | + |
| 420 | + // Sort by key name and rebuild content |
| 421 | + key_value_pairs.sort_by_key(|(key_name, _)| *key_name); |
| 422 | + |
| 423 | + let mut sorted_content |
| 424 | + = key_value_pairs[0].1.clone(); |
| 425 | + |
| 426 | + for (_, entry_bytes) in key_value_pairs.iter().skip(1) { |
| 427 | + sorted_content.extend_from_slice(&separator); |
| 428 | + sorted_content.extend_from_slice(entry_bytes); |
| 429 | + } |
| 430 | + |
| 431 | + self.replace_range(keys_by_position[0].1..content_end_offset, &sorted_content)?; |
| 432 | + |
| 433 | + Ok(true) |
| 434 | + } |
| 435 | + |
379 | 436 | /** |
380 | 437 | * Return the indent level at the given offset. Return None if the given |
381 | 438 | * offset is inline (i.e. not at the beginning of a line). |
@@ -870,4 +927,51 @@ mod tests { |
870 | 927 | document.set_path(&Path::from_segments(path.into_iter().map(|s| s.to_string()).collect()), value).unwrap(); |
871 | 928 | assert_eq!(String::from_utf8(document.input).unwrap(), String::from_utf8(expected.to_vec()).unwrap()); |
872 | 929 | } |
| 930 | + |
| 931 | + // ===== sort_object_keys tests ===== |
| 932 | + |
| 933 | + #[rstest] |
| 934 | + // Sort unsorted keys at top level |
| 935 | + #[case(b"{\"zebra\": \"z\", \"apple\": \"a\", \"mango\": \"m\"}", vec![], b"{\"apple\": \"a\", \"mango\": \"m\", \"zebra\": \"z\"}", true)] |
| 936 | + |
| 937 | + // Sort unsorted keys with newlines |
| 938 | + #[case(b"{\n \"zebra\": \"z\",\n \"apple\": \"a\",\n \"mango\": \"m\"\n}", vec![], b"{\n \"apple\": \"a\",\n \"mango\": \"m\",\n \"zebra\": \"z\"\n}", true)] |
| 939 | + |
| 940 | + // Already sorted - no change |
| 941 | + #[case(b"{\"apple\": \"a\", \"mango\": \"m\", \"zebra\": \"z\"}", vec![], b"{\"apple\": \"a\", \"mango\": \"m\", \"zebra\": \"z\"}", false)] |
| 942 | + |
| 943 | + // Already sorted with newlines - no change |
| 944 | + #[case(b"{\n \"apple\": \"a\",\n \"mango\": \"m\",\n \"zebra\": \"z\"\n}", vec![], b"{\n \"apple\": \"a\",\n \"mango\": \"m\",\n \"zebra\": \"z\"\n}", false)] |
| 945 | + |
| 946 | + // Empty object - no change |
| 947 | + #[case(b"{}", vec![], b"{}", false)] |
| 948 | + |
| 949 | + // Single key - no change |
| 950 | + #[case(b"{\"only\": \"key\"}", vec![], b"{\"only\": \"key\"}", false)] |
| 951 | + |
| 952 | + // Sort nested object keys |
| 953 | + #[case(b"{\"deps\": {\"zebra\": \"1.0\", \"apple\": \"2.0\"}}", vec!["deps"], b"{\"deps\": {\"apple\": \"2.0\", \"zebra\": \"1.0\"}}", true)] |
| 954 | + |
| 955 | + // Sort nested object with newlines |
| 956 | + #[case(b"{\n \"deps\": {\n \"zebra\": \"1.0\",\n \"apple\": \"2.0\"\n }\n}", vec!["deps"], b"{\n \"deps\": {\n \"apple\": \"2.0\",\n \"zebra\": \"1.0\"\n }\n}", true)] |
| 957 | + |
| 958 | + // Non-existent path - no change |
| 959 | + #[case(b"{\"foo\": \"bar\"}", vec!["nonexistent"], b"{\"foo\": \"bar\"}", false)] |
| 960 | + |
| 961 | + // Complex values preserved during sort |
| 962 | + #[case(b"{\"z\": {\"nested\": true}, \"a\": [1, 2, 3]}", vec![], b"{\"a\": [1, 2, 3], \"z\": {\"nested\": true}}", true)] |
| 963 | + |
| 964 | + // Scoped package names sort correctly |
| 965 | + #[case(b"{\"deps\": {\"@types/node\": \"1.0\", \"@babel/core\": \"2.0\", \"lodash\": \"3.0\"}}", vec!["deps"], b"{\"deps\": {\"@babel/core\": \"2.0\", \"@types/node\": \"1.0\", \"lodash\": \"3.0\"}}", true)] |
| 966 | + |
| 967 | + fn test_sort_object_keys(#[case] document: &[u8], #[case] path: Vec<&str>, #[case] expected: &[u8], #[case] expected_sorted: bool) { |
| 968 | + let mut document |
| 969 | + = JsonDocument::new(document.to_vec()).unwrap(); |
| 970 | + |
| 971 | + let sorted |
| 972 | + = document.sort_object_keys(&Path::from_segments(path.into_iter().map(|s| s.to_string()).collect())).unwrap(); |
| 973 | + |
| 974 | + assert_eq!(sorted, expected_sorted, "sort_object_keys return value mismatch"); |
| 975 | + assert_eq!(String::from_utf8(document.input).unwrap(), String::from_utf8(expected.to_vec()).unwrap()); |
| 976 | + } |
873 | 977 | } |
0 commit comments