iota_grpc_types/field/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5mod field_mask_tree;
6mod field_mask_util;
7
8pub use field_mask_tree::FieldMaskTree;
9pub use field_mask_util::FieldMaskUtil;
10pub use prost_types::FieldMask;
11
12/// Separator between field paths when a FieldMask is encoded as a string
13pub const FIELD_PATH_SEPARATOR: char = ',';
14
15/// Separator between fields in a field path
16pub const FIELD_SEPARATOR: char = '.';
17
18pub const FIELD_PATH_WILDCARD: &str = "*";
19
20fn is_valid_path(path: &str) -> bool {
21    if path == FIELD_PATH_WILDCARD {
22        return true;
23    }
24
25    path.split(FIELD_SEPARATOR).all(is_valid_path_component)
26}
27
28// A valid path component needs to be a valid protobuf identifier which is
29// defined by the following:
30//
31// ```
32// letter        = "A" … "Z" | "a" … "z" | "_" .
33// decimal_digit = "0" … "9"
34// identifier = letter { letter | decimal_digit }
35// ```
36fn is_valid_path_component(component: &str) -> bool {
37    if component.is_empty() || component == "_" {
38        return false;
39    }
40
41    let component = component.as_bytes();
42
43    if !(component[0].is_ascii_alphabetic() || component[0] == b'_') {
44        return false;
45    }
46
47    for &byte in &component[1..] {
48        if !(byte.is_ascii_alphabetic() || byte.is_ascii_digit() || byte == b'_') {
49            return false;
50        }
51    }
52
53    true
54}
55
56pub trait MessageFields {
57    const FIELDS: &'static [&'static MessageField];
58
59    /// Oneof group names declared in this message.
60    ///
61    /// A oneof name acts as a virtual parent path for its variant fields
62    /// during read mask validation.  For example, a message with
63    /// `oneof execution_result { CommandResults command_results = 3; ... }`
64    /// lists `"execution_result"` here so that paths like
65    /// `"execution_result.command_results"` are accepted by `validate()`.
66    const ONEOFS: &'static [&'static str] = &[];
67}
68
69pub struct MessageField {
70    pub name: &'static str,
71    pub json_name: &'static str,
72    pub number: i32,
73    pub is_optional: bool,
74    pub is_map: bool,
75    pub message_fields: Option<&'static [&'static MessageField]>,
76}
77
78impl MessageField {
79    // Returns the field name and all nested field names
80    pub fn get_optional_message_field_names(&self) -> Vec<String> {
81        let mut names = Vec::new();
82
83        // Recursively get nested field names and prefix them with the parent field name
84        if let Some(nested_fields) = self.message_fields {
85            for field in nested_fields {
86                for nested_name in field.get_optional_message_field_names() {
87                    names.push(format!("{}.{}", self.name, nested_name));
88                }
89            }
90        } else if self.is_optional {
91            // No nested fields, just return the field name if it's optional
92            names.push(self.name.to_string());
93        }
94
95        names
96    }
97}
98
99impl AsRef<str> for MessageField {
100    fn as_ref(&self) -> &str {
101        self.name
102    }
103}
104
105#[doc(hidden)]
106impl MessageField {
107    pub const fn new(name: &'static str) -> Self {
108        Self {
109            name,
110            json_name: "",
111            number: 0,
112            is_optional: false,
113            is_map: false,
114            message_fields: None,
115        }
116    }
117
118    pub const fn with_message_fields(
119        mut self,
120        message_fields: &'static [&'static MessageField],
121    ) -> Self {
122        self.message_fields = Some(message_fields);
123        self
124    }
125
126    pub const fn with_optional(mut self, is_optional: bool) -> Self {
127        self.is_optional = is_optional;
128        self
129    }
130
131    pub const fn with_is_map(mut self, is_map: bool) -> Self {
132        self.is_map = is_map;
133        self
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_valid_path_component() {
143        let cases = [
144            ("foo", true),
145            ("_", false),
146            ("", false),
147            ("_abc", true),
148            ("BAR", true),
149            ("foo.bar", false),
150        ];
151
152        for (case, expected) in cases {
153            assert_eq!(is_valid_path_component(case), expected);
154        }
155    }
156
157    #[test]
158    fn test_valid_path() {
159        let cases = [
160            ("*", true),
161            ("**", false),
162            ("foo.bar", true),
163            ("foo.bar.baz", true),
164            ("_", false),
165            (".", false),
166            ("", false),
167            ("_abc", true),
168            ("BAR", true),
169        ];
170
171        for (case, expected) in cases {
172            assert_eq!(is_valid_path(case), expected);
173        }
174    }
175}