use std::collections::BTreeMap;
use serde_json::Value;
pub(super) struct SectionDoc {
pub description: Option<String>,
pub deprecated: bool,
pub fields: BTreeMap<String, FieldDoc>,
}
pub(super) struct FieldDoc {
pub description: Option<String>,
pub enum_values: Vec<String>,
pub deprecated: bool,
pub is_table: bool,
pub is_array: bool,
}
pub(super) struct SchemaIndex {
sections: BTreeMap<String, SectionDoc>,
}
impl SchemaIndex {
pub(super) fn build() -> Self {
let schema = crate::cmd::config_schema().unwrap_or(Value::Null);
let defs = schema.get("$defs");
let mut sections = BTreeMap::new();
if let Some(props) = schema.get("properties").and_then(Value::as_object) {
for (name, section) in props {
let description = string_field(section, "description");
let def = section
.get("$ref")
.and_then(Value::as_str)
.and_then(|r| r.strip_prefix("#/$defs/"))
.and_then(|def_name| defs.and_then(|d| d.get(def_name)));
let fields = def.map(field_docs).unwrap_or_default();
let deprecated = [Some(section), def]
.into_iter()
.flatten()
.any(is_deprecated);
sections.insert(
name.clone(),
SectionDoc {
description,
deprecated,
fields,
},
);
}
}
Self { sections }
}
pub(super) fn section(&self, name: &str) -> Option<&SectionDoc> {
self.sections.get(name)
}
pub(super) fn header_paths(&self) -> Vec<String> {
let mut paths: Vec<String> = Vec::new();
for (name, doc) in &self.sections {
paths.push(name.clone());
for (field, field_doc) in &doc.fields {
if field_doc.is_table {
paths.push(format!("{name}.{field}"));
}
}
}
paths.sort();
paths
}
}
fn field_docs(def: &Value) -> BTreeMap<String, FieldDoc> {
let Some(props) = def.get("properties").and_then(Value::as_object) else {
return BTreeMap::new();
};
props
.iter()
.map(|(field, schema)| {
let enum_values = schema
.get("enum")
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
(
field.clone(),
FieldDoc {
description: string_field(schema, "description"),
enum_values,
deprecated: is_deprecated(schema),
is_table: has_type(schema, "object"),
is_array: has_type(schema, "array"),
},
)
})
.collect()
}
fn has_type(schema: &Value, wanted: &str) -> bool {
match schema.get("type") {
Some(Value::String(s)) => s == wanted,
Some(Value::Array(types)) => types.iter().any(|t| t.as_str() == Some(wanted)),
_ => false,
}
}
fn is_deprecated(schema: &Value) -> bool {
schema.get("deprecated").and_then(Value::as_bool) == Some(true)
}
fn string_field(value: &Value, key: &str) -> Option<String> {
value.get(key).and_then(Value::as_str).map(str::to_string)
}