docs_renderer/
renderer.rs

1use std::collections::{HashMap, VecDeque};
2
3use anyhow::Result;
4use serde::Serialize;
5use serde_json::{Map, Value};
6use snafu::Snafu;
7use tracing::debug;
8use vector_config::schema::parser::query::{
9    QueryError, QueryableSchema, SchemaQuerier, SchemaType,
10};
11use vector_config_common::constants;
12
13#[derive(Debug, Snafu)]
14pub enum RenderError {
15    #[snafu(display("rendering failed: {reason}"))]
16    Failed { reason: String },
17
18    #[snafu(display("query error during rendering: {source}"), context(false))]
19    Query { source: QueryError },
20}
21
22#[derive(Serialize)]
23#[serde(transparent)]
24pub struct RenderData {
25    root: Value,
26}
27
28impl RenderData {
29    fn with_mut_object<F, V>(&mut self, f: F) -> V
30    where
31        F: FnOnce(&mut Map<String, Value>) -> V,
32    {
33        // TODO: We should refactor this method so that it takes the desired path, a boolean for
34        // whether or not to create missing path nodes, and a closure to call with the object
35        // reference/object key if it exists.. and then this way, `write` and `delete` become simple
36        // calls with simple closures that just do `map.insert(...)` and `map.delete(...)` and so
37        // on.
38        //
39        // tl;dr: make it DRY.
40        let map = self
41            .root
42            .as_object_mut()
43            .expect("Render data should always have an object value as root.");
44        f(map)
45    }
46
47    /// Writes a value at the given path.
48    ///
49    /// The path follows the form of `/part1/part/.../partN`, where each slash-separated segment
50    /// represents a nested object within the overall object hierarchy. For example, a path of
51    /// `/root/nested/key2` would map to the value "weee!" if applied against the following JSON
52    /// object:
53    ///
54    ///   { "root": { "nested": { "key2": "weee!" } } }
55    ///
56    /// # Panics
57    ///
58    /// If the path does not start with a forward slash, this method will panic. Likewise, if the
59    /// path is _only_ a forward slash (aka there is no segment to describe the key within the
60    /// object to write the value to), this method will panic.
61    ///
62    /// If any nested object within the path does not yet exist, it will be created. If any segment,
63    /// other than the leaf segment, points to a value that is not an object/map, this method will
64    /// panic.
65    pub fn write<V: Into<Value>>(&mut self, path: &str, value: V) {
66        if !path.starts_with('/') {
67            panic!("Paths must always start with a leading forward slash (`/`).");
68        }
69
70        self.with_mut_object(|map| {
71            // Split the path, and take the last element as the actual map key to write to.
72            let mut segments = path.split('/').collect::<VecDeque<_>>();
73            let key = segments.pop_back().expect("Path must end with a key.");
74
75            // Iterate over the remaining elements, traversing into the root object one level at a
76            // time, based on using `token` as the map key. If there's no map at the given key,
77            // we'll create one. If there's something other than a map, we'll panic.
78            let mut destination = map;
79            while let Some(segment) = segments.pop_front() {
80                if destination.contains_key(segment) {
81                    match destination.get_mut(segment) {
82                        Some(Value::Object(ref mut next)) => {
83                            destination = next;
84                            continue;
85                        }
86                        Some(_) => {
87                            panic!("Only leaf nodes should be allowed to be non-object values.")
88                        }
89                        None => unreachable!("Already asserted that the given key exists."),
90                    }
91                } else {
92                    destination.insert(segment.to_string(), Value::Object(Map::new()));
93                    match destination.get_mut(segment) {
94                        Some(Value::Object(ref mut next)) => {
95                            destination = next;
96                        }
97                        _ => panic!("New object was just inserted."),
98                    }
99                }
100            }
101
102            destination.insert(key.to_string(), value.into());
103        });
104    }
105
106    /// Deletes the value at the given path.
107    ///
108    /// The path follows the form of `/part1/part/.../partN`, where each slash-separated segment
109    /// represents a nested object within the overall object hierarchy. For example, a path of
110    /// `/root/nested/key2` would map to the value "weee!" if applied against the following JSON
111    /// object:
112    ///
113    ///   { "root": { "nested": { "key2": "weee!" } } }
114    ///
115    /// # Panics
116    ///
117    /// If the path does not start with a forward slash, this method will panic. Likewise, if the
118    /// path is _only_ a forward slash (aka there is no segment to describe the key within the
119    /// object to write the value to), this method will panic.
120    ///
121    /// If any nested object within the path does not yet exist, it will be created. If any segment,
122    /// other than the leaf segment, points to a value that is not an object/map, this method will
123    /// panic.
124    pub fn delete(&mut self, path: &str) -> bool {
125        if !path.starts_with('/') {
126            panic!("Paths must always start with a leading forward slash (`/`).");
127        }
128
129        self.with_mut_object(|map| {
130            // Split the path, and take the last element as the actual map key to write to.
131            let mut segments = path.split('/').collect::<VecDeque<_>>();
132            let key = segments
133                .pop_back()
134                .expect("Path cannot point directly to the root. Use `clear` instead.");
135
136            // Iterate over the remaining elements, traversing into the root object one level at a
137            // time, based on using `token` as the map key. If there's no map at the given key,
138            // we'll create one. If there's something other than a map, we'll panic.
139            let mut destination = map;
140            while let Some(segment) = segments.pop_front() {
141                match destination.get_mut(segment) {
142                    Some(Value::Object(ref mut next)) => {
143                        destination = next;
144                        continue;
145                    }
146                    Some(_) => panic!("Only leaf nodes should be allowed to be non-object values."),
147                    // If the next segment doesn't exist, there's nothing for us to delete, so return `false`.
148                    None => return false,
149                }
150            }
151
152            destination.remove(key).is_some()
153        })
154    }
155
156    /// Gets whether or not a value at the given path.
157    ///
158    /// The path follows the form of `/part1/part/.../partN`, where each slash-separated segment
159    /// represents a nested object within the overall object hierarchy. For example, a path of
160    /// `/root/nested/key2` would map to the value "weee!" if applied against the following JSON
161    /// object:
162    ///
163    ///   { "root": { "nested": { "key2": "weee!" } } }
164    ///
165    /// # Panics
166    ///
167    /// If the path does not start with a forward slash, this method will panic.
168    pub fn exists(&self, path: &str) -> bool {
169        if !path.starts_with('/') {
170            panic!("Paths must always start with a leading forward slash (`/`).");
171        }
172
173        // The root path always exists.
174        if path == "/" {
175            return true;
176        }
177
178        self.root.pointer(path).is_some()
179    }
180
181    /// Merges the data from `other` into `self`.
182    ///
183    /// Uses a "deep" merge strategy, which will recursively merge both objects together. This
184    /// strategy behaves as follows:
185    ///
186    /// - strings, booleans, integers, numbers, and nulls are "highest priority wins" (`self` has
187    ///   highest priority)
188    /// - arrays are merged together without any deduplication, with the items from `self` appearing
189    ///   first
190    /// - objects have their properties merged together, but if an overlapping property is
191    ///   encountered:
192    ///   - if it has the same type on both sides, the property is merged normally (using the
193    ///     standard merge behavior)
194    ///   - if it does not have the same type on both sides, the property value on the `self` side
195    ///     takes precedence
196    ///
197    /// The only exception to the merge behavior above is if an overlapping object property does not
198    /// have the same type on both sides, but the type on the `self` side is an array. When the type
199    /// is an array, the value on the `other` side is appended to that array, regardless of the
200    /// contents of the array.
201    pub fn merge(&mut self, _other: Self) {
202        todo!()
203    }
204}
205
206impl Default for RenderData {
207    fn default() -> Self {
208        Self {
209            root: Value::Object(Map::new()),
210        }
211    }
212}
213
214pub struct SchemaRenderer<'a, T> {
215    querier: &'a SchemaQuerier,
216    schema: T,
217    data: RenderData,
218}
219
220impl<'a, T> SchemaRenderer<'a, T>
221where
222    T: QueryableSchema,
223{
224    pub fn new(querier: &'a SchemaQuerier, schema: T) -> Self {
225        Self {
226            querier,
227            schema,
228            data: RenderData::default(),
229        }
230    }
231
232    pub fn render(self) -> Result<RenderData, RenderError> {
233        let Self {
234            querier,
235            schema,
236            mut data,
237        } = self;
238
239        // If a schema is hidden, then we intentionally do not want to render it.
240        if schema.has_flag_attribute(constants::DOCS_META_HIDDEN)? {
241            debug!("Schema is marked as hidden. Skipping rendering.");
242
243            return Ok(data);
244        }
245
246        // If a schema has an overridden type, we return some barebones render data.
247        if schema.has_flag_attribute(constants::DOCS_META_TYPE_OVERRIDE)? {
248            debug!("Schema has overridden type.");
249
250            data.write("type", "blank");
251            apply_schema_description(&schema, &mut data)?;
252
253            return Ok(data);
254        }
255
256        // Now that we've handled any special cases, attempt to render the schema.
257        render_bare_schema(querier, &schema, &mut data)?;
258
259        // If the rendered schema represents an array schema, remove any description that is present
260        // for the schema of the array items themselves. We want the description of whatever object
261        // property that is using this array schema to be the one that is used.
262        //
263        // We just do this blindly because the control flow doesn't change depending on whether or
264        // not it's an array schema and we do or don't delete anything.
265        if data.delete("/type/array/items/description") {
266            debug!("Cleared description for items schema from top-level array schema.");
267        }
268
269        // Apply any necessary defaults, descriptions, and so on, to the rendered schema.
270        //
271        // This must happen here because there could be callsite-specific overrides to default
272        // values/descriptions/etc which must take precedence, so that must occur after any nested
273        // rendering in order to maintain that precedence.
274        apply_schema_default_value(&schema, &mut data)?;
275        apply_schema_metadata(&schema, &mut data)?;
276        apply_schema_description(&schema, &mut data)?;
277
278        Ok(data)
279    }
280}
281
282fn render_bare_schema<T: QueryableSchema>(
283    querier: &SchemaQuerier,
284    schema: T,
285    data: &mut RenderData,
286) -> Result<(), RenderError> {
287    match schema.schema_type() {
288        SchemaType::AllOf(subschemas) => {
289            // Composite (`allOf`) schemas are indeed the sum of all of their parts, so render each
290            // subschema and simply merge the rendered subschemas together.
291            for subschema in subschemas {
292                let subschema_renderer = SchemaRenderer::new(querier, subschema);
293                let rendered_subschema = subschema_renderer.render()?;
294                data.merge(rendered_subschema);
295            }
296        }
297        SchemaType::OneOf(_subschemas) => {}
298        SchemaType::AnyOf(_subschemas) => {}
299        SchemaType::Constant(const_value) => {
300            // All we need to do is figure out the rendered type for the constant value, so we can
301            // generate the right type path and stick the constant value in it.
302            let rendered_const_type = get_rendered_value_type(&schema, const_value)?;
303            let const_type_path = format!("/type/{rendered_const_type}/const");
304            data.write(const_type_path.as_str(), const_value.clone());
305        }
306        SchemaType::Enum(enum_values) => {
307            // Similar to constant schemas, we just need to figure out the rendered type for each
308            // enum value, so that we can group them together and then write the grouped values to
309            // each of their respective type paths.
310            let mut type_map = HashMap::new();
311
312            for enum_value in enum_values {
313                let rendered_enum_type = get_rendered_value_type(&schema, enum_value)?;
314                let type_group_entry = type_map.entry(rendered_enum_type).or_insert_with(Vec::new);
315                type_group_entry.push(enum_value.clone());
316            }
317
318            let structured_type_map = type_map
319                .into_iter()
320                .map(|(key, values)| {
321                    let mut nested = Map::new();
322                    nested.insert("enum".into(), Value::Array(values));
323
324                    (key, Value::Object(nested))
325                })
326                .collect::<Map<_, _>>();
327
328            data.write("/type", structured_type_map);
329        }
330        SchemaType::Typed(_instance_types) => {
331            // TODO: Technically speaking, we could have multiple instance types declared here,
332            // which is _entirely_ valid for JSON Schema. The trick is simply that we'll likely want
333            // to do something equivalent to how we handle composite schemas where we just render
334            // the schema in the context of each instance type, and then merge that rendered data
335            // together.
336            //
337            // This means that we'll need another render method that operates on a schema + instance
338            // type basis, since trying to do it all in `render_bare_schema` would get ugly fast.
339            //
340            // Practically, all of this is fine for regular ol' data types because they don't
341            // intersect, but the tricky bit would be if we encountered the null instance type. It's
342            // a real/valid data type, but the main problem is that there's nothing that really
343            // makes sense to do with it.
344            //
345            // An object property, for example, that can be X or null, is essentially an optional
346            // field. We handle that by including, or excluding, that property from the object's
347            // required fields, which is specific to object.
348            //
349            // The only real world scenario where we would theoretically hit that is for an untagged
350            // enum, as a unit variant in an untagged enum is represented by `null` in JSON, in
351            // terms of its serialized value. _However_, we only generate enums as `oneOf`/`anyOf`
352            // schemas, so the `null` instance type should only ever show up by itself.
353            //
354            // Long story short, we can likely have a hard-coded check that rejects any "X or null"
355            // instance type groupings, knowing that _we_ never generate schemas like that, but it's
356            // still technically possible in a real-world JSON Schema document... so we should at
357            // least make the error message half-way decent so that it explains as much.
358            todo!()
359        }
360    }
361
362    Ok(())
363}
364
365fn apply_schema_default_value<T: QueryableSchema>(
366    _schema: T,
367    _data: &mut RenderData,
368) -> Result<(), RenderError> {
369    Ok(())
370}
371
372fn apply_schema_metadata<T: QueryableSchema>(
373    schema: T,
374    data: &mut RenderData,
375) -> Result<(), RenderError> {
376    // If the schema is marked as being templateable, update the syntax of the string type field to
377    // use the special `template` sentinel value, which drives template-specific logic during the
378    // documentation generation phase.
379    if schema.has_flag_attribute(constants::DOCS_META_TEMPLATEABLE)? && data.exists("/type/string")
380    {
381        data.write("/type/string/syntax", "template");
382    }
383
384    // TODO: Add examples.
385    // TODO: Add units.
386    // TODO: Syntax override.
387
388    Ok(())
389}
390
391fn apply_schema_description<T: QueryableSchema>(
392    schema: T,
393    data: &mut RenderData,
394) -> Result<(), RenderError> {
395    if let Some(description) = render_schema_description(schema)? {
396        data.write("/description", description);
397    }
398
399    Ok(())
400}
401
402fn get_rendered_value_type<T: QueryableSchema>(
403    _schema: T,
404    _value: &Value,
405) -> Result<String, RenderError> {
406    todo!()
407}
408
409fn render_schema_description<T: QueryableSchema>(schema: T) -> Result<Option<String>, RenderError> {
410    let maybe_title = schema.title();
411    let maybe_description = schema.description();
412
413    match (maybe_title, maybe_description) {
414        (Some(_title), None) => Err(RenderError::Failed {
415            reason: "a schema should never have a title without a description".into(),
416        }),
417        (None, None) => Ok(None),
418        (None, Some(description)) => Ok(Some(description.trim().to_string())),
419        (Some(title), Some(description)) => {
420            let concatenated = format!("{title}\n\n{description}");
421            Ok(Some(concatenated.trim().to_string()))
422        }
423    }
424}