fallow_types/
serde_path.rs1use std::path::{Path, PathBuf};
6
7use serde::Serializer;
8
9pub fn serialize<S: Serializer>(path: &Path, s: S) -> Result<S::Ok, S::Error> {
15 s.serialize_str(&path.to_string_lossy().replace('\\', "/"))
16}
17
18pub 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 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]
88 fn backslash_replacement_in_string() {
89 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
106 mod proptests {
112 use proptest::prelude::*;
113 use serde::Serialize;
114 use std::path::PathBuf;
115
116 #[derive(Serialize)]
118 struct ScalarPath {
119 #[serde(serialize_with = "crate::serde_path::serialize")]
120 path: PathBuf,
121 }
122
123 #[derive(Serialize)]
125 struct PathList {
126 #[serde(serialize_with = "crate::serde_path::serialize_vec")]
127 paths: Vec<PathBuf>,
128 }
129
130 fn path_like() -> impl Strategy<Value = String> {
134 prop::collection::vec(
135 prop::sample::select(vec!['a', 'b', '1', '/', '\\', '.', '-', '_', ' ']),
136 0..40,
137 )
138 .prop_map(|chars| chars.into_iter().collect())
139 }
140
141 fn scalar_json(path: &str) -> String {
143 let value = serde_json::to_value(ScalarPath {
144 path: PathBuf::from(path),
145 })
146 .expect("scalar wrapper serializes");
147 value["path"].as_str().expect("path is a string").to_owned()
148 }
149
150 proptest! {
151 #[test]
154 fn serialize_emits_only_forward_slashes(path in path_like()) {
155 let out = scalar_json(&path);
156 prop_assert!(!out.contains('\\'), "output {out:?} still contains a backslash");
157 prop_assert_eq!(out, path.replace('\\', "/"));
158 }
159
160 #[test]
164 fn serialize_then_read_back_is_normalized(path in path_like()) {
165 let json = serde_json::to_string(&ScalarPath { path: PathBuf::from(&path) })
166 .expect("scalar wrapper serializes");
167 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
168 let restored = parsed["path"].as_str().expect("path is a string");
169 prop_assert_eq!(restored, path.replace('\\', "/"));
170 }
171
172 #[test]
175 fn serialize_is_idempotent(path in path_like()) {
176 let once = scalar_json(&path);
177 let twice = scalar_json(&once);
178 prop_assert_eq!(once, twice);
179 }
180
181 #[test]
184 fn serialize_vec_matches_scalar(paths in prop::collection::vec(path_like(), 0..8)) {
185 let value = serde_json::to_value(PathList {
186 paths: paths.iter().map(PathBuf::from).collect(),
187 })
188 .expect("vec wrapper serializes");
189 let array = value["paths"].as_array().expect("paths is an array");
190 prop_assert_eq!(array.len(), paths.len());
191 for (element, original) in array.iter().zip(&paths) {
192 let serialized = element.as_str().expect("element is a string");
193 prop_assert_eq!(serialized.to_owned(), scalar_json(original));
194 }
195 }
196 }
197 }
198}