vdev/commands/build/component_docs/
schema_utils.rs1use super::{SchemaContext, get_schema_metadata};
2use anyhow::Result;
3use serde_json::{Map, Value};
4
5fn scalar_const_key(value: &Value) -> Option<String> {
9 match value {
10 Value::String(s) => Some(s.clone()),
11 Value::Number(n) => Some(n.to_string()),
12 Value::Bool(b) => Some(b.to_string()),
13 _ => None,
14 }
15}
16
17impl SchemaContext {
18 pub fn get_rendered_description_from_schema(&self, schema: &Value) -> String {
19 let raw_description = schema
20 .get("description")
21 .and_then(|v| v.as_str())
22 .unwrap_or("");
23 let raw_title = schema.get("title").and_then(|v| v.as_str()).unwrap_or("");
24
25 let description = if raw_title.is_empty() {
26 raw_description.to_string()
27 } else {
28 format!("{raw_title}\n\n{raw_description}")
29 };
30 description.trim().to_string()
31 }
32
33 pub fn unwrap_resolved_schema(
34 &mut self,
35 schema_name: &str,
36 friendly_name: &str,
37 ) -> Result<Map<String, Value>> {
38 info!("[*] Resolving schema definition for {}...", friendly_name);
39
40 let resolved_schema = self.resolve_schema_by_name(schema_name)?;
41
42 let unwrapped_obj = resolved_schema
43 .pointer("/type/object/options")
44 .and_then(Value::as_object)
45 .cloned()
46 .ok_or_else(|| {
47 anyhow::anyhow!(
48 "Configuration types must always resolve to an object schema; '{schema_name}' did not. Resolved: {resolved_schema}"
49 )
50 })?;
51
52 Ok(Self::sort_hash_nested(&unwrapped_obj))
54 }
55
56 pub fn fix_grouped_enums_if_numeric(
57 &self,
58 grouped: &mut indexmap::IndexMap<String, Vec<Value>>,
59 ) {
60 let mut numeric_vals = Vec::new();
61 if let Some(ints) = grouped.shift_remove("integer") {
62 numeric_vals.extend(ints);
63 }
64 if let Some(nums) = grouped.shift_remove("number") {
65 numeric_vals.extend(nums);
66 }
67
68 if !numeric_vals.is_empty() {
69 let is_integer = numeric_vals.iter().all(|v| v.is_i64() || v.is_u64());
70 let within_uint = numeric_vals.iter().all(serde_json::Value::is_u64);
71 let contains_signed = numeric_vals
72 .iter()
73 .all(|v| v.is_i64() || v.as_i64().is_some());
74
75 let numeric_type = if !is_integer || (!contains_signed && !within_uint) {
76 "float"
77 } else if within_uint {
78 "uint"
79 } else if contains_signed {
80 "int"
81 } else {
82 "float"
83 };
84
85 grouped.insert(numeric_type.to_string(), numeric_vals);
86 }
87 }
88
89 #[allow(clippy::self_only_used_in_recursion)]
90 pub fn get_reduced_schema(&self, schema: &Value) -> Value {
91 let mut reduced = schema.clone();
92 if let Value::Object(ref mut map) = reduced {
93 let allowed_properties = [
94 "type",
95 "const",
96 "enum",
97 "allOf",
98 "oneOf",
99 "$ref",
100 "items",
101 "properties",
102 ];
103 map.retain(|k, _| allowed_properties.contains(&k.as_str()));
104
105 if let Some(items) = map.get_mut("items") {
106 *items = self.get_reduced_schema(items);
107 }
108
109 if let Some(Value::Object(properties)) = map.get_mut("properties") {
110 for (_, prop) in properties.iter_mut() {
111 *prop = self.get_reduced_schema(prop);
112 }
113 }
114
115 for key in &["allOf", "oneOf"] {
116 if let Some(Value::Array(arr)) = map.get_mut(*key) {
117 for sub in arr.iter_mut() {
118 *sub = self.get_reduced_schema(sub);
119 }
120 }
121 }
122 }
123 reduced
124 }
125
126 #[allow(clippy::self_only_used_in_recursion)]
127 pub fn get_reduced_resolved_schema(&self, schema: &Value) -> Value {
128 let mut reduced = schema.clone();
129 let allowed_types = [
130 "condition",
131 "object",
132 "array",
133 "enum",
134 "const",
135 "string",
136 "bool",
137 "float",
138 "int",
139 "uint",
140 ];
141
142 if let Value::Object(ref mut map) = reduced {
143 map.retain(|k, _| k == "type");
144
145 if let Some(Value::Object(type_defs)) = map.get_mut("type") {
146 type_defs.retain(|k, _| allowed_types.contains(&k.as_str()));
147
148 for (type_name, type_def) in type_defs.iter_mut() {
149 if type_name == "object" {
150 if let Value::Object(def_map) = type_def {
151 def_map.retain(|k, _| k == "options");
152 if let Some(Value::Object(opts)) = def_map.get_mut("options") {
153 for (_, prop) in opts.iter_mut() {
154 *prop = self.get_reduced_resolved_schema(prop);
155 }
156 }
157 }
158 } else if type_name == "array" {
159 if let Value::Object(def_map) = type_def {
160 def_map.retain(|k, _| k == "items");
161 if let Some(items) = def_map.get_mut("items") {
162 *items = self.get_reduced_resolved_schema(items);
163 }
164 }
165 } else if let Value::Object(def_map) = type_def {
166 def_map.retain(|k, _| allowed_types.contains(&k.as_str()));
167 }
168 }
169 }
170 }
171 reduced
172 }
173
174 pub fn find_nested_object_property_schema<'a>(
175 &self,
176 schema: &'a Value,
177 property_name: &str,
178 ) -> Option<&'a Value> {
179 if let Some(prop) = schema.get("properties").and_then(|p| p.get(property_name)) {
180 return Some(prop);
181 }
182
183 let mut matches: Vec<&'a Value> = Vec::new();
188 let mut unvisited: Vec<&'a Value> = Vec::new();
189 for key in &["oneOf", "anyOf", "allOf"] {
190 if let Some(Value::Array(arr)) = schema.get(*key) {
191 unvisited.extend(arr.iter());
192 }
193 }
194
195 while let Some(sub) = unvisited.pop() {
196 if let Some(prop) = sub.get("properties").and_then(|p| p.get(property_name)) {
197 matches.push(prop);
198 continue;
199 }
200 for key in &["oneOf", "anyOf", "allOf"] {
201 if let Some(Value::Array(arr)) = sub.get(*key) {
202 unvisited.extend(arr.iter());
203 }
204 }
205 }
206
207 let first = matches.first()?;
208 let reduced_first = self.get_reduced_schema(first);
209 for other in matches.iter().skip(1) {
210 if self.get_reduced_schema(other) != reduced_first {
211 return None;
212 }
213 }
214 Some(first)
215 }
216
217 pub fn apply_schema_default_value(
218 &self,
219 source_schema: &Value,
220 resolved_schema: &mut Value,
221 ) -> Result<()> {
222 debug!("Applying schema default values.");
223
224 let default_value = match source_schema.get("default") {
225 Some(v) if !v.is_null() => v.clone(),
226 _ => return Ok(()),
227 };
228
229 let default_value_type = self.get_docs_type_for_value(Some(source_schema), &default_value);
230
231 if resolved_schema
235 .pointer(&format!("/type/{default_value_type}"))
236 .is_none()
237 {
238 anyhow::bail!(
239 "Schema has default value declared that does not match type of resolved schema:\n\
240 Source schema: {}\n\
241 Default value: {} (type: {})\n\
242 Resolved schema: {}",
243 serde_json::to_string_pretty(source_schema)?,
244 serde_json::to_string_pretty(&default_value)?,
245 default_value_type,
246 serde_json::to_string_pretty(resolved_schema)?,
247 );
248 }
249
250 if default_value_type == "object" {
251 let Value::Object(def_obj) = default_value else {
252 anyhow::bail!("Default value typed 'object' was not a JSON object");
253 };
254 let props = resolved_schema
255 .pointer_mut("/type/object/options")
256 .and_then(Value::as_object_mut)
257 .ok_or_else(|| {
258 anyhow::anyhow!("Resolved object schema is missing /type/object/options")
259 })?;
260
261 for (prop_name, prop_default_value) in def_obj {
262 if prop_default_value.is_null() {
263 continue;
264 }
265 let Some(resolved_prop) = props.get_mut(&prop_name) else {
266 continue;
267 };
268
269 if let Some(source_prop) =
270 self.find_nested_object_property_schema(source_schema, &prop_name)
271 {
272 let mut source_with_default = source_prop.clone();
273 source_with_default
274 .as_object_mut()
275 .unwrap()
276 .insert("default".to_string(), prop_default_value);
277 self.apply_schema_default_value(&source_with_default, resolved_prop)?;
278 } else {
279 let value_type = self.get_docs_type_for_value(None, &prop_default_value);
280 if let Some(Value::Object(type_obj)) = resolved_prop.get_mut("type")
281 && let Some(Value::Object(type_def)) = type_obj.get_mut(value_type)
282 {
283 type_def.insert("default".to_string(), prop_default_value);
284 }
285 }
286 resolved_prop
287 .as_object_mut()
288 .unwrap()
289 .insert("required".to_string(), Value::Bool(false));
290 }
291 } else {
292 let type_def = resolved_schema
293 .pointer_mut(&format!("/type/{default_value_type}"))
294 .and_then(Value::as_object_mut)
295 .expect("/type/{default_value_type} existence verified above");
296 type_def.insert("default".to_string(), default_value);
297 }
298 Ok(())
299 }
300
301 pub fn apply_schema_metadata(&self, source_schema: &Value, resolved_schema: &mut Value) {
302 let is_templateable = get_schema_metadata(source_schema, "docs::templateable")
303 .and_then(Value::as_bool)
304 .unwrap_or(false);
305
306 if let Some(Value::Object(types)) = resolved_schema.get_mut("type")
307 && let Some(Value::Object(string_def)) = types.get_mut("string")
308 && is_templateable
309 {
310 string_def.insert("syntax".to_string(), Value::String("template".to_string()));
311 }
312
313 if let Some(examples) = get_schema_metadata(source_schema, "docs::examples") {
314 let mut flattened_examples = match examples {
315 Value::Array(arr) => arr.clone(),
316 v => vec![v.clone()],
317 };
318
319 for ex in &mut flattened_examples {
320 if let Value::Object(obj) = ex {
321 let sorted_obj = Self::sort_hash_nested(obj);
322 *ex = Value::Object(sorted_obj);
323 }
324 }
325
326 if let Some(Value::Object(type_obj)) = resolved_schema.get_mut("type") {
327 for (type_name, def) in type_obj.iter_mut() {
328 if let Value::Object(def_map) = def {
329 if type_name == "array" {
330 if let Some(Value::Object(items_obj)) = def_map.get_mut("items")
331 && let Some(Value::Object(subtypes)) = items_obj.get_mut("type")
332 {
333 for (subtype_name, subtype_def) in subtypes.iter_mut() {
334 if subtype_name != "array"
335 && let Value::Object(s_def) = subtype_def
336 {
337 s_def.insert(
338 "examples".to_string(),
339 Value::Array(flattened_examples.clone()),
340 );
341 }
342 }
343 }
344 } else {
345 def_map.insert(
346 "examples".to_string(),
347 Value::Array(flattened_examples.clone()),
348 );
349 }
350 }
351 }
352 }
353 }
354
355 if let Some(type_unit) = get_schema_metadata(source_schema, "docs::type_unit") {
356 let unit_str = match type_unit {
357 Value::String(s) => s.clone(),
358 v => v.to_string(),
359 };
360 if let Some(schema_type) = self.numeric_schema_type(resolved_schema)
361 && let Some(Value::Object(types)) = resolved_schema.get_mut("type")
362 && let Some(Value::Object(def)) = types.get_mut(schema_type)
363 {
364 def.insert("unit".to_string(), Value::String(unit_str));
365 }
366 }
367
368 if let Some(syntax_override) = get_schema_metadata(source_schema, "docs::syntax_override") {
369 let syntax_str = match syntax_override {
370 Value::String(s) => s.clone(),
371 v => v.to_string(),
372 };
373 if self.resolved_schema_type(resolved_schema) == Some("string")
374 && let Some(Value::Object(types)) = resolved_schema.get_mut("type")
375 && let Some(Value::Object(string_def)) = types.get_mut("string")
376 {
377 string_def.insert("syntax".to_string(), Value::String(syntax_str));
378 }
379 }
380 }
381
382 pub fn sort_hash_nested(
383 input: &serde_json::Map<String, Value>,
384 ) -> serde_json::Map<String, Value> {
385 let mut sorted = serde_json::Map::new();
386 let mut keys: Vec<&String> = input.keys().collect();
387 keys.sort();
388 for key in keys {
389 let val = input.get(key).unwrap();
390 let new_val = if let Value::Object(obj) = val {
391 Value::Object(Self::sort_hash_nested(obj))
392 } else {
393 val.clone()
394 };
395 sorted.insert(key.clone(), new_val);
396 }
397 sorted
398 }
399
400 pub fn apply_object_property_fields(
401 &self,
402 parent_schema: &Value,
403 property_schema: &Value,
404 property_name: &str,
405 property: &mut Value,
406 ) {
407 let required_properties = parent_schema.get("required").and_then(|r| r.as_array());
408
409 let has_self_default_value = property_schema.get("default").is_some_and(|v| !v.is_null());
410 let has_parent_default_value = parent_schema
411 .get("default")
412 .and_then(|d| d.get(property_name))
413 .is_some_and(|v| !v.is_null());
414 let has_default_value = has_self_default_value || has_parent_default_value;
415
416 let is_required = required_properties
417 .is_some_and(|reqs| reqs.contains(&Value::String(property_name.to_string())))
418 || property_schema
419 .get("required")
420 .and_then(Value::as_bool)
421 .unwrap_or(false);
422
423 property.as_object_mut().unwrap().insert(
424 "required".to_string(),
425 Value::Bool(is_required && !has_default_value),
426 );
427 }
428
429 pub fn reconcile_resolved_schema(resolved: &mut Value) {
430 let Some(type_obj) = resolved.get("type").and_then(Value::as_object) else {
431 return;
432 };
433
434 if let Some(options) = type_obj
435 .get("object")
436 .and_then(|o| o.get("options"))
437 .and_then(Value::as_object)
438 {
439 let property_keys: Vec<String> = options.keys().cloned().collect();
440 for key in property_keys {
441 if let Some(prop) = resolved
442 .pointer_mut(&format!("/type/object/options/{key}"))
443 .filter(|v| v.is_object())
444 {
445 Self::reconcile_resolved_schema(prop);
446 }
447 }
448 return;
449 }
450
451 let is_required = resolved
452 .get("required")
453 .and_then(Value::as_bool)
454 .unwrap_or(false);
455 if is_required {
456 let type_field_keys: Vec<String> = type_obj.keys().cloned().collect();
457 for type_field in &type_field_keys {
458 let pointer = format!("/type/{type_field}");
459 if let Some(Value::Object(field)) = resolved.pointer_mut(&pointer)
460 && let Some(Value::Null) = field.get("default")
461 {
462 field.shift_remove("default");
463 }
464 }
465 }
466
467 let schema_description = resolved
468 .get("description")
469 .and_then(Value::as_str)
470 .map(str::to_owned);
471
472 let type_field_keys: Vec<String> = resolved
473 .get("type")
474 .and_then(Value::as_object)
475 .map(|o| o.keys().cloned().collect())
476 .unwrap_or_default();
477
478 for type_field in &type_field_keys {
479 let const_pointer = format!("/type/{type_field}/const");
480 let Some(const_value) = resolved.pointer(&const_pointer).cloned() else {
481 continue;
482 };
483
484 let entries = match &const_value {
485 Value::Array(items) => items
486 .iter()
487 .filter_map(|item| {
488 let key = scalar_const_key(item.get("value")?)?;
489 let desc = item
490 .get("description")
491 .and_then(Value::as_str)
492 .unwrap_or("")
493 .to_string();
494 Some((key, desc))
495 })
496 .collect::<Vec<_>>(),
497 Value::Object(single) => {
498 let Some(key) = single.get("value").and_then(scalar_const_key) else {
499 continue;
500 };
501 let desc = single
502 .get("description")
503 .and_then(Value::as_str)
504 .map(str::to_owned)
505 .or_else(|| schema_description.clone())
506 .unwrap_or_default();
507 vec![(key, desc)]
508 }
509 _ => continue,
510 };
511
512 let mut enum_map = Map::new();
513 for (key, desc) in entries {
514 enum_map.insert(key, Value::String(desc));
515 }
516
517 if let Some(Value::Object(field)) = resolved.pointer_mut(&format!("/type/{type_field}"))
518 {
519 field.shift_remove("const");
520 field.insert("enum".to_string(), Value::Object(enum_map));
521 }
522 }
523 }
524
525 pub fn numeric_schema_type(&self, resolved_schema: &Value) -> Option<&'static str> {
526 let schema_type = self.resolved_schema_type(resolved_schema)?;
527 if matches!(schema_type, "uint" | "int" | "float") {
528 Some(schema_type)
529 } else {
530 None
531 }
532 }
533
534 pub fn resolved_schema_type(&self, resolved_schema: &Value) -> Option<&'static str> {
535 if let Some(Value::Object(types)) = resolved_schema.get("type")
536 && types.len() == 1
537 {
538 let type_name = types.keys().next().unwrap();
539 return match type_name.as_str() {
540 "object" => Some("object"),
541 "array" => Some("array"),
542 "string" => Some("string"),
543 "bool" => Some("bool"),
544 "uint" => Some("uint"),
545 "int" => Some("int"),
546 "float" => Some("float"),
547 "condition" => Some("condition"),
548 "enum" => Some("enum"),
549 "const" => Some("const"),
550 "*" => Some("*"),
551 _ => None,
552 };
553 }
554 None
555 }
556
557 pub fn get_docs_type_for_value(&self, schema: Option<&Value>, value: &Value) -> &'static str {
558 let value_type = super::json_type_str(value);
559 if matches!(value_type, "number" | "integer")
560 && let Some(s) = schema
561 && let Some(numeric_type) =
562 get_schema_metadata(s, "docs::numeric_type").and_then(|n| n.as_str())
563 {
564 return match numeric_type {
565 "uint" => "uint",
566 "int" => "int",
567 "float" => "float",
568 _ => super::docs_type_str(value),
569 };
570 }
571 super::docs_type_str(value)
572 }
573}