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                    && !has_schema_metadata_attr_str(
64                        property_schema,
65                        constants::DOCS_META_HUMAN_NAME,
66                    )
67                {
68                    let human_name = generate_human_friendly_string(property_name);
69                    set_schema_metadata_attr_str(
70                        property_schema,
71                        constants::DOCS_META_HUMAN_NAME,
72                        human_name,
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 super::GenerateHumanFriendlyNameVisitor;
110    use crate::schema::visitors::test::{as_schema, assert_schemas_eq};
111
112    #[test]
113    fn logical_name() {
114        let mut actual_schema = as_schema(json!({
115            "type": "string",
116            "_metadata": {
117                "logical_name": "LogToMetric"
118            }
119        }));
120
121        let expected_schema = as_schema(json!({
122            "type": "string",
123            "_metadata": {
124                "docs::human_name": "Log To Metric",
125                "logical_name": "LogToMetric"
126            }
127        }));
128
129        let mut visitor = GenerateHumanFriendlyNameVisitor;
130        visitor.visit_root_schema(&mut actual_schema);
131
132        assert_schemas_eq(expected_schema, actual_schema);
133    }
134
135    #[test]
136    fn logical_name_with_replacement() {
137        let mut actual_schema = as_schema(json!({
138            "type": "string",
139            "_metadata": {
140                "logical_name": "AwsCloudwatchLogs"
141            }
142        }));
143
144        let expected_schema = as_schema(json!({
145            "type": "string",
146            "_metadata": {
147                "docs::human_name": "AWS CloudWatch Logs",
148                "logical_name": "AwsCloudwatchLogs"
149            }
150        }));
151
152        let mut visitor = GenerateHumanFriendlyNameVisitor;
153        visitor.visit_root_schema(&mut actual_schema);
154
155        assert_schemas_eq(expected_schema, actual_schema);
156    }
157
158    #[test]
159    fn property_name() {
160        let mut actual_schema = as_schema(json!({
161            "type": "object",
162            "properties": {
163                "store_key": { "type": "boolean" }
164            }
165        }));
166
167        let expected_schema = as_schema(json!({
168            "type": "object",
169            "properties": {
170                "store_key": {
171                    "type": "boolean",
172                    "_metadata": {
173                      "docs::human_name": "Store Key"
174                    }
175                }
176            }
177        }));
178
179        let mut visitor = GenerateHumanFriendlyNameVisitor;
180        visitor.visit_root_schema(&mut actual_schema);
181
182        assert_schemas_eq(expected_schema, actual_schema);
183    }
184
185    #[test]
186    fn property_name_with_replacement() {
187        let mut actual_schema = as_schema(json!({
188            "type": "object",
189            "properties": {
190                "store_api_key": { "type": "boolean" }
191            }
192        }));
193
194        let expected_schema = as_schema(json!({
195            "type": "object",
196            "properties": {
197                "store_api_key": {
198                    "type": "boolean",
199                    "_metadata": {
200                      "docs::human_name": "Store API Key"
201                    }
202                }
203            }
204        }));
205
206        let mut visitor = GenerateHumanFriendlyNameVisitor;
207        visitor.visit_root_schema(&mut actual_schema);
208
209        assert_schemas_eq(expected_schema, actual_schema);
210    }
211
212    #[test]
213    fn logical_name_override() {
214        let mut actual_schema = as_schema(json!({
215            "type": "string",
216            "_metadata": {
217                "docs::human_name": "AWS EC2 Metadata",
218                "logical_name": "Ec2Metadata"
219            }
220        }));
221
222        let expected_schema = actual_schema.clone();
223
224        let mut visitor = GenerateHumanFriendlyNameVisitor;
225        visitor.visit_root_schema(&mut actual_schema);
226
227        assert_schemas_eq(expected_schema, actual_schema);
228    }
229
230    #[test]
231    fn property_name_override() {
232        let mut actual_schema = as_schema(json!({
233            "type": "object",
234            "properties": {
235                "store_api_key": {
236                    "type": "boolean",
237                    "_metadata": {
238                        "docs::human_name": "Store_api_key"
239                    }
240                }
241            }
242        }));
243
244        let expected_schema = actual_schema.clone();
245
246        let mut visitor = GenerateHumanFriendlyNameVisitor;
247        visitor.visit_root_schema(&mut actual_schema);
248
249        assert_schemas_eq(expected_schema, actual_schema);
250    }
251
252    #[test]
253    fn mixed_with_replacement() {
254        let mut actual_schema = as_schema(json!({
255            "type": "object",
256            "properties": {
257                "store_api_key": { "type": "boolean" }
258            },
259            "_metadata": {
260                "logical_name": "AwsEc2Metadata"
261            }
262        }));
263
264        let expected_schema = as_schema(json!({
265            "type": "object",
266            "properties": {
267                "store_api_key": {
268                    "type": "boolean",
269                    "_metadata": {
270                      "docs::human_name": "Store API Key"
271                    }
272                }
273            },
274            "_metadata": {
275                "docs::human_name": "AWS EC2 Metadata",
276                "logical_name": "AwsEc2Metadata"
277            }
278        }));
279
280        let mut visitor = GenerateHumanFriendlyNameVisitor;
281        visitor.visit_root_schema(&mut actual_schema);
282
283        assert_schemas_eq(expected_schema, actual_schema);
284    }
285}