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 FieldMask {
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")]
77use alloc::vec::Vec;
78
79#[cfg(feature = "json")]
80fn snake_to_camel(path: &str) -> String {
81 path.split('.')
82 .map(|component| {
83 let mut out = String::with_capacity(component.len());
84 let mut capitalize_next = false;
85 for ch in component.chars() {
86 if ch == '_' {
87 capitalize_next = true;
88 } else if capitalize_next {
89 out.extend(ch.to_uppercase());
90 capitalize_next = false;
91 } else {
92 out.push(ch);
93 }
94 }
95 out
96 })
97 .collect::<Vec<_>>()
98 .join(".")
99}
100
101#[cfg(feature = "json")]
103fn camel_to_snake(path: &str) -> String {
104 path.split('.')
105 .map(|component| {
106 let mut out = String::with_capacity(component.len() + 4);
107 for ch in component.chars() {
108 if ch.is_uppercase() {
109 if !out.is_empty() {
112 out.push('_');
113 }
114 out.extend(ch.to_lowercase());
115 } else {
116 out.push(ch);
117 }
118 }
119 out
120 })
121 .collect::<Vec<_>>()
122 .join(".")
123}
124
125#[cfg(feature = "json")]
128impl serde::Serialize for FieldMask {
129 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
137 let camel_paths: Vec<String> = self
138 .paths
139 .iter()
140 .map(|p| {
141 let camel = snake_to_camel(p);
142 if camel_to_snake(&camel) != *p {
143 return Err(serde::ser::Error::custom(alloc::format!(
144 "FieldMask path '{p}' cannot round-trip through camelCase conversion"
145 )));
146 }
147 Ok(camel)
148 })
149 .collect::<Result<_, _>>()?;
150 s.serialize_str(&camel_paths.join(","))
151 }
152}
153
154#[cfg(feature = "json")]
155impl<'de> serde::Deserialize<'de> for FieldMask {
156 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
163 let s: String = serde::Deserialize::deserialize(d)?;
164 let paths = if s.is_empty() {
165 Vec::new()
166 } else {
167 s.split(',')
168 .map(|component| {
169 if component.contains('_') {
170 return Err(serde::de::Error::custom(alloc::format!(
171 "FieldMask path '{component}' contains underscore, \
172 which is invalid in JSON (lowerCamelCase) representation"
173 )));
174 }
175 Ok(camel_to_snake(component))
176 })
177 .collect::<Result<_, _>>()?
178 };
179 Ok(FieldMask {
180 paths,
181 ..Default::default()
182 })
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn from_paths_empty() {
192 let mask = FieldMask::from_paths(core::iter::empty::<&str>());
193 assert!(mask.paths.is_empty());
194 assert!(mask.is_empty());
195 assert_eq!(mask.len(), 0);
196 }
197
198 #[test]
199 fn len_and_is_empty() {
200 let mask = FieldMask::from_paths(["a", "b", "c"]);
201 assert_eq!(mask.len(), 3);
202 assert!(!mask.is_empty());
203 }
204
205 #[test]
206 fn iter_yields_all_paths() {
207 let mask = FieldMask::from_paths(["x.y", "z"]);
208 let collected: Vec<_> = mask.iter().collect();
209 assert_eq!(collected, [&"x.y".to_string(), &"z".to_string()]);
210 }
211
212 #[test]
213 fn from_paths_string_slices() {
214 let mask = FieldMask::from_paths(["a.b", "c.d"]);
215 assert_eq!(mask.paths, vec!["a.b", "c.d"]);
216 }
217
218 #[test]
219 fn from_paths_owned_strings() {
220 let paths = vec!["x".to_string(), "y.z".to_string()];
221 let mask = FieldMask::from_paths(paths);
222 assert_eq!(mask.paths, vec!["x", "y.z"]);
223 }
224
225 #[test]
226 fn contains_returns_true_for_present_path() {
227 let mask = FieldMask::from_paths(["user.name", "user.email"]);
228 assert!(mask.contains("user.name"));
229 assert!(mask.contains("user.email"));
230 }
231
232 #[test]
233 fn contains_returns_false_for_absent_path() {
234 let mask = FieldMask::from_paths(["user.name"]);
235 assert!(!mask.contains("user.age"));
236 }
237
238 #[test]
239 fn contains_is_exact_match_not_prefix() {
240 let mask = FieldMask::from_paths(["user"]);
241 assert!(!mask.contains("user.name"));
242 }
243
244 #[test]
245 fn contains_is_case_sensitive() {
246 let mask = FieldMask::from_paths(["user.Name"]);
247 assert!(!mask.contains("user.name"));
248 }
249
250 #[cfg(feature = "json")]
251 mod serde_tests {
252 use super::*;
253
254 #[test]
257 fn snake_to_camel_simple() {
258 assert_eq!(snake_to_camel("foo_bar"), "fooBar");
259 assert_eq!(snake_to_camel("foo"), "foo");
260 assert_eq!(snake_to_camel("foo_bar_baz"), "fooBarBaz");
261 }
262
263 #[test]
264 fn snake_to_camel_dotted() {
265 assert_eq!(snake_to_camel("user.first_name"), "user.firstName");
266 }
267
268 #[test]
269 fn camel_to_snake_simple() {
270 assert_eq!(camel_to_snake("fooBar"), "foo_bar");
271 assert_eq!(camel_to_snake("foo"), "foo");
272 assert_eq!(camel_to_snake("fooBarBaz"), "foo_bar_baz");
273 }
274
275 #[test]
276 fn camel_to_snake_pascal_case_no_leading_underscore() {
277 assert_eq!(camel_to_snake("FooBar"), "foo_bar");
281 assert_eq!(camel_to_snake("Foo"), "foo");
282 assert_eq!(camel_to_snake("A.B"), "a.b");
283 }
284
285 #[test]
286 fn camel_to_snake_dotted() {
287 assert_eq!(camel_to_snake("user.firstName"), "user.first_name");
288 }
289
290 #[test]
291 fn snake_to_camel_camel_to_snake_roundtrip() {
292 let original = "user.first_name";
293 assert_eq!(camel_to_snake(&snake_to_camel(original)), original);
294 }
295
296 #[test]
299 fn field_mask_empty_roundtrip() {
300 let m = FieldMask::from_paths(core::iter::empty::<&str>());
301 let json = serde_json::to_string(&m).unwrap();
302 assert_eq!(json, r#""""#);
303 let back: FieldMask = serde_json::from_str(&json).unwrap();
304 assert!(back.paths.is_empty());
305 }
306
307 #[test]
308 fn field_mask_single_path_roundtrip() {
309 let m = FieldMask::from_paths(["foo_bar"]);
310 let json = serde_json::to_string(&m).unwrap();
311 assert_eq!(json, r#""fooBar""#);
312 let back: FieldMask = serde_json::from_str(&json).unwrap();
313 assert_eq!(back.paths, ["foo_bar"]);
314 }
315
316 #[test]
317 fn field_mask_multiple_paths_roundtrip() {
318 let m = FieldMask::from_paths(["user_id", "display_name"]);
319 let json = serde_json::to_string(&m).unwrap();
320 assert_eq!(json, r#""userId,displayName""#);
321 let back: FieldMask = serde_json::from_str(&json).unwrap();
322 assert_eq!(back.paths, ["user_id", "display_name"]);
323 }
324
325 #[test]
326 fn field_mask_dotted_path_roundtrip() {
327 let m = FieldMask::from_paths(["user.email_address"]);
328 let json = serde_json::to_string(&m).unwrap();
329 assert_eq!(json, r#""user.emailAddress""#);
330 let back: FieldMask = serde_json::from_str(&json).unwrap();
331 assert_eq!(back.paths, ["user.email_address"]);
332 }
333
334 #[test]
337 fn serialize_rejects_already_camel_case_path() {
338 let m = FieldMask::from_paths(["fooBar"]);
339 assert!(serde_json::to_string(&m).is_err());
340 }
341
342 #[test]
343 fn serialize_rejects_digit_after_underscore() {
344 let m = FieldMask::from_paths(["foo_3_bar"]);
345 assert!(serde_json::to_string(&m).is_err());
346 }
347
348 #[test]
349 fn serialize_rejects_consecutive_underscores() {
350 let m = FieldMask::from_paths(["foo__bar"]);
351 assert!(serde_json::to_string(&m).is_err());
352 }
353
354 #[test]
357 fn deserialize_rejects_underscore_in_json() {
358 let result: Result<FieldMask, _> = serde_json::from_str(r#""foo_bar""#);
359 assert!(result.is_err());
360 }
361
362 #[test]
363 fn deserialize_rejects_underscore_in_multi_path() {
364 let result: Result<FieldMask, _> = serde_json::from_str(r#""fooBar,baz_qux""#);
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn serialize_accepts_path_with_digit_not_after_underscore() {
370 let m = FieldMask::from_paths(["foo3_bar"]);
371 let json = serde_json::to_string(&m).unwrap();
372 assert_eq!(json, r#""foo3Bar""#);
373 let back: FieldMask = serde_json::from_str(&json).unwrap();
374 assert_eq!(back.paths, ["foo3_bar"]);
375 }
376
377 #[test]
378 fn serialize_rejects_trailing_underscore() {
379 let m = FieldMask::from_paths(["foo_"]);
380 assert!(serde_json::to_string(&m).is_err());
381 }
382 }
383}