vector_config/schema/visitors/
human_name.rs

1use serde_json::Value;
2use vector_config_common::{
3    constants,
4    human_friendly::generate_human_friendly_string,
5    schema::{visit::Visitor, *},
6};
7
8/// A visitor that generates a human-friendly name for enum variants and fields as metadata.
9///
10/// Generally, we rely on rich documentation to provide human-friendly descriptions of types and
11/// fields, but there is no such mechanism to provide a human-friendly name for types and fields
12/// directly from their documentation comments. While it is possible to do so with manual metadata
13/// annotations, it is laborious and prone to error.
14///
15/// This visitor generates a human-friendly name for types and fields, stored in metadata
16/// (`docs::human_name`) using a simple set of heuristics to figure out how to break apart
17/// type/field names, as well as what the case of each word should be, including accommodations for
18/// well-known technical terms/acronyms, and so on.
19///
20/// ## Opting out of the visitor behavior
21///
22/// This approach has a very high hit rate, as the corpus we're operating on is generally small and
23/// well contained, leading to requiring only a small set of replacements and logic. However, for
24/// cases when this approach is not suitable, upstream usages can declare `docs::human_name`
25/// themselves. Whenever the visitor sees that the metadata annotation is already present, it will
26/// skip generating it.
27#[derive(Debug, Default)]
28pub struct GenerateHumanFriendlyNameVisitor;
29
30impl GenerateHumanFriendlyNameVisitor {
31    pub fn from_settings(_: &SchemaSettings) -> Self {
32        Self
33    }
34}
35
36impl Visitor for GenerateHumanFriendlyNameVisitor {
37    fn visit_schema_object(
38        &mut self,
39        definitions: &mut Map<String, Schema>,
40        schema: &mut SchemaObject,
41    ) {
42        // Recursively visit this schema first.
43        visit::visit_schema_object(self, definitions, schema);
44
45        // Skip this schema if it already has a human-friendly name defined.
46        if has_schema_metadata_attr_str(schema, constants::DOCS_META_HUMAN_NAME) {
47            return;
48        }
49
50        // When a logical name (via `logical_name`) is present, we use that as the source for
51        // generating the human-friendly name. Logical name is populated for schemas that represent
52        // an enum variant.
53        if let Some(logical_name) = get_schema_metadata_attr_str(schema, constants::LOGICAL_NAME) {
54            let human_name = generate_human_friendly_string(logical_name);
55            set_schema_metadata_attr_str(schema, constants::DOCS_META_HUMAN_NAME, human_name);
56        }
57
58        // If the schema has object properties, we'll individually add the human name to each
59        // property's schema if it doesn't already have a human-friendly name defined.
60        if let Some(properties) = schema.object.as_mut().map(|object| &mut object.properties) {
61            for (property_name, property_schema) in properties.iter_mut() {
62                if let Some(property_schema) = property_schema.as_object_mut() {
63                    if !has_schema_metadata_attr_str(
64                        property_schema,
65                        constants::DOCS_META_HUMAN_NAME,
66                    ) {
67                        let human_name = generate_human_friendly_string(property_name);
68                        set_schema_metadata_attr_str(
69                            property_schema,
70                            constants::DOCS_META_HUMAN_NAME,
71                            human_name,
72                        );
73                    }
74                }
75            }
76        }
77    }
78}
79
80fn has_schema_metadata_attr_str(schema: &SchemaObject, key: &str) -> bool {
81    get_schema_metadata_attr_str(schema, key).is_some()
82}
83
84fn get_schema_metadata_attr_str<'a>(schema: &'a SchemaObject, key: &str) -> Option<&'a str> {
85    schema
86        .extensions
87        .get(constants::METADATA)
88        .and_then(|metadata| metadata.get(key))
89        .and_then(|value| value.as_str())
90}
91
92fn set_schema_metadata_attr_str(schema: &mut SchemaObject, key: &str, value: String) {
93    let metadata = schema
94        .extensions
95        .entry(constants::METADATA.to_string())
96        .or_insert_with(|| Value::Object(serde_json::Map::new()));
97
98    let metadata_map = metadata
99        .as_object_mut()
100        .expect("schema metadata must always be an object");
101    metadata_map.insert(key.to_string(), Value::String(value));
102}
103
104#[cfg(test)]
105mod tests {
106    use serde_json::json;
107    use vector_config_common::schema::visit::Visitor;
108
109    use crate::schema::visitors::test::{as_schema, assert_schemas_eq};
110
111    use super::GenerateHumanFriendlyNameVisitor;
112
113    #[test]
114    fn logical_name() {
115        let mut actual_schema = as_schema(json!({
116            "type": "string",
117            "_metadata": {
118                "logical_name": "LogToMetric"
119            }
120        }));
121
122        let expected_schema = as_schema(json!({
123            "type": "string",
124            "_metadata": {
125                "docs::human_name": "Log To Metric",
126                "logical_name": "LogToMetric"
127            }
128        }));
129
130        let mut visitor = GenerateHumanFriendlyNameVisitor;
131        visitor.visit_root_schema(&mut actual_schema);
132
133        assert_schemas_eq(expected_schema, actual_schema);
134    }
135
136    #[test]
137    fn logical_name_with_replacement() {
138        let mut actual_schema = as_schema(json!({
139            "type": "string",
140            "_metadata": {
141                "logical_name": "AwsCloudwatchLogs"
142            }
143        }));
144
145        let expected_schema = as_schema(json!({
146            "type": "string",
147            "_metadata": {
148                "docs::human_name": "AWS CloudWatch Logs",
149                "logical_name": "AwsCloudwatchLogs"
150            }
151        }));
152
153        let mut visitor = GenerateHumanFriendlyNameVisitor;
154        visitor.visit_root_schema(&mut actual_schema);
155
156        assert_schemas_eq(expected_schema, actual_schema);
157    }
158
159    #[test]
160    fn property_name() {
161        let mut actual_schema = as_schema(json!({
162            "type": "object",
163            "properties": {
164                "store_key": { "type": "boolean" }
165            }
166        }));
167
168        let expected_schema = as_schema(json!({
169            "type": "object",
170            "properties": {
171                "store_key": {
172                    "type": "boolean",
173                    "_metadata": {
174                      "docs::human_name": "Store Key"
175                    }
176                }
177            }
178        }));
179
180        let mut visitor = GenerateHumanFriendlyNameVisitor;
181        visitor.visit_root_schema(&mut actual_schema);
182
183        assert_schemas_eq(expected_schema, actual_schema);
184    }
185
186    #[test]
187    fn property_name_with_replacement() {
188        let mut actual_schema = as_schema(json!({
189            "type": "object",
190            "properties": {
191                "store_api_key": { "type": "boolean" }
192            }
193        }));
194
195        let expected_schema = as_schema(json!({
196            "type": "object",
197            "properties": {
198                "store_api_key": {
199                    "type": "boolean",
200                    "_metadata": {
201                      "docs::human_name": "Store API Key"
202                    }
203                }
204            }
205        }));
206
207        let mut visitor = GenerateHumanFriendlyNameVisitor;
208        visitor.visit_root_schema(&mut actual_schema);
209
210        assert_schemas_eq(expected_schema, actual_schema);
211    }
212
213    #[test]
214    fn logical_name_override() {
215        let mut actual_schema = as_schema(json!({
216            "type": "string",
217            "_metadata": {
218                "docs::human_name": "AWS EC2 Metadata",
219                "logical_name": "Ec2Metadata"
220            }
221        }));
222
223        let expected_schema = actual_schema.clone();
224
225        let mut visitor = GenerateHumanFriendlyNameVisitor;
226        visitor.visit_root_schema(&mut actual_schema);
227
228        assert_schemas_eq(expected_schema, actual_schema);
229    }
230
231    #[test]
232    fn property_name_override() {
233        let mut actual_schema = as_schema(json!({
234            "type": "object",
235            "properties": {
236                "store_api_key": {
237                    "type": "boolean",
238                    "_metadata": {
239                        "docs::human_name": "Store_api_key"
240                    }
241                }
242            }
243        }));
244
245        let expected_schema = actual_schema.clone();
246
247        let mut visitor = GenerateHumanFriendlyNameVisitor;
248        visitor.visit_root_schema(&mut actual_schema);
249
250        assert_schemas_eq(expected_schema, actual_schema);
251    }
252
253    #[test]
254    fn mixed_with_replacement() {
255        let mut actual_schema = as_schema(json!({
256            "type": "object",
257            "properties": {
258                "store_api_key": { "type": "boolean" }
259            },
260            "_metadata": {
261                "logical_name": "AwsEc2Metadata"
262            }
263        }));
264
265        let expected_schema = as_schema(json!({
266            "type": "object",
267            "properties": {
268                "store_api_key": {
269                    "type": "boolean",
270                    "_metadata": {
271                      "docs::human_name": "Store API Key"
272                    }
273                }
274            },
275            "_metadata": {
276                "docs::human_name": "AWS EC2 Metadata",
277                "logical_name": "AwsEc2Metadata"
278            }
279        }));
280
281        let mut visitor = GenerateHumanFriendlyNameVisitor;
282        visitor.visit_root_schema(&mut actual_schema);
283
284        assert_schemas_eq(expected_schema, actual_schema);
285    }
286}