1#![forbid(unsafe_code)]
24#![doc(html_root_url = "https://docs.rs/sort-keys/0.1.0")]
25
26use core::cmp::Ordering;
27use serde_json::{Map, Value};
28
29#[cfg(doctest)]
31#[doc = include_str!("../README.md")]
32struct ReadmeDoctests;
33
34#[derive(Debug, Clone, Default)]
36pub struct Options {
37 deep: bool,
38 ignore_keys: Vec<String>,
39}
40
41impl Options {
42 #[must_use]
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 #[must_use]
50 pub fn deep(mut self, value: bool) -> Self {
51 self.deep = value;
52 self
53 }
54
55 #[must_use]
58 pub fn ignore_keys<I, S>(mut self, keys: I) -> Self
59 where
60 I: IntoIterator<Item = S>,
61 S: Into<String>,
62 {
63 self.ignore_keys = keys.into_iter().map(Into::into).collect();
64 self
65 }
66}
67
68#[must_use]
70pub fn compare_utf16(a: &str, b: &str) -> Ordering {
71 a.encode_utf16().cmp(b.encode_utf16())
72}
73
74#[must_use]
82pub fn sort_keys(value: &Value) -> Value {
83 sort_keys_with(value, &Options::new())
84}
85
86#[must_use]
88pub fn sort_keys_with(value: &Value, options: &Options) -> Value {
89 sort_keys_by(value, options, compare_utf16)
90}
91
92#[must_use]
102pub fn sort_keys_by<F>(value: &Value, options: &Options, compare: F) -> Value
103where
104 F: Fn(&str, &str) -> Ordering,
105{
106 sort_value(value, options, &compare)
107}
108
109fn sort_value<F>(value: &Value, options: &Options, compare: &F) -> Value
110where
111 F: Fn(&str, &str) -> Ordering,
112{
113 match value {
114 Value::Object(map) => Value::Object(sort_object(map, options, compare)),
115 Value::Array(array) => Value::Array(sort_array(array, options, compare)),
116 other => other.clone(),
117 }
118}
119
120fn sort_object<F>(map: &Map<String, Value>, options: &Options, compare: &F) -> Map<String, Value>
121where
122 F: Fn(&str, &str) -> Ordering,
123{
124 let mut ignored: Vec<&String> = Vec::new();
125 let mut to_sort: Vec<&String> = Vec::new();
126 for key in map.keys() {
127 if options
128 .ignore_keys
129 .iter()
130 .any(|ignored_key| ignored_key == key)
131 {
132 ignored.push(key);
133 } else {
134 to_sort.push(key);
135 }
136 }
137 to_sort.sort_by(|a, b| compare(a, b));
138
139 let mut result = Map::new();
140 for key in ignored.into_iter().chain(to_sort) {
141 let value = &map[key];
142 let new_value = if options.deep {
143 recurse_value(value, options, compare)
144 } else {
145 value.clone()
146 };
147 result.insert(key.clone(), new_value);
148 }
149 result
150}
151
152fn sort_array<F>(array: &[Value], options: &Options, compare: &F) -> Vec<Value>
153where
154 F: Fn(&str, &str) -> Ordering,
155{
156 array
157 .iter()
158 .map(|item| {
159 if options.deep {
160 recurse_value(item, options, compare)
161 } else {
162 item.clone()
163 }
164 })
165 .collect()
166}
167
168fn recurse_value<F>(value: &Value, options: &Options, compare: &F) -> Value
171where
172 F: Fn(&str, &str) -> Ordering,
173{
174 match value {
175 Value::Object(map) => Value::Object(sort_object(map, options, compare)),
176 Value::Array(array) => Value::Array(sort_array(array, options, compare)),
177 other => other.clone(),
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use serde_json::json;
185
186 #[test]
187 fn shallow() {
188 assert_eq!(
189 sort_keys(&json!({ "c": 0, "a": 0, "b": 0 })),
190 json!({ "a": 0, "b": 0, "c": 0 })
191 );
192 assert_eq!(
194 sort_keys(&json!({ "b": { "d": 0, "c": 0 }, "a": 0 })),
195 json!({ "a": 0, "b": { "d": 0, "c": 0 } })
196 );
197 }
198
199 #[test]
200 fn deep() {
201 assert_eq!(
202 sort_keys_with(
203 &json!({ "b": { "d": 0, "c": 0 }, "a": 0 }),
204 &Options::new().deep(true)
205 ),
206 json!({ "a": 0, "b": { "c": 0, "d": 0 } })
207 );
208 assert_eq!(
209 sort_keys_with(
210 &json!({ "b": [{ "d": 0, "c": 0 }], "a": 0 }),
211 &Options::new().deep(true)
212 ),
213 json!({ "a": 0, "b": [{ "c": 0, "d": 0 }] })
214 );
215 assert_eq!(
217 sort_keys_with(&json!([{ "c": 0, "a": 0 }]), &Options::new().deep(true)),
218 json!([{ "a": 0, "c": 0 }])
219 );
220 assert_eq!(
221 sort_keys(&json!([{ "c": 0, "a": 0 }])),
222 json!([{ "c": 0, "a": 0 }])
223 );
224 }
225
226 #[test]
227 fn custom_compare() {
228 let reversed = sort_keys_by(
229 &json!({ "a": 0, "c": 0, "b": 0 }),
230 &Options::new(),
231 |a, b| b.cmp(a),
232 );
233 assert_eq!(reversed, json!({ "c": 0, "b": 0, "a": 0 }));
234 }
235
236 #[test]
237 fn ignore_keys() {
238 let result = sort_keys_with(
240 &json!({ "z": 0, "b": 0, "a": 0 }),
241 &Options::new().ignore_keys(["z"]),
242 );
243 assert_eq!(result, json!({ "z": 0, "a": 0, "b": 0 }));
244 }
245
246 #[test]
247 fn scalars_passthrough() {
248 assert_eq!(sort_keys(&json!(42)), json!(42));
249 assert_eq!(sort_keys(&json!("s")), json!("s"));
250 }
251}