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