fast-json-lint 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ # This file is autogenerated by maturin v1.15.0
2
+ # To update, run
3
+ #
4
+ # maturin generate-ci github --platform linux --platform macos
5
+ #
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - main
12
+ - master
13
+ tags:
14
+ - 'v*'
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ linux:
23
+ runs-on: ${{ matrix.platform.runner }}
24
+ strategy:
25
+ matrix:
26
+ platform:
27
+ - runner: ubuntu-22.04
28
+ target: x86_64
29
+ - runner: ubuntu-22.04
30
+ target: aarch64
31
+ steps:
32
+ - uses: actions/checkout@v6
33
+ - name: Build wheels
34
+ uses: PyO3/maturin-action@v1
35
+ with:
36
+ target: ${{ matrix.platform.target }}
37
+ args: --release --out dist
38
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
39
+ manylinux: auto
40
+ - name: Upload wheels
41
+ uses: actions/upload-artifact@v6
42
+ with:
43
+ name: wheels-linux-${{ matrix.platform.target }}
44
+ path: dist
45
+
46
+ macos:
47
+ runs-on: ${{ matrix.platform.runner }}
48
+ strategy:
49
+ matrix:
50
+ platform:
51
+ - runner: macos-15-intel
52
+ target: x86_64
53
+ - runner: macos-latest
54
+ target: aarch64
55
+ steps:
56
+ - uses: actions/checkout@v6
57
+ - name: Build wheels
58
+ uses: PyO3/maturin-action@v1
59
+ with:
60
+ target: ${{ matrix.platform.target }}
61
+ args: --release --out dist
62
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
63
+ - name: Upload wheels
64
+ uses: actions/upload-artifact@v6
65
+ with:
66
+ name: wheels-macos-${{ matrix.platform.target }}
67
+ path: dist
68
+
69
+ sdist:
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v6
73
+ - name: Build sdist
74
+ uses: PyO3/maturin-action@v1
75
+ with:
76
+ command: sdist
77
+ args: --out dist
78
+ - name: Upload sdist
79
+ uses: actions/upload-artifact@v6
80
+ with:
81
+ name: wheels-sdist
82
+ path: dist
83
+
84
+ release:
85
+ name: Release
86
+ runs-on: ubuntu-latest
87
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
88
+ needs: [linux, macos, sdist]
89
+ permissions:
90
+ # Use to sign the release artifacts
91
+ id-token: write
92
+ # Used to upload release artifacts
93
+ contents: write
94
+ # Used to generate artifact attestation
95
+ attestations: write
96
+ steps:
97
+ - uses: actions/download-artifact@v7
98
+ - name: Generate artifact attestation
99
+ uses: actions/attest@v4
100
+ with:
101
+ subject-path: 'wheels-*/*'
102
+ - name: Install uv
103
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
104
+ uses: astral-sh/setup-uv@v7
105
+ - name: Publish to PyPI
106
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
107
+ # Trusted Publishing (OIDC): configure this repo + release.yml on PyPI, no token needed.
108
+ run: uv publish --trusted-publishing always 'wheels-*/*'
@@ -0,0 +1,2 @@
1
+ /target
2
+ /dist
@@ -0,0 +1,135 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "equivalent"
7
+ version = "1.0.2"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
10
+
11
+ [[package]]
12
+ name = "hashbrown"
13
+ version = "0.17.1"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
16
+
17
+ [[package]]
18
+ name = "indexmap"
19
+ version = "2.14.2"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
22
+ dependencies = [
23
+ "equivalent",
24
+ "hashbrown",
25
+ ]
26
+
27
+ [[package]]
28
+ name = "itoa"
29
+ version = "1.0.18"
30
+ source = "registry+https://github.com/rust-lang/crates.io-index"
31
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
32
+
33
+ [[package]]
34
+ name = "json-lint"
35
+ version = "0.1.0"
36
+ dependencies = [
37
+ "libc",
38
+ "serde_json",
39
+ ]
40
+
41
+ [[package]]
42
+ name = "libc"
43
+ version = "0.2.189"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
46
+
47
+ [[package]]
48
+ name = "memchr"
49
+ version = "2.8.3"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
52
+
53
+ [[package]]
54
+ name = "proc-macro2"
55
+ version = "1.0.107"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
58
+ dependencies = [
59
+ "unicode-ident",
60
+ ]
61
+
62
+ [[package]]
63
+ name = "quote"
64
+ version = "1.0.47"
65
+ source = "registry+https://github.com/rust-lang/crates.io-index"
66
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
67
+ dependencies = [
68
+ "proc-macro2",
69
+ ]
70
+
71
+ [[package]]
72
+ name = "serde"
73
+ version = "1.0.229"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
76
+ dependencies = [
77
+ "serde_core",
78
+ ]
79
+
80
+ [[package]]
81
+ name = "serde_core"
82
+ version = "1.0.229"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
85
+ dependencies = [
86
+ "serde_derive",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "serde_derive"
91
+ version = "1.0.229"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
94
+ dependencies = [
95
+ "proc-macro2",
96
+ "quote",
97
+ "syn",
98
+ ]
99
+
100
+ [[package]]
101
+ name = "serde_json"
102
+ version = "1.0.151"
103
+ source = "registry+https://github.com/rust-lang/crates.io-index"
104
+ checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
105
+ dependencies = [
106
+ "indexmap",
107
+ "itoa",
108
+ "memchr",
109
+ "serde",
110
+ "serde_core",
111
+ "zmij",
112
+ ]
113
+
114
+ [[package]]
115
+ name = "syn"
116
+ version = "3.0.6"
117
+ source = "registry+https://github.com/rust-lang/crates.io-index"
118
+ checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
119
+ dependencies = [
120
+ "proc-macro2",
121
+ "quote",
122
+ "unicode-ident",
123
+ ]
124
+
125
+ [[package]]
126
+ name = "unicode-ident"
127
+ version = "1.0.26"
128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
129
+ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
130
+
131
+ [[package]]
132
+ name = "zmij"
133
+ version = "1.0.23"
134
+ source = "registry+https://github.com/rust-lang/crates.io-index"
135
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "json-lint"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ readme = "README.md"
6
+
7
+ [dependencies]
8
+ libc = "0.2.189"
9
+ serde_json = { version = "1.0.151", features = ["preserve_order", "arbitrary_precision"] }
10
+
11
+ [profile.release]
12
+ lto = "fat"
13
+ codegen-units = 1
14
+ panic = "abort"
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: fast-json-lint
3
+ Version: 0.1.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Operating System :: POSIX :: Linux
6
+ Classifier: Operating System :: MacOS
7
+ Classifier: Environment :: Console
8
+ Summary: Fast JSON linter and fixer: natural key sorting and 2-space formatting
9
+ Keywords: json,lint,formatter
10
+ Author-email: Kamil Kobza <kamil1pl@gmail.com>
11
+ License: MIT
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
14
+
15
+ # json-lint
16
+
17
+ A fast Rust JSON linter and fixer, inspired by
18
+ [json-linter](https://github.com/atomicptr/json-linter). It is distributed as a
19
+ prebuilt binary on PyPI, the same way as ruff.
20
+
21
+ ```bash
22
+ pip install fast-json-lint
23
+ json-lint my-file.json some-dir/
24
+ ```
25
+
26
+ For every file (or every `*.json` file in a given directory) it:
27
+
28
+ - sorts object keys recursively in natural order (`a2` before `a10`)
29
+ - rewrites the file with 2-space indentation and a trailing newline
30
+ - reports files that are not valid JSON and leaves them unchanged
31
+
32
+ It exits with status 1 if any file fails.
33
+
@@ -0,0 +1,18 @@
1
+ # json-lint
2
+
3
+ A fast Rust JSON linter and fixer, inspired by
4
+ [json-linter](https://github.com/atomicptr/json-linter). It is distributed as a
5
+ prebuilt binary on PyPI, the same way as ruff.
6
+
7
+ ```bash
8
+ pip install fast-json-lint
9
+ json-lint my-file.json some-dir/
10
+ ```
11
+
12
+ For every file (or every `*.json` file in a given directory) it:
13
+
14
+ - sorts object keys recursively in natural order (`a2` before `a10`)
15
+ - rewrites the file with 2-space indentation and a trailing newline
16
+ - reports files that are not valid JSON and leaves them unchanged
17
+
18
+ It exits with status 1 if any file fails.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.5,<2"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "fast-json-lint"
7
+ description = "Fast JSON linter and fixer: natural key sorting and 2-space formatting"
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Kamil Kobza", email = "kamil1pl@gmail.com" }]
12
+ keywords = ["json", "lint", "formatter"]
13
+ classifiers = [
14
+ "Programming Language :: Rust",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Operating System :: MacOS",
17
+ "Environment :: Console",
18
+ ]
19
+ dynamic = ["version"]
20
+
21
+ [tool.maturin]
22
+ bindings = "bin"
23
+ strip = true
@@ -0,0 +1,173 @@
1
+ //! A Rust take on `json-linter PATH... --fix --config-set indent=2`
2
+ //! (json-linter 1.3.0, https://github.com/atomicptr/json-linter): sorts object
3
+ //! keys naturally, rewrites files with 2-space indent, then lints them.
4
+
5
+ mod natural;
6
+
7
+ use std::collections::BTreeMap;
8
+ use std::ffi::OsStr;
9
+ use std::io::{self, Write};
10
+ use std::os::unix::ffi::OsStrExt;
11
+ use std::path::Path;
12
+ use std::{env, fs};
13
+
14
+ use serde_json::Value;
15
+
16
+
17
+ const COLOR_GREEN: &str = "\x1b[92m";
18
+ const COLOR_RED: &str = "\x1b[91m";
19
+ const COLOR_RESET: &str = "\x1b[00m";
20
+
21
+ /// One rule outcome: its name and the failure message, if it failed.
22
+ type RuleResult = (&'static str, Option<String>);
23
+
24
+ fn main() {
25
+ let args: Vec<_> = env::args_os().skip(1).collect();
26
+ if args.is_empty() || args.iter().any(|a| a.as_bytes().starts_with(b"-")) {
27
+ eprintln!("usage: json-lint PATH [PATH ...]");
28
+ eprintln!("Fixes and lints like: json-linter PATH... --fix --config-set indent=2");
29
+ std::process::exit(2);
30
+ }
31
+ let mut out = io::BufWriter::new(io::stdout().lock());
32
+ let code = run(&args, &mut out);
33
+ let _ = out.flush();
34
+ std::process::exit(code);
35
+ }
36
+
37
+ fn run(args: &[std::ffi::OsString], out: &mut impl Write) -> i32 {
38
+ let cwd = normalize(b"", env::current_dir().unwrap().as_os_str().as_bytes());
39
+ let files: Vec<Vec<u8>> = args.iter().flat_map(|a| gather_files(&cwd, a.as_bytes())).collect();
40
+ if files.is_empty() {
41
+ let _ = writeln!(out, "No files found.");
42
+ return 1;
43
+ }
44
+
45
+ let mut per_path: BTreeMap<Vec<u8>, Vec<RuleResult>> = BTreeMap::new();
46
+ for file in &files {
47
+ let results = per_path.entry(display_path(file, &cwd)).or_default();
48
+ results.extend(fix_and_lint(file));
49
+ }
50
+
51
+ print_header(out, "json-linter", None);
52
+ let (mut passed, mut total) = (0, 0);
53
+ for (path, results) in &per_path {
54
+ let _ = out.write_all(path);
55
+ let _ = out.write_all(b" ");
56
+ for (_, err) in results {
57
+ let (color, marker) = if err.is_none() { (COLOR_GREEN, '.') } else { (COLOR_RED, 'F') };
58
+ let _ = write!(out, "{color}{marker}{COLOR_RESET}");
59
+ }
60
+ let _ = writeln!(out);
61
+ for (name, err) in results {
62
+ if let Some(msg) = err {
63
+ let msg = if msg.is_empty() { String::new() } else { format!(": {msg}") };
64
+ let _ = writeln!(out, "\t{COLOR_RED}{name}{msg}{COLOR_RESET}");
65
+ }
66
+ }
67
+ total += results.len();
68
+ passed += results.iter().filter(|(_, e)| e.is_none()).count();
69
+ }
70
+ print_header(out, &format!("{passed} / {total} passed"), Some(COLOR_GREEN));
71
+ if passed == total { 0 } else { 1 }
72
+ }
73
+
74
+ /// Rewrites the file with naturally sorted keys and 2-space indent, then
75
+ /// returns the rule results. Unparseable files are reported and left alone.
76
+ fn fix_and_lint(path: &[u8]) -> Vec<RuleResult> {
77
+ let value = match fix_file(path) {
78
+ Ok(v) => v,
79
+ Err(e) => return vec![("lint_file", Some(e))],
80
+ };
81
+ let sorted = natural::keys_are_sorted(&value);
82
+ vec![
83
+ ("rule_keys_are_sorted", (!sorted).then(String::new)),
84
+ // No naming style is configured, so every key name is accepted.
85
+ ("rule_naming_style_is_correct", None),
86
+ ]
87
+ }
88
+
89
+ fn fix_file(path: &[u8]) -> Result<Value, String> {
90
+ let p = as_path(path);
91
+ let data = fs::read(p).map_err(|e| format!("IOError: {e}"))?;
92
+ let mut value: Value =
93
+ serde_json::from_slice(&data).map_err(|e| format!("JSONDecodeError: {e}"))?;
94
+ natural::sort_keys(&mut value);
95
+ let mut fixed = serde_json::to_vec_pretty(&value).map_err(|e| e.to_string())?;
96
+ fixed.push(b'\n');
97
+ fs::write(p, fixed).map_err(|e| format!("IOError: {e}"))?;
98
+ Ok(value)
99
+ }
100
+
101
+ // ------------------------------------------------------------------- files --
102
+
103
+ /// Joins `path` onto `base` and normalizes it lexically, like `pathlib.Path`.
104
+ fn normalize(base: &[u8], path: &[u8]) -> Vec<u8> {
105
+ let joined = if path.starts_with(b"/") { path.to_vec() } else { [base, b"/", path].concat() };
106
+ let mut out = Vec::with_capacity(joined.len());
107
+ for part in joined.split(|&c| c == b'/').filter(|p| !p.is_empty() && *p != b".") {
108
+ out.push(b'/');
109
+ out.extend_from_slice(part);
110
+ }
111
+ if out.is_empty() {
112
+ out.push(b'/');
113
+ }
114
+ out
115
+ }
116
+
117
+ fn as_path(p: &[u8]) -> &Path {
118
+ Path::new(OsStr::from_bytes(p))
119
+ }
120
+
121
+ /// The file itself, or a directory's `*.json` entries (non-recursive, sorted).
122
+ fn gather_files(cwd: &[u8], arg: &[u8]) -> Vec<Vec<u8>> {
123
+ let path = normalize(cwd, arg);
124
+ let p = as_path(&path);
125
+ if p.is_file() {
126
+ return vec![path];
127
+ }
128
+ let Ok(entries) = fs::read_dir(p) else {
129
+ return vec![];
130
+ };
131
+ let mut files: Vec<_> = entries
132
+ .filter_map(Result::ok)
133
+ .filter(|e| e.path().is_file() && e.file_name().as_bytes().ends_with(b".json"))
134
+ .map(|e| normalize(&path, e.file_name().as_bytes()))
135
+ .collect();
136
+ files.sort();
137
+ files
138
+ }
139
+
140
+ /// The path relative to cwd when it is inside it, otherwise absolute.
141
+ fn display_path(path: &[u8], cwd: &[u8]) -> Vec<u8> {
142
+ if cwd == b"/" {
143
+ return path[1..].to_vec();
144
+ }
145
+ match path.strip_prefix(cwd) {
146
+ Some(rest) if rest.starts_with(b"/") => rest[1..].to_vec(),
147
+ _ => path.to_vec(),
148
+ }
149
+ }
150
+
151
+ // ------------------------------------------------------------------ output --
152
+
153
+ /// The text centered in `=` signs, 80 per side when stdout is not a terminal.
154
+ fn print_header(out: &mut impl Write, text: &str, color: Option<&str>) {
155
+ let width = match terminal_columns() {
156
+ Some(cols) => (cols as f64 / 2.0 - (text.chars().count() as f64 + 2.0) / 2.0).floor(),
157
+ None => 80.0,
158
+ };
159
+ let segment = "=".repeat(width.max(0.0) as usize);
160
+ let _ = match color {
161
+ Some(c) => writeln!(out, "{c}{segment} {text} {segment}{COLOR_RESET}"),
162
+ None => writeln!(out, "{segment} {text} {segment}"),
163
+ };
164
+ }
165
+
166
+ /// Width of the terminal attached to stdout, if any.
167
+ fn terminal_columns() -> Option<u16> {
168
+ // SAFETY: TIOCGWINSZ only writes into the provided winsize struct.
169
+ unsafe {
170
+ let mut ws: libc::winsize = std::mem::zeroed();
171
+ (libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) == 0).then_some(ws.ws_col)
172
+ }
173
+ }
@@ -0,0 +1,100 @@
1
+ //! Natural key ordering ("a2" < "a10") and the recursive key-sorting fixer.
2
+
3
+ use std::cmp::Ordering;
4
+
5
+ use serde_json::{Map, Value};
6
+
7
+ /// Compares strings with ASCII digit runs ordered by numeric value.
8
+ pub fn natural_cmp(a: &str, b: &str) -> Ordering {
9
+ let (mut a, mut b) = (a.as_bytes(), b.as_bytes());
10
+ loop {
11
+ match (a.first(), b.first()) {
12
+ (None, None) => return Ordering::Equal,
13
+ (None, Some(_)) => return Ordering::Less,
14
+ (Some(_), None) => return Ordering::Greater,
15
+ (Some(x), Some(y)) if x.is_ascii_digit() && y.is_ascii_digit() => {
16
+ let (na, ra) = split_digits(a);
17
+ let (nb, rb) = split_digits(b);
18
+ let ord = na.len().cmp(&nb.len()).then_with(|| na.cmp(nb));
19
+ if ord != Ordering::Equal {
20
+ return ord;
21
+ }
22
+ (a, b) = (ra, rb);
23
+ }
24
+ // A digit run ends the text before it, so it sorts before any other char.
25
+ (Some(x), Some(_)) if x.is_ascii_digit() => return Ordering::Less,
26
+ (Some(_), Some(y)) if y.is_ascii_digit() => return Ordering::Greater,
27
+ (Some(x), Some(y)) => {
28
+ if x != y {
29
+ return x.cmp(y);
30
+ }
31
+ (a, b) = (&a[1..], &b[1..]);
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ /// Splits off a leading digit run, returning it without leading zeros.
38
+ fn split_digits(s: &[u8]) -> (&[u8], &[u8]) {
39
+ let end = s.iter().position(|c| !c.is_ascii_digit()).unwrap_or(s.len());
40
+ let zeros = s[..end].iter().take_while(|&&c| c == b'0').count();
41
+ (&s[zeros..end], &s[end..])
42
+ }
43
+
44
+ /// Recursively sorts every object's keys in natural order (stable on ties).
45
+ pub fn sort_keys(v: &mut Value) {
46
+ match v {
47
+ Value::Array(items) => items.iter_mut().for_each(sort_keys),
48
+ Value::Object(map) => {
49
+ let mut entries: Vec<_> = std::mem::take(map).into_iter().collect();
50
+ entries.sort_by(|(a, _), (b, _)| natural_cmp(a, b));
51
+ *map = entries
52
+ .into_iter()
53
+ .map(|(k, mut v)| {
54
+ sort_keys(&mut v);
55
+ (k, v)
56
+ })
57
+ .collect::<Map<_, _>>();
58
+ }
59
+ _ => {}
60
+ }
61
+ }
62
+
63
+ /// The `keys_are_sorted` rule.
64
+ pub fn keys_are_sorted(v: &Value) -> bool {
65
+ match v {
66
+ Value::Array(items) => items.iter().all(keys_are_sorted),
67
+ Value::Object(map) => {
68
+ let keys: Vec<_> = map.keys().collect();
69
+ keys.windows(2).all(|w| natural_cmp(w[0], w[1]) != Ordering::Greater)
70
+ && map.values().all(keys_are_sorted)
71
+ }
72
+ _ => true,
73
+ }
74
+ }
75
+
76
+ #[cfg(test)]
77
+ mod tests {
78
+ use super::*;
79
+ use serde_json::json;
80
+
81
+ #[test]
82
+ fn natural_order() {
83
+ assert_eq!(natural_cmp("a2", "a10"), Ordering::Less);
84
+ assert_eq!(natural_cmp("a01", "a1"), Ordering::Equal);
85
+ assert_eq!(natural_cmp("", "a"), Ordering::Less);
86
+ assert_eq!(natural_cmp("10b", "9b"), Ordering::Greater);
87
+ assert_eq!(natural_cmp("x1", "x1y"), Ordering::Less);
88
+ assert_eq!(natural_cmp("B", "a"), Ordering::Less);
89
+ assert_eq!(natural_cmp("20Z", "-a"), Ordering::Less);
90
+ assert_eq!(natural_cmp("a1", "a "), Ordering::Less);
91
+ }
92
+
93
+ #[test]
94
+ fn sorts_nested() {
95
+ let mut v = json!([{"b10": {"z": 1, "y": 2}, "b9": 0, "a01": 1, "a1": 2}]);
96
+ sort_keys(&mut v);
97
+ assert_eq!(v.to_string(), r#"[{"a01":1,"a1":2,"b9":0,"b10":{"y":2,"z":1}}]"#);
98
+ assert!(keys_are_sorted(&v));
99
+ }
100
+ }