weavatrix-rust 2.16.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use blazingly_json::Value;
use std::collections::BTreeMap;

pub(super) struct SchemaView {
    pub required: Vec<String>,
    pub property_names: Vec<String>,
    pub types: BTreeMap<String, String>,
    pub enums: BTreeMap<String, String>,
    pub bounds: BTreeMap<String, String>,
    pub additional: String,
    pub shape: String,
    pub unsupported: Vec<String>,
    pub unresolved_ref: bool,
}

#[must_use]
pub(super) fn meta_labels(schema: &Value, description: &str) -> Vec<String> {
    let view = inspect(schema);
    let mut labels = vec![
        format!("digest:{}", digest(&view, description)),
        format!("required:{}", view.required.join(",")),
        format!("properties:{}", view.property_names.join(",")),
        format!("shape:{}", view.shape),
        format!("additionalProperties:{}", view.additional),
    ];
    for (name, ty) in &view.types {
        labels.push(format!("type:{name}={ty}"));
    }
    for (name, values) in &view.enums {
        labels.push(format!("enum:{name}={values}"));
    }
    for (name, spec) in &view.bounds {
        labels.push(format!("bound:{name}={spec}"));
    }
    for reason in &view.unsupported {
        labels.push(format!("schema:unsupported:{reason}"));
    }
    if view.unresolved_ref {
        labels.push("schema:unresolved_ref".into());
    }
    labels
}

#[must_use]
pub(super) fn inspect(schema: &Value) -> SchemaView {
    let mut unsupported = Vec::new();
    let unresolved_ref = schema.get("$ref").is_some();
    if unresolved_ref {
        unsupported.push("ref".into());
    }
    mark_keywords(schema, &mut unsupported);
    mark_root_type(schema, &mut unsupported);
    let additional = additional_of(schema, &mut unsupported);
    let required = required_names(schema);
    let mut types = BTreeMap::new();
    let mut enums = BTreeMap::new();
    let mut bounds = BTreeMap::new();
    let mut property_names = Vec::new();
    if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
        for (name, spec) in properties {
            property_names.push(name.clone());
            inspect_property(
                name,
                spec,
                &mut types,
                &mut enums,
                &mut bounds,
                &mut unsupported,
            );
        }
    }
    property_names.sort();
    unsupported.sort();
    unsupported.dedup();
    let shape = canonical(&required, &types, &enums, &bounds, &additional);
    SchemaView {
        required,
        property_names,
        types,
        enums,
        bounds,
        additional,
        shape,
        unsupported,
        unresolved_ref,
    }
}

fn mark_keywords(schema: &Value, unsupported: &mut Vec<String>) {
    for key in [
        "anyOf",
        "oneOf",
        "allOf",
        "not",
        "if",
        "then",
        "else",
        "$defs",
        "definitions",
        "patternProperties",
        "dependentSchemas",
        "unevaluatedProperties",
        "prefixItems",
        "const",
        "pattern",
    ] {
        if schema.get(key).is_some() {
            unsupported.push(key.to_owned());
        }
    }
}

fn mark_root_type(schema: &Value, unsupported: &mut Vec<String>) {
    match schema.get("type") {
        Some(Value::String(ty)) if ty == "object" => {}
        Some(Value::Array(_)) => unsupported.push("type_union".into()),
        None if schema.get("properties").is_some() => {}
        None => unsupported.push("missing_type".into()),
        _ => unsupported.push("root_type".into()),
    }
}

fn additional_of(schema: &Value, unsupported: &mut Vec<String>) -> String {
    match schema.get("additionalProperties") {
        None => "absent".into(),
        Some(Value::Bool(value)) => value.to_string(),
        Some(Value::Object(_)) => {
            unsupported.push("additionalProperties_schema".into());
            "schema".into()
        }
        Some(_) => {
            unsupported.push("additionalProperties".into());
            "unknown".into()
        }
    }
}

fn inspect_property(
    name: &str,
    spec: &Value,
    types: &mut BTreeMap<String, String>,
    enums: &mut BTreeMap<String, String>,
    bounds: &mut BTreeMap<String, String>,
    unsupported: &mut Vec<String>,
) {
    if spec.get("$ref").is_some() {
        unsupported.push(format!("prop_ref:{name}"));
    }
    match spec.get("type") {
        Some(Value::String(ty))
            if matches!(
                ty.as_str(),
                "string" | "integer" | "number" | "boolean" | "null"
            ) =>
        {
            types.insert(name.to_owned(), ty.clone());
        }
        Some(Value::String(ty)) => {
            types.insert(name.to_owned(), ty.clone());
            unsupported.push(format!("nested:{name}"));
        }
        Some(Value::Array(_)) => {
            types.insert(name.to_owned(), "union".into());
            unsupported.push(format!("type_union:{name}"));
        }
        _ => {
            types.insert(name.to_owned(), "unknown".into());
            unsupported.push(format!("untyped:{name}"));
        }
    }
    if let Some(values) = spec.get("enum").and_then(Value::as_array) {
        if values.iter().all(|item| {
            item.as_str().is_some()
                || item.as_i64().is_some()
                || item.as_f64().is_some()
                || item.as_bool().is_some()
        }) {
            let mut rendered = values.iter().filter_map(render_enum).collect::<Vec<_>>();
            rendered.sort();
            enums.insert(name.to_owned(), rendered.join(","));
        } else {
            unsupported.push(format!("enum:{name}"));
        }
    }
    if let Some(bound) = bound_spec(spec) {
        bounds.insert(name.to_owned(), bound);
    }
}

fn required_names(schema: &Value) -> Vec<String> {
    let mut names = schema
        .get("required")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(Value::as_str)
        .map(ToOwned::to_owned)
        .collect::<Vec<_>>();
    names.sort();
    names
}

fn bound_spec(spec: &Value) -> Option<String> {
    let min = spec
        .get("minimum")
        .or_else(|| spec.get("minLength"))
        .or_else(|| spec.get("exclusiveMinimum"));
    let max = spec
        .get("maximum")
        .or_else(|| spec.get("maxLength"))
        .or_else(|| spec.get("exclusiveMaximum"));
    if min.is_none() && max.is_none() {
        return None;
    }
    Some(format!(
        "min:{},max:{}",
        min.map_or_else(|| "none".into(), render_number),
        max.map_or_else(|| "none".into(), render_number)
    ))
}

fn render_enum(value: &Value) -> Option<String> {
    value
        .as_str()
        .map(ToOwned::to_owned)
        .or_else(|| value.as_i64().map(|item| item.to_string()))
        .or_else(|| value.as_bool().map(|item| item.to_string()))
        .or_else(|| value.as_f64().map(|item| item.to_string()))
}

fn render_number(value: &Value) -> String {
    value
        .as_i64()
        .map(|item| item.to_string())
        .or_else(|| value.as_f64().map(|item| item.to_string()))
        .or_else(|| value.as_str().map(ToOwned::to_owned))
        .unwrap_or_else(|| "unknown".into())
}

fn digest(view: &SchemaView, description: &str) -> String {
    format!(
        "req={};props={};desc={}",
        view.required.join(","),
        view.property_names.join(","),
        description
    )
}

fn canonical(
    required: &[String],
    types: &BTreeMap<String, String>,
    enums: &BTreeMap<String, String>,
    bounds: &BTreeMap<String, String>,
    additional: &str,
) -> String {
    let props = types
        .iter()
        .map(|(name, ty)| format!("{name}:{ty}"))
        .collect::<Vec<_>>()
        .join(",");
    let enums = enums
        .iter()
        .map(|(name, values)| format!("{name}:{values}"))
        .collect::<Vec<_>>()
        .join(";");
    let bounds = bounds
        .iter()
        .map(|(name, spec)| format!("{name}:{spec}"))
        .collect::<Vec<_>>()
        .join(";");
    format!(
        "add={additional};req={};p={props};e={enums};b={bounds}",
        required.join(",")
    )
}