buffa_types/
field_mask_ext.rs1use alloc::string::String;
4
5use crate::google::protobuf::FieldMask;
6
7impl FieldMask {
8 pub fn from_paths(paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
19 Self {
20 paths: paths.into_iter().map(Into::into).collect(),
21 ..Default::default()
22 }
23 }
24
25 pub fn contains(&self, path: &str) -> bool {
30 self.paths.iter().any(|p| p == path)
31 }
32
33 #[inline]
35 pub fn len(&self) -> usize {
36 self.paths.len()
37 }
38
39 #[inline]
41 pub fn is_empty(&self) -> bool {
42 self.paths.is_empty()
43 }
44
45 #[inline]
47 pub fn iter(&self) -> core::slice::Iter<'_, String> {
48 self.paths.iter()
49 }
50}
51
52impl<'a> IntoIterator for &'a FieldMask {
53 type Item = &'a String;
54 type IntoIter = core::slice::Iter<'a, String>;
55
56 fn into_iter(self) -> Self::IntoIter {
57 self.paths.iter()
58 }
59}
60
61impl IntoIterator for FieldMask {
62 type Item = String;
63 type IntoIter = alloc::vec::IntoIter<String>;
64
65 fn into_iter(self) -> Self::IntoIter {
66 self.paths.into_iter()
67 }
68}
69
70#[cfg(feature = "json")]
78use alloc::vec::Vec;
79#[cfg(feature = "json")]
80use buffa::json_helpers::wkt::{camel_to_snake, snake_to_camel};
81
82#[cfg(feature = "json")]
85impl serde::Serialize for FieldMask {
86 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
94 let camel_paths: Vec<String> = self
95 .paths
96 .iter()
97 .map(|p| {
98 let camel = snake_to_camel(p);
99 if camel_to_snake(&camel) != *p {
100 return Err(serde::ser::Error::custom(alloc::format!(
101 "FieldMask path '{p}' cannot round-trip through camelCase conversion"
102 )));
103 }
104 Ok(camel)
105 })
106 .collect::<Result<_, _>>()?;
107 s.serialize_str(&camel_paths.join(","))
108 }
109}
110
111#[cfg(feature = "json")]
112impl<'de> serde::Deserialize<'de> for FieldMask {
113 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
120 let s: String = serde::Deserialize::deserialize(d)?;
121 let paths = if s.is_empty() {
122 Vec::new()
123 } else {
124 s.split(',')
125 .map(|component| {
126 if component.contains('_') {
127 return Err(serde::de::Error::custom(alloc::format!(
128 "FieldMask path '{component}' contains underscore, \
129 which is invalid in JSON (lowerCamelCase) representation"
130 )));
131 }
132 Ok(camel_to_snake(component))
133 })
134 .collect::<Result<_, _>>()?
135 };
136 Ok(Self {
137 paths,
138 ..Default::default()
139 })
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn from_paths_empty() {
149 let mask = FieldMask::from_paths(core::iter::empty::<&str>());
150 assert!(mask.paths.is_empty());
151 assert!(mask.is_empty());
152 assert_eq!(mask.len(), 0);
153 }
154
155 #[test]
156 fn len_and_is_empty() {
157 let mask = FieldMask::from_paths(["a", "b", "c"]);
158 assert_eq!(mask.len(), 3);
159 assert!(!mask.is_empty());
160 }
161
162 #[test]
163 fn iter_yields_all_paths() {
164 let mask = FieldMask::from_paths(["x.y", "z"]);
165 let collected: Vec<_> = mask.iter().collect();
166 assert_eq!(collected, [&"x.y".to_string(), &"z".to_string()]);
167 }
168
169 #[test]
170 fn from_paths_string_slices() {
171 let mask = FieldMask::from_paths(["a.b", "c.d"]);
172 assert_eq!(mask.paths, vec!["a.b", "c.d"]);
173 }
174
175 #[test]
176 fn from_paths_owned_strings() {
177 let paths = vec!["x".to_string(), "y.z".to_string()];
178 let mask = FieldMask::from_paths(paths);
179 assert_eq!(mask.paths, vec!["x", "y.z"]);
180 }
181
182 #[test]
183 fn contains_returns_true_for_present_path() {
184 let mask = FieldMask::from_paths(["user.name", "user.email"]);
185 assert!(mask.contains("user.name"));
186 assert!(mask.contains("user.email"));
187 }
188
189 #[test]
190 fn contains_returns_false_for_absent_path() {
191 let mask = FieldMask::from_paths(["user.name"]);
192 assert!(!mask.contains("user.age"));
193 }
194
195 #[test]
196 fn contains_is_exact_match_not_prefix() {
197 let mask = FieldMask::from_paths(["user"]);
198 assert!(!mask.contains("user.name"));
199 }
200
201 #[test]
202 fn contains_is_case_sensitive() {
203 let mask = FieldMask::from_paths(["user.Name"]);
204 assert!(!mask.contains("user.name"));
205 }
206
207 #[cfg(feature = "json")]
208 mod serde_tests {
209 use super::*;
210
211 #[test]
214 fn snake_to_camel_simple() {
215 assert_eq!(snake_to_camel("foo_bar"), "fooBar");
216 assert_eq!(snake_to_camel("foo"), "foo");
217 assert_eq!(snake_to_camel("foo_bar_baz"), "fooBarBaz");
218 }
219
220 #[test]
221 fn snake_to_camel_dotted() {
222 assert_eq!(snake_to_camel("user.first_name"), "user.firstName");
223 }
224
225 #[test]
226 fn camel_to_snake_simple() {
227 assert_eq!(camel_to_snake("fooBar"), "foo_bar");
228 assert_eq!(camel_to_snake("foo"), "foo");
229 assert_eq!(camel_to_snake("fooBarBaz"), "foo_bar_baz");
230 }
231
232 #[test]
233 fn camel_to_snake_pascal_case_no_leading_underscore() {
234 assert_eq!(camel_to_snake("FooBar"), "foo_bar");
238 assert_eq!(camel_to_snake("Foo"), "foo");
239 assert_eq!(camel_to_snake("A.B"), "a.b");
240 }
241
242 #[test]
243 fn camel_to_snake_dotted() {
244 assert_eq!(camel_to_snake("user.firstName"), "user.first_name");
245 }
246
247 #[test]
248 fn snake_to_camel_camel_to_snake_roundtrip() {
249 let original = "user.first_name";
250 assert_eq!(camel_to_snake(&snake_to_camel(original)), original);
251 }
252
253 #[test]
256 fn field_mask_empty_roundtrip() {
257 let m = FieldMask::from_paths(core::iter::empty::<&str>());
258 let json = serde_json::to_string(&m).unwrap();
259 assert_eq!(json, r#""""#);
260 let back: FieldMask = serde_json::from_str(&json).unwrap();
261 assert!(back.paths.is_empty());
262 }
263
264 #[test]
265 fn field_mask_single_path_roundtrip() {
266 let m = FieldMask::from_paths(["foo_bar"]);
267 let json = serde_json::to_string(&m).unwrap();
268 assert_eq!(json, r#""fooBar""#);
269 let back: FieldMask = serde_json::from_str(&json).unwrap();
270 assert_eq!(back.paths, ["foo_bar"]);
271 }
272
273 #[test]
274 fn field_mask_multiple_paths_roundtrip() {
275 let m = FieldMask::from_paths(["user_id", "display_name"]);
276 let json = serde_json::to_string(&m).unwrap();
277 assert_eq!(json, r#""userId,displayName""#);
278 let back: FieldMask = serde_json::from_str(&json).unwrap();
279 assert_eq!(back.paths, ["user_id", "display_name"]);
280 }
281
282 #[test]
283 fn field_mask_dotted_path_roundtrip() {
284 let m = FieldMask::from_paths(["user.email_address"]);
285 let json = serde_json::to_string(&m).unwrap();
286 assert_eq!(json, r#""user.emailAddress""#);
287 let back: FieldMask = serde_json::from_str(&json).unwrap();
288 assert_eq!(back.paths, ["user.email_address"]);
289 }
290
291 #[test]
294 fn serialize_rejects_already_camel_case_path() {
295 let m = FieldMask::from_paths(["fooBar"]);
296 assert!(serde_json::to_string(&m).is_err());
297 }
298
299 #[test]
300 fn serialize_rejects_digit_after_underscore() {
301 let m = FieldMask::from_paths(["foo_3_bar"]);
302 assert!(serde_json::to_string(&m).is_err());
303 }
304
305 #[test]
306 fn serialize_rejects_consecutive_underscores() {
307 let m = FieldMask::from_paths(["foo__bar"]);
308 assert!(serde_json::to_string(&m).is_err());
309 }
310
311 #[test]
314 fn deserialize_rejects_underscore_in_json() {
315 let result: Result<FieldMask, _> = serde_json::from_str(r#""foo_bar""#);
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn deserialize_rejects_underscore_in_multi_path() {
321 let result: Result<FieldMask, _> = serde_json::from_str(r#""fooBar,baz_qux""#);
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn serialize_accepts_path_with_digit_not_after_underscore() {
327 let m = FieldMask::from_paths(["foo3_bar"]);
328 let json = serde_json::to_string(&m).unwrap();
329 assert_eq!(json, r#""foo3Bar""#);
330 let back: FieldMask = serde_json::from_str(&json).unwrap();
331 assert_eq!(back.paths, ["foo3_bar"]);
332 }
333
334 #[test]
335 fn serialize_rejects_trailing_underscore() {
336 let m = FieldMask::from_paths(["foo_"]);
337 assert!(serde_json::to_string(&m).is_err());
338 }
339 }
340}