Skip to main content

fallow_types/
serde_path.rs

1//! Custom serde serializers for `PathBuf` and `Vec<PathBuf>` that always
2//! output forward slashes, regardless of platform. This ensures consistent
3//! JSON/SARIF output on Windows.
4
5use std::path::{Path, PathBuf};
6
7use serde::Serializer;
8
9/// Serialize a `Path` with forward slashes for cross-platform consistency.
10///
11/// # Errors
12///
13/// Returns any serializer error produced while writing the normalized path string.
14pub fn serialize<S: Serializer>(path: &Path, s: S) -> Result<S::Ok, S::Error> {
15    s.serialize_str(&path.to_string_lossy().replace('\\', "/"))
16}
17
18/// Serialize a `Vec<PathBuf>` with forward slashes for cross-platform consistency.
19///
20/// # Errors
21///
22/// Returns any serializer error produced while writing the normalized path list.
23pub fn serialize_vec<S: Serializer>(paths: &[PathBuf], s: S) -> Result<S::Ok, S::Error> {
24    use serde::ser::SerializeSeq;
25    let mut seq = s.serialize_seq(Some(paths.len()))?;
26    for p in paths {
27        seq.serialize_element(&p.to_string_lossy().replace('\\', "/"))?;
28    }
29    seq.end()
30}
31
32#[cfg(test)]
33mod tests {
34    use std::path::Path;
35
36    /// The core logic of `serialize` is `path.to_string_lossy().replace('\\', "/")`.
37    /// Test that transformation directly since `serde_json` is not a dependency of this crate.
38    fn normalize(path: &Path) -> String {
39        path.to_string_lossy().replace('\\', "/")
40    }
41
42    #[test]
43    fn unix_path_unchanged() {
44        assert_eq!(
45            normalize(Path::new("src/utils/index.ts")),
46            "src/utils/index.ts"
47        );
48    }
49
50    #[test]
51    fn empty_path() {
52        assert_eq!(normalize(Path::new("")), "");
53    }
54
55    #[test]
56    fn single_component_path() {
57        assert_eq!(normalize(Path::new("file.ts")), "file.ts");
58    }
59
60    #[test]
61    fn deep_nested_path() {
62        assert_eq!(normalize(Path::new("a/b/c/d/e.ts")), "a/b/c/d/e.ts");
63    }
64
65    #[test]
66    fn path_with_spaces() {
67        assert_eq!(
68            normalize(Path::new("my project/src/file.ts")),
69            "my project/src/file.ts"
70        );
71    }
72
73    #[test]
74    fn dot_relative_path() {
75        assert_eq!(normalize(Path::new("./src/file.ts")), "./src/file.ts");
76    }
77
78    #[test]
79    fn parent_relative_path() {
80        assert_eq!(normalize(Path::new("../other/file.ts")), "../other/file.ts");
81    }
82
83    // Test the actual backslash replacement — the core purpose of this module.
84    // On Unix, Path::new doesn't split on backslash, so to_string_lossy() preserves
85    // literal backslashes, and .replace('\\', "/") converts them.
86
87    #[test]
88    fn backslash_replacement_in_string() {
89        // Directly test the replace logic that runs on Windows paths
90        let windows_path = "src\\utils\\index.ts";
91        assert_eq!(windows_path.replace('\\', "/"), "src/utils/index.ts");
92    }
93
94    #[test]
95    fn mixed_separators_normalized() {
96        let mixed = "src/utils\\helpers\\index.ts";
97        assert_eq!(mixed.replace('\\', "/"), "src/utils/helpers/index.ts");
98    }
99
100    #[test]
101    fn backslash_only_path() {
102        let path = "src\\deep\\nested\\file.ts";
103        assert_eq!(path.replace('\\', "/"), "src/deep/nested/file.ts");
104    }
105}