vdev/commands/build/component_docs/
schema_resolve.rs1use super::{SchemaContext, docs_type_str, get_schema_metadata, nested_merge};
2use anyhow::{Result, bail};
3use serde_json::{Value, json};
4
5impl SchemaContext {
6 pub fn resolve_schema_by_name(&mut self, schema_name: &str) -> Result<Value> {
7 if let Some(resolved) = self.resolved_schema_cache.get(schema_name) {
8 return Ok(resolved.clone());
9 }
10
11 let schema = self.get_schema_by_name(schema_name)?;
12 let resolved = self.resolve_schema(&schema)?;
13
14 self.resolved_schema_cache
15 .insert(schema_name.to_string(), resolved.clone());
16 Ok(resolved)
17 }
18
19 pub fn resolve_schema(&mut self, schema: &Value) -> Result<Value> {
20 let expanded = self.expand_schema_references(schema)?;
21
22 if get_schema_metadata(&expanded, "docs::hidden").is_some() {
23 debug!("Instructed to skip resolution for the given schema.");
24 return Ok(Value::Null); }
26
27 if let Some(type_override) =
28 get_schema_metadata(&expanded, "docs::type_override").and_then(|t| t.as_str())
29 {
30 let mut resolved = if type_override == "ascii_char" {
31 if let Some(Value::Number(n)) = expanded.get("default") {
32 if let Some(c) = n.as_u64() {
33 #[allow(clippy::cast_possible_truncation)]
34 let c_char = (c as u8) as char;
35 json!({ "type": { type_override: { "default": c_char.to_string() } } })
36 } else {
37 json!({ "type": { type_override: {} } })
38 }
39 } else {
40 json!({ "type": { type_override: {} } })
41 }
42 } else {
43 json!({ "type": { type_override: {} } })
44 };
45
46 let desc = self.get_rendered_description_from_schema(&expanded);
47 if !desc.is_empty() {
48 resolved
49 .as_object_mut()
50 .unwrap()
51 .insert("description".to_string(), Value::String(desc));
52 }
53 return Ok(resolved);
54 }
55
56 let mut resolved = self.resolve_bare_schema(&expanded)?;
57 if resolved.is_null() {
58 return Ok(Value::Null);
59 }
60
61 if let Some(items_schema) = resolved.pointer_mut("/type/array/items")
63 && let Value::Object(obj) = items_schema
64 {
65 obj.shift_remove("description");
66 }
67
68 self.apply_schema_default_value(&expanded, &mut resolved)?;
69 self.apply_schema_metadata(&expanded, &mut resolved);
70
71 let desc = self.get_rendered_description_from_schema(&expanded);
72 if !desc.is_empty() {
73 resolved
74 .as_object_mut()
75 .unwrap()
76 .insert("description".to_string(), Value::String(desc));
77 }
78
79 if expanded
80 .get("deprecated")
81 .and_then(serde_json::Value::as_bool)
82 .unwrap_or(false)
83 {
84 resolved
85 .as_object_mut()
86 .unwrap()
87 .insert("deprecated".to_string(), Value::Bool(true));
88 if let Some(msg) = get_schema_metadata(&expanded, "deprecated_message") {
89 resolved
90 .as_object_mut()
91 .unwrap()
92 .insert("deprecated_message".to_string(), msg.clone());
93 }
94 }
95
96 if let Some(common) = get_schema_metadata(&expanded, "docs::common") {
97 resolved
98 .as_object_mut()
99 .unwrap()
100 .insert("common".to_string(), common.clone());
101 }
102
103 if let Some(req) = get_schema_metadata(&expanded, "docs::required") {
104 resolved
105 .as_object_mut()
106 .unwrap()
107 .insert("required".to_string(), req.clone());
108 }
109
110 if let Some(warnings) = get_schema_metadata(&expanded, "docs::warnings") {
111 let warnings_array = if let Some(arr) = warnings.as_array() {
112 Value::Array(arr.clone())
113 } else {
114 Value::Array(vec![warnings.clone()])
115 };
116 resolved
117 .as_object_mut()
118 .unwrap()
119 .insert("warnings".to_string(), warnings_array);
120 }
121
122 SchemaContext::reconcile_resolved_schema(&mut resolved);
123
124 Ok(resolved)
125 }
126
127 #[allow(clippy::too_many_lines)]
128 pub fn resolve_bare_schema(&mut self, schema: &Value) -> Result<Value> {
129 let schema_type = self.get_json_schema_type(schema);
130
131 let res = match schema_type {
132 Some("all-of") => {
133 debug!("Resolving composite schema.");
134 if let Some(Value::Array(all_of)) = schema.get("allOf") {
135 let mut reduced = Value::Null;
136 for subschema in all_of {
137 let sub_res = self.resolve_schema(subschema)?;
138 if !sub_res.is_null() {
139 nested_merge(&mut reduced, &sub_res);
140 }
141 }
142 if reduced.is_null() {
143 return Ok(Value::Null);
144 }
145 reduced.get("type").cloned().unwrap_or(Value::Null)
146 } else {
147 Value::Null
148 }
149 }
150 Some("one-of" | "any-of") => {
151 debug!("Resolving enum schema.");
152 let mut wrapped = self.resolve_enum_schema(schema)?;
153 wrapped
154 .get_mut("_resolved")
155 .and_then(|r| r.get_mut("type"))
156 .cloned()
157 .unwrap_or(Value::Null)
158 }
159 Some("array") => {
160 debug!("Resolving array schema.");
161 let items_resolved = if let Some(items) = schema.get("items") {
162 self.resolve_schema(items)?
163 } else {
164 Value::Null
165 };
166 json!({ "array": { "items": items_resolved } })
167 }
168 Some("object") => {
169 debug!("Resolving object schema.");
170 let properties = schema.get("properties").and_then(|p| p.as_object());
171 let mut options = serde_json::Map::new();
172
173 if let Some(props) = properties {
174 for (prop_name, prop_schema) in props {
175 debug!("Resolving object property '{}'...", prop_name);
176 let mut resolved_property = self.resolve_schema(prop_schema)?;
177 if !resolved_property.is_null() {
178 self.apply_object_property_fields(
179 schema,
180 prop_schema,
181 prop_name,
182 &mut resolved_property,
183 );
184 options.insert(prop_name.clone(), resolved_property);
185 }
186 }
187 }
188
189 if let Some(addl_props) = schema.get("additionalProperties") {
190 debug!("Handling additional properties.");
191 let Some(sing_desc) =
192 get_schema_metadata(schema, "docs::additional_props_description")
193 else {
194 bail!(
195 "Missing 'docs::additional_props_description' metadata for a wildcard field. Schema: {schema}"
196 );
197 };
198
199 let mut resolved_addl = self.resolve_schema(addl_props)?;
200 if let Value::Object(ref mut map) = resolved_addl {
201 map.insert("required".to_string(), Value::Bool(true));
202 map.insert("description".to_string(), sing_desc.clone());
203 options.insert("*".to_string(), Value::Object(map.clone()));
204 }
205 }
206
207 json!({ "object": { "options": options } })
208 }
209 Some("string") => {
210 debug!("Resolving string schema.");
211 let mut def = json!({});
212 if let Some(d) = schema.get("default")
213 && !d.is_null()
214 {
215 def.as_object_mut()
216 .unwrap()
217 .insert("default".to_string(), d.clone());
218 }
219 json!({ "string": def })
220 }
221 Some("number" | "integer") => {
222 debug!("Resolving number schema.");
223 let num_type = get_schema_metadata(schema, "docs::numeric_type")
224 .and_then(|n| n.as_str())
225 .unwrap_or("number");
226
227 let mut def = json!({});
228 if let Some(d) = schema.get("default")
229 && !d.is_null()
230 {
231 def.as_object_mut()
232 .unwrap()
233 .insert("default".to_string(), d.clone());
234 }
235 json!({ num_type: def })
236 }
237 Some("boolean") => {
238 debug!("Resolving boolean schema.");
239 let mut def = json!({});
240 if let Some(d) = schema.get("default")
241 && !d.is_null()
242 {
243 def.as_object_mut()
244 .unwrap()
245 .insert("default".to_string(), d.clone());
246 }
247 json!({ "bool": def })
248 }
249 Some("const") => {
250 debug!("Resolving const schema.");
251 let const_val = schema.get("const").unwrap();
252 let type_str = self.get_docs_type_for_value(Some(schema), const_val);
253
254 let mut def = json!({ "value": const_val.clone() });
255 let desc = self.get_rendered_description_from_schema(schema);
256 if !desc.is_empty() {
257 def.as_object_mut()
258 .unwrap()
259 .insert("description".to_string(), Value::String(desc));
260 }
261
262 json!({ type_str: { "const": def } })
263 }
264 Some("enum") => {
265 debug!("Resolving enum const schema.");
266 if let Some(Value::Array(enum_vals)) = schema.get("enum") {
267 let mut grouped: indexmap::IndexMap<String, Vec<Value>> =
268 indexmap::IndexMap::new();
269 for val in enum_vals {
270 let t = docs_type_str(val);
271 grouped.entry(t.to_string()).or_default().push(val.clone());
272 }
273 self.fix_grouped_enums_if_numeric(&mut grouped);
274
275 let mut res = serde_json::Map::new();
276 for (k, v) in grouped {
277 let mut enum_map = serde_json::Map::new();
278 for item in v {
279 let key_str = item
280 .as_str()
281 .map_or_else(|| item.to_string(), std::string::ToString::to_string);
282 enum_map.insert(key_str, Value::String(String::new()));
285 }
286 res.insert(k, json!({ "enum": enum_map }));
287 }
288 Value::Object(res)
289 } else {
290 Value::Null
291 }
292 }
293 None => {
294 debug!("Resolving unconstrained schema.");
295 json!({ "*": {} })
296 }
297 _ => bail!("Failed to resolve schema: {schema:?}"),
298 };
299
300 Ok(json!({ "type": res }))
301 }
302}