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_option<S: Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
24 match path {
25 Some(path) => s.serialize_some(&path.to_string_lossy().replace('\\', "/")),
26 None => s.serialize_none(),
27 }
28}
29
30pub fn serialize_vec<S: Serializer>(paths: &[PathBuf], s: S) -> Result<S::Ok, S::Error> {
36 use serde::ser::SerializeSeq;
37 let mut seq = s.serialize_seq(Some(paths.len()))?;
38 for p in paths {
39 seq.serialize_element(&p.to_string_lossy().replace('\\', "/"))?;
40 }
41 seq.end()
42}
43
44#[cfg(test)]
45mod tests {
46 mod proptests {
52 use proptest::prelude::*;
53 use serde::Serialize;
54 use std::path::PathBuf;
55
56 #[derive(Serialize)]
58 struct ScalarPath {
59 #[serde(serialize_with = "crate::serde_path::serialize")]
60 path: PathBuf,
61 }
62
63 #[derive(Serialize)]
65 struct OptionalPath {
66 #[serde(serialize_with = "crate::serde_path::serialize_option")]
67 path: Option<PathBuf>,
68 }
69
70 #[derive(Serialize)]
72 struct PathList {
73 #[serde(serialize_with = "crate::serde_path::serialize_vec")]
74 paths: Vec<PathBuf>,
75 }
76
77 fn path_like() -> impl Strategy<Value = String> {
81 prop::collection::vec(
82 prop::sample::select(vec!['a', 'b', '1', '/', '\\', '.', '-', '_', ' ']),
83 0..40,
84 )
85 .prop_map(|chars| chars.into_iter().collect())
86 }
87
88 fn scalar_json(path: &str) -> String {
90 let value = serde_json::to_value(ScalarPath {
91 path: PathBuf::from(path),
92 })
93 .expect("scalar wrapper serializes");
94 value["path"].as_str().expect("path is a string").to_owned()
95 }
96
97 fn option_json(path: Option<&str>) -> serde_json::Value {
99 serde_json::to_value(OptionalPath {
100 path: path.map(PathBuf::from),
101 })
102 .expect("option wrapper serializes")
103 }
104
105 proptest! {
106 #[test]
109 fn serialize_emits_only_forward_slashes(path in path_like()) {
110 let out = scalar_json(&path);
111 prop_assert!(!out.contains('\\'), "output {out:?} still contains a backslash");
112 prop_assert_eq!(out, path.replace('\\', "/"));
113 }
114
115 #[test]
119 fn serialize_then_read_back_is_normalized(path in path_like()) {
120 let json = serde_json::to_string(&ScalarPath { path: PathBuf::from(&path) })
121 .expect("scalar wrapper serializes");
122 let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid json");
123 let restored = parsed["path"].as_str().expect("path is a string");
124 prop_assert_eq!(restored, path.replace('\\', "/"));
125 }
126
127 #[test]
130 fn serialize_is_idempotent(path in path_like()) {
131 let once = scalar_json(&path);
132 let twice = scalar_json(&once);
133 prop_assert_eq!(once, twice);
134 }
135
136 #[test]
138 fn serialize_option_normalizes_some(path in path_like()) {
139 let value = option_json(Some(&path));
140 let out = value["path"].as_str().expect("path is a string");
141 prop_assert!(!out.contains('\\'), "output {out:?} still contains a backslash");
142 prop_assert_eq!(out, path.replace('\\', "/"));
143 }
144
145 #[test]
147 fn serialize_option_none_is_null(_path in path_like()) {
148 let value = option_json(None);
149 prop_assert!(value["path"].is_null());
150 }
151
152 #[test]
155 fn serialize_vec_matches_scalar(paths in prop::collection::vec(path_like(), 0..8)) {
156 let value = serde_json::to_value(PathList {
157 paths: paths.iter().map(PathBuf::from).collect(),
158 })
159 .expect("vec wrapper serializes");
160 let array = value["paths"].as_array().expect("paths is an array");
161 prop_assert_eq!(array.len(), paths.len());
162 for (element, original) in array.iter().zip(&paths) {
163 let serialized = element.as_str().expect("element is a string");
164 prop_assert_eq!(serialized.to_owned(), scalar_json(original));
165 }
166 }
167 }
168 }
169}