vdev/commands/build/component_docs/
schema.rs

1#[path = "schema_core.rs"]
2pub mod core;
3#[path = "schema_enum.rs"]
4pub mod r#enum;
5#[path = "schema_resolve.rs"]
6pub mod resolve;
7#[path = "schema_utils.rs"]
8pub mod utils;
9
10use anyhow::{Result, anyhow};
11use indexmap::IndexMap;
12use serde_json::Value;
13use std::env;
14
15pub struct SchemaContext {
16    pub root_schema: Value,
17    pub cue_binary_path: String,
18    pub resolved_schema_cache: IndexMap<String, Value>,
19    pub expanded_schema_cache: IndexMap<String, Value>,
20}
21
22impl SchemaContext {
23    pub fn new(root_schema: Value) -> Result<Self> {
24        let cue_binary_path = find_command_on_path("cue")
25            .ok_or_else(|| anyhow!("Failed to find 'cue' binary on the current path."))?;
26        Ok(Self {
27            root_schema,
28            cue_binary_path,
29            resolved_schema_cache: IndexMap::new(),
30            expanded_schema_cache: IndexMap::new(),
31        })
32    }
33}
34
35pub fn find_command_on_path(command: &str) -> Option<String> {
36    let exts = env::var("PATHEXT")
37        .unwrap_or_else(|_| String::new())
38        .split(';')
39        .map(std::string::ToString::to_string)
40        .collect::<Vec<String>>();
41
42    let path_var = env::var("PATH").unwrap_or_default();
43    let paths = std::env::split_paths(&path_var);
44
45    for path in paths {
46        for ext in &exts {
47            let mut expected = path.join(command);
48            expected.set_extension(ext.replace('.', ""));
49
50            if expected.is_file() {
51                return expected.to_str().map(std::string::ToString::to_string);
52            }
53        }
54    }
55    None
56}
57
58pub fn json_type_str(value: &Value) -> &'static str {
59    match value {
60        Value::String(_) => "string",
61        Value::Number(n) if n.is_f64() => "number",
62        Value::Number(_) => "integer",
63        Value::Bool(_) => "boolean",
64        Value::Array(_) => "array",
65        Value::Object(_) => "object",
66        Value::Null => "null",
67    }
68}
69
70pub fn docs_type_str(value: &Value) -> &'static str {
71    let type_str = json_type_str(value);
72    if type_str == "boolean" {
73        "bool"
74    } else {
75        type_str
76    }
77}
78
79pub fn get_schema_metadata<'a>(schema: &'a Value, key: &str) -> Option<&'a Value> {
80    schema.get("_metadata").and_then(|m| m.get(key))
81}
82
83pub fn get_schema_ref(schema: &Value) -> Option<&str> {
84    schema.get("$ref").and_then(|r| r.as_str())
85}
86
87pub fn nested_merge(base: &mut Value, override_val: &Value) {
88    if override_val.is_null() {
89        return;
90    }
91
92    if base.is_null() {
93        *base = override_val.clone();
94        return;
95    }
96
97    if base.is_object() && override_val.is_object() {
98        let base_obj = base.as_object_mut().unwrap();
99        let over_obj = override_val.as_object().unwrap();
100        for (k, v) in over_obj {
101            let entry = base_obj.entry(k.clone()).or_insert_with(|| Value::Null);
102            nested_merge(entry, v);
103        }
104    } else if base.is_array() && override_val.is_array() {
105        let base_arr = base.as_array_mut().unwrap();
106        let over_arr = override_val.as_array().unwrap();
107        for val in over_arr {
108            if !base_arr.contains(val) {
109                base_arr.push(val.clone());
110            }
111        }
112    } else {
113        *base = override_val.clone();
114    }
115}
116
117pub fn schema_aware_nested_merge(base: &mut Value, override_val: &Value) {
118    if override_val.is_null() {
119        return;
120    }
121
122    if base.is_null() {
123        *base = override_val.clone();
124        return;
125    }
126
127    if base.is_object() && override_val.is_object() {
128        let base_obj = base.as_object_mut().unwrap();
129        let over_obj = override_val.as_object().unwrap();
130
131        for (k, v) in over_obj {
132            if k == "const"
133                && is_const_variant(v)
134                && is_existing_const_variants(base_obj.get("const"))
135            {
136                let base_vals = std::mem::take(base_obj.get_mut("const").unwrap());
137                let mut result = Vec::new();
138
139                push_const_variants(&mut result, base_vals);
140                push_const_variants(&mut result, v.clone());
141
142                base_obj.insert("const".to_string(), Value::Array(result));
143            } else {
144                let entry = base_obj.entry(k.clone()).or_insert_with(|| Value::Null);
145                schema_aware_nested_merge(entry, v);
146            }
147        }
148    } else if base.is_array() && override_val.is_array() {
149        let base_arr = base.as_array_mut().unwrap();
150        let over_arr = override_val.as_array().unwrap();
151        for val in over_arr {
152            if !base_arr.contains(val) {
153                base_arr.push(val.clone());
154            }
155        }
156    } else {
157        *base = override_val.clone();
158    }
159}
160
161/// True for a `{value, ...}` const variant, or an array of such variants.
162fn is_const_variant(value: &Value) -> bool {
163    match value {
164        Value::Object(o) => o.contains_key("value"),
165        Value::Array(arr) => arr.iter().all(is_const_variant),
166        _ => false,
167    }
168}
169
170fn is_existing_const_variants(existing: Option<&Value>) -> bool {
171    existing.is_some_and(is_const_variant)
172}
173
174fn push_const_variants(result: &mut Vec<Value>, value: Value) {
175    match value {
176        Value::Array(arr) => {
177            for item in arr {
178                if !result.contains(&item) {
179                    result.push(item);
180                }
181            }
182        }
183        obj @ Value::Object(_) => {
184            if !result.contains(&obj) {
185                result.push(obj);
186            }
187        }
188        _ => {}
189    }
190}