Skip to main content

google_cloud_wkt/
field_mask.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/// `FieldMask` represents a set of symbolic field paths.
16///
17/// # Example
18/// ```
19/// # use google_cloud_wkt::FieldMask;
20/// let mask = FieldMask::default().set_paths(["f.a", "f.b.d"]);
21/// assert_eq!(mask.paths, vec!["f.a".to_string(), "f.b.d".to_string()]);
22/// # Ok::<(), anyhow::Error>(())
23/// ```
24///
25/// # Background
26///
27/// Consider this text proto representation:
28///
29/// ```norust
30///     paths: "f.a"
31///     paths: "f.b.d"
32/// ```
33///
34/// Here `f` represents a field in some root message, `a` and `b`
35/// fields in the message found in `f`, and `d` a field found in the
36/// message in `f.b`.
37///
38/// Field masks are used to specify a subset of fields that should be
39/// returned by a get operation or modified by an update operation.
40/// Field masks also have a custom JSON encoding (see below).
41///
42/// # Field Masks in Projections
43///
44/// When used in the context of a projection, a response message or
45/// sub-message is filtered by the API to only contain those fields as
46/// specified in the mask. For example, if the mask in the previous
47/// example is applied to a response message as follows:
48///
49/// ```norust
50///     f {
51///       a : 22
52///       b {
53///         d : 1
54///         x : 2
55///       }
56///       y : 13
57///     }
58///     z: 8
59/// ```
60///
61/// The result will not contain specific values for fields x,y and z
62/// (their value will be set to the default, and omitted in proto text
63/// output):
64///
65///
66/// ```norust
67///     f {
68///       a : 22
69///       b {
70///         d : 1
71///       }
72///     }
73/// ```
74///
75/// A repeated field is not allowed except at the last position of a
76/// paths string.
77///
78/// If a FieldMask object is not present in a get operation, the
79/// operation applies to all fields (as if a FieldMask of all fields
80/// had been specified).
81///
82/// Note that a field mask does not necessarily apply to the
83/// top-level response message. In case of a REST get operation, the
84/// field mask applies directly to the response, but in case of a REST
85/// list operation, the mask instead applies to each individual message
86/// in the returned resource list. In case of a REST custom method,
87/// other definitions may be used. Where the mask applies will be
88/// clearly documented together with its declaration in the API.  In
89/// any case, the effect on the returned resource/resources is required
90/// behavior for APIs.
91///
92/// # Field Masks in Update Operations
93///
94/// A field mask in update operations specifies which fields of the
95/// targeted resource are going to be updated. The API is required
96/// to only change the values of the fields as specified in the mask
97/// and leave the others untouched. If a resource is passed in to
98/// describe the updated values, the API ignores the values of all
99/// fields not covered by the mask.
100///
101/// If a repeated field is specified for an update operation, new values will
102/// be appended to the existing repeated field in the target resource. Note that
103/// a repeated field is only allowed in the last position of a `paths` string.
104///
105/// If a sub-message is specified in the last position of the field mask for an
106/// update operation, then new value will be merged into the existing sub-message
107/// in the target resource.
108///
109/// For example, given the target message:
110///
111/// ```norust
112///     f {
113///       b {
114///         d: 1
115///         x: 2
116///       }
117///       c: [1]
118///     }
119/// ```
120///
121/// And an update message:
122///
123/// ```norust
124///     f {
125///       b {
126///         d: 10
127///       }
128///       c: [2]
129///     }
130/// ```
131///
132/// then if the field mask is:
133///
134/// ```norust
135///  paths: ["f.b", "f.c"]
136/// ```
137///
138/// then the result will be:
139///
140/// ```norust
141///     f {
142///       b {
143///         d: 10
144///         x: 2
145///       }
146///       c: [1, 2]
147///     }
148/// ```
149///
150/// An implementation may provide options to override this default behavior for
151/// repeated and message fields.
152///
153/// In order to reset a field's value to the default, the field must
154/// be in the mask and set to the default value in the provided resource.
155/// Hence, in order to reset all fields of a resource, provide a default
156/// instance of the resource and set all fields in the mask, or do
157/// not provide a mask as described below.
158///
159/// If a field mask is not present on update, the operation applies to
160/// all fields (as if a field mask of all fields has been specified).
161/// Note that in the presence of schema evolution, this may mean that
162/// fields the client does not know and has therefore not filled into
163/// the request will be reset to their default. If this is unwanted
164/// behavior, a specific service may require a client to always specify
165/// a field mask, producing an error if not.
166///
167/// As with get operations, the location of the resource which
168/// describes the updated values in the request message depends on the
169/// operation kind. In any case, the effect of the field mask is
170/// required to be honored by the API.
171///
172/// ## Considerations for HTTP REST
173///
174/// The HTTP kind of an update operation which uses a field mask must
175/// be set to PATCH instead of PUT in order to satisfy HTTP semantics
176/// (PUT must only be used for full updates).
177///
178/// # JSON Encoding of Field Masks
179///
180/// In JSON, a field mask is encoded as a single string where paths are
181/// separated by a comma. Fields name in each path are converted
182/// to/from lower-camel naming conventions.
183///
184/// As an example, consider the following message declarations:
185///
186/// ```norust
187///     message Profile {
188///       User user = 1;
189///       Photo photo = 2;
190///     }
191///     message User {
192///       string display_name = 1;
193///       string address = 2;
194///     }
195/// ```
196///
197/// In proto a field mask for `Profile` may look as such:
198///
199/// ```norust
200///     mask {
201///       paths: "user.display_name"
202///       paths: "photo"
203///     }
204/// ```
205///
206/// In JSON, the same mask is represented as below:
207///
208/// ```norust
209///     {
210///       mask: "user.displayName,photo"
211///     }
212/// ```
213///
214/// # Field Masks and Oneof Fields
215///
216/// Field masks treat fields in oneofs just as regular fields. Consider the
217/// following message:
218///
219/// ```norust
220///     message SampleMessage {
221///       oneof test_oneof {
222///         string name = 4;
223///         SubMessage sub_message = 9;
224///       }
225///     }
226/// ```
227///
228/// The field mask can be:
229///
230/// ```norust
231///     mask {
232///       paths: "name"
233///     }
234/// ```
235///
236/// Or:
237///
238/// ```norust
239///     mask {
240///       paths: "sub_message"
241///     }
242/// ```
243///
244/// Note that oneof type names ("test_oneof" in this case) cannot be used in
245/// paths.
246///
247/// ## Field Mask Verification
248///
249/// The implementation of any API method which has a FieldMask type field in the
250/// request should verify the included field paths, and return an
251/// `INVALID_ARGUMENT` error if any path is unmappable.
252#[derive(Clone, Debug, Default, PartialEq)]
253#[non_exhaustive]
254pub struct FieldMask {
255    /// The set of field mask paths.
256    pub paths: Vec<String>,
257}
258
259impl FieldMask {
260    /// Set the paths.
261    ///
262    /// # Example
263    /// ```
264    /// # use google_cloud_wkt::FieldMask;
265    /// let mask = FieldMask::default().set_paths(["abc", "xyz"]);
266    /// assert_eq!(mask.paths, vec!["abc".to_string(), "xyz".to_string()]);
267    /// # Ok::<(), anyhow::Error>(())
268    /// ```
269    pub fn set_paths<T, V>(mut self, paths: T) -> Self
270    where
271        T: IntoIterator<Item = V>,
272        V: Into<String>,
273    {
274        self.paths = paths.into_iter().map(|v| v.into()).collect();
275        self
276    }
277}
278
279impl crate::message::Message for FieldMask {
280    fn typename() -> &'static str {
281        "type.googleapis.com/google.protobuf.FieldMask"
282    }
283
284    #[allow(private_interfaces)]
285    fn serializer() -> impl crate::message::MessageSerializer<Self> {
286        crate::message::ValueSerializer::<Self>::new()
287    }
288}
289
290/// Implement [serde] serialization for [FieldMask]
291#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
292impl serde::ser::Serialize for FieldMask {
293    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
294    where
295        S: serde::ser::Serializer,
296    {
297        let paths = self
298            .paths
299            .iter()
300            .map(|p| to_camel_case(p))
301            .collect::<Vec<_>>()
302            .join(",");
303        serializer.serialize_str(&paths)
304    }
305}
306
307/// Implement [serde] deserialization for [FieldMask].
308#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
309impl<'de> serde::de::Deserialize<'de> for FieldMask {
310    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
311    where
312        D: serde::Deserializer<'de>,
313    {
314        let paths = deserializer.deserialize_any(PathVisitor)?;
315        Ok(FieldMask { paths })
316    }
317}
318
319struct PathVisitor;
320
321impl serde::de::Visitor<'_> for PathVisitor {
322    type Value = Vec<String>;
323
324    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
325        formatter.write_str("a string with comma-separated field mask paths)")
326    }
327
328    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
329    where
330        E: serde::de::Error,
331    {
332        if value.is_empty() {
333            Ok(Vec::new())
334        } else {
335            Ok(value.split(',').map(to_snake_case).collect())
336        }
337    }
338}
339
340fn to_camel_case(snake: &str) -> String {
341    let mut camel = String::with_capacity(snake.len());
342    let mut capitalize_next = false;
343    for c in snake.chars() {
344        if c == '_' {
345            capitalize_next = true;
346        } else if capitalize_next {
347            camel.push(c.to_ascii_uppercase());
348            capitalize_next = false;
349        } else {
350            camel.push(c);
351        }
352    }
353    camel
354}
355
356fn to_snake_case(camel: &str) -> String {
357    let mut snake = String::with_capacity(camel.len() + 5);
358    for c in camel.chars() {
359        if c.is_ascii_uppercase() {
360            snake.push('_');
361            snake.push(c.to_ascii_lowercase());
362        } else {
363            snake.push(c);
364        }
365    }
366    snake
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use serde_json::{Value, json};
373    use test_case::test_case;
374
375    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
376
377    #[test_case(vec![], ""; "Serialize empty")]
378    #[test_case(vec!["field1"], "field1"; "Serialize single")]
379    #[test_case(vec!["field1", "field2", "field3"], "field1,field2,field3"; "Serialize multiple")]
380    #[test_case(vec!["field_one"], "fieldOne"; "Serialize snake to camel")]
381    #[test_case(vec!["user.display_name"], "user.displayName"; "Serialize path snake to camel")]
382    fn test_serialize(paths: Vec<&str>, want: &str) -> Result {
383        let value = serde_json::to_value(FieldMask::default().set_paths(paths))?;
384        assert!(matches!(&value, Value::String(s) if s == want), "{value:?}");
385        Ok(())
386    }
387
388    #[test_case("", vec![]; "Deserialize empty")]
389    #[test_case("field1", vec!["field1"]; "Deserialize single")]
390    #[test_case("field1,field2,field3", vec!["field1" ,"field2", "field3"]; "Deserialize multiple")]
391    #[test_case("fieldOne", vec!["field_one"]; "Deserialize camel to snake")]
392    #[test_case("user.displayName", vec!["user.display_name"]; "Deserialize path camel to snake")]
393    fn test_deserialize(paths: &str, mut want: Vec<&str>) -> Result {
394        let value = json!(paths);
395        let mut got = serde_json::from_value::<FieldMask>(value)?;
396        want.sort();
397        got.paths.sort();
398        assert_eq!(got.paths, want);
399        Ok(())
400    }
401
402    #[test]
403    fn deserialize_unexpected_input_type() -> Result {
404        let err = serde_json::from_value::<FieldMask>(json!({"paths": {"a": "b"}})).unwrap_err();
405        assert!(err.is_data(), "{err:?}");
406        let msg = err.to_string();
407        assert!(
408            msg.contains("field mask paths"),
409            "message={msg}, debug={err:?}"
410        );
411        Ok(())
412    }
413
414    #[test_case("field_one", "fieldOne")]
415    #[test_case("user.display_name", "user.displayName")]
416    #[test_case("field_1", "field1")]
417    #[test_case("active__user", "activeUser")]
418    #[test_case("field", "field")]
419    #[test_case("alreadyCamel", "alreadyCamel")]
420    #[test_case("a_b_c", "aBC")]
421    fn test_to_camel_case_fn(input: &str, expected: &str) {
422        assert_eq!(to_camel_case(input), expected);
423    }
424
425    #[test_case("fieldOne", "field_one")]
426    #[test_case("user.displayName", "user.display_name")]
427    #[test_case("field", "field")]
428    #[test_case("already_snake", "already_snake")]
429    fn test_to_snake_case_fn(input: &str, expected: &str) {
430        assert_eq!(to_snake_case(input), expected);
431    }
432
433    #[test]
434    fn test_exhaustive_roundtrip() {
435        let chars = b"abcdefghijklmnopqrstuvwxyz_";
436        let mut buf = [0u8; 4];
437        for &c1 in chars {
438            buf[0] = c1;
439            for &c2 in chars {
440                buf[1] = c2;
441                for &c3 in chars {
442                    buf[2] = c3;
443                    for &c4 in chars {
444                        buf[3] = c4;
445                        let s = std::str::from_utf8(&buf).unwrap();
446                        if s.starts_with('_') || s.ends_with('_') || s.contains("__") {
447                            continue;
448                        }
449                        let camel = to_camel_case(s);
450                        let snake = to_snake_case(&camel);
451                        assert_eq!(snake, s, "Failed for s='{}', camel='{}'", s, camel);
452                    }
453                }
454            }
455        }
456    }
457}