vdev/commands/build/component_docs/
schema_resolve.rs

1use 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); // Returning null to indicate skipped
25        }
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        // Remove description from array items
62        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                            // Match the shape every other enum site emits: keys map to a
283                            // string description (empty when the schema doesn't carry one).
284                            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}