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}