#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct FieldMask {
pub paths: Vec<String>,
}
impl FieldMask {
pub fn set_paths<T, V>(mut self, paths: T) -> Self
where
T: IntoIterator<Item = V>,
V: Into<String>,
{
self.paths = paths.into_iter().map(|v| v.into()).collect();
self
}
}
impl crate::message::Message for FieldMask {
fn typename() -> &'static str {
"type.googleapis.com/google.protobuf.FieldMask"
}
#[allow(private_interfaces)]
fn serializer() -> impl crate::message::MessageSerializer<Self> {
crate::message::ValueSerializer::<Self>::new()
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
impl serde::ser::Serialize for FieldMask {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let paths = self
.paths
.iter()
.map(|p| to_camel_case(p))
.collect::<Vec<_>>()
.join(",");
serializer.serialize_str(&paths)
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
impl<'de> serde::de::Deserialize<'de> for FieldMask {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let paths = deserializer.deserialize_any(PathVisitor)?;
Ok(FieldMask { paths })
}
}
struct PathVisitor;
impl serde::de::Visitor<'_> for PathVisitor {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string with comma-separated field mask paths)")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value.is_empty() {
Ok(Vec::new())
} else {
Ok(value.split(',').map(to_snake_case).collect())
}
}
}
fn to_camel_case(snake: &str) -> String {
let mut camel = String::with_capacity(snake.len());
let mut capitalize_next = false;
for c in snake.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
camel.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
camel.push(c);
}
}
camel
}
fn to_snake_case(camel: &str) -> String {
let mut snake = String::with_capacity(camel.len() + 5);
for c in camel.chars() {
if c.is_ascii_uppercase() {
snake.push('_');
snake.push(c.to_ascii_lowercase());
} else {
snake.push(c);
}
}
snake
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Value, json};
use test_case::test_case;
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
#[test_case(vec![], ""; "Serialize empty")]
#[test_case(vec!["field1"], "field1"; "Serialize single")]
#[test_case(vec!["field1", "field2", "field3"], "field1,field2,field3"; "Serialize multiple")]
#[test_case(vec!["field_one"], "fieldOne"; "Serialize snake to camel")]
#[test_case(vec!["user.display_name"], "user.displayName"; "Serialize path snake to camel")]
fn test_serialize(paths: Vec<&str>, want: &str) -> Result {
let value = serde_json::to_value(FieldMask::default().set_paths(paths))?;
assert!(matches!(&value, Value::String(s) if s == want), "{value:?}");
Ok(())
}
#[test_case("", vec![]; "Deserialize empty")]
#[test_case("field1", vec!["field1"]; "Deserialize single")]
#[test_case("field1,field2,field3", vec!["field1" ,"field2", "field3"]; "Deserialize multiple")]
#[test_case("fieldOne", vec!["field_one"]; "Deserialize camel to snake")]
#[test_case("user.displayName", vec!["user.display_name"]; "Deserialize path camel to snake")]
fn test_deserialize(paths: &str, mut want: Vec<&str>) -> Result {
let value = json!(paths);
let mut got = serde_json::from_value::<FieldMask>(value)?;
want.sort();
got.paths.sort();
assert_eq!(got.paths, want);
Ok(())
}
#[test]
fn deserialize_unexpected_input_type() -> Result {
let err = serde_json::from_value::<FieldMask>(json!({"paths": {"a": "b"}})).unwrap_err();
assert!(err.is_data(), "{err:?}");
let msg = err.to_string();
assert!(
msg.contains("field mask paths"),
"message={msg}, debug={err:?}"
);
Ok(())
}
#[test_case("field_one", "fieldOne")]
#[test_case("user.display_name", "user.displayName")]
#[test_case("field_1", "field1")]
#[test_case("active__user", "activeUser")]
#[test_case("field", "field")]
#[test_case("alreadyCamel", "alreadyCamel")]
#[test_case("a_b_c", "aBC")]
fn test_to_camel_case_fn(input: &str, expected: &str) {
assert_eq!(to_camel_case(input), expected);
}
#[test_case("fieldOne", "field_one")]
#[test_case("user.displayName", "user.display_name")]
#[test_case("field", "field")]
#[test_case("already_snake", "already_snake")]
fn test_to_snake_case_fn(input: &str, expected: &str) {
assert_eq!(to_snake_case(input), expected);
}
#[test]
fn test_exhaustive_roundtrip() {
let chars = b"abcdefghijklmnopqrstuvwxyz_";
let mut buf = [0u8; 4];
for &c1 in chars {
buf[0] = c1;
for &c2 in chars {
buf[1] = c2;
for &c3 in chars {
buf[2] = c3;
for &c4 in chars {
buf[3] = c4;
let s = std::str::from_utf8(&buf).unwrap();
if s.starts_with('_') || s.ends_with('_') || s.contains("__") {
continue;
}
let camel = to_camel_case(s);
let snake = to_snake_case(&camel);
assert_eq!(snake, s, "Failed for s='{}', camel='{}'", s, camel);
}
}
}
}
}
}