greentic-component 0.4.75

High-level component loader and store for Greentic components
Documentation
use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JsonPath(String);

impl JsonPath {
    pub fn new(path: impl Into<String>) -> Self {
        Self(path.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for JsonPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

#[derive(Debug, Error)]
pub enum SchemaIntrospectionError {
    #[error("schema json parse failed: {0}")]
    Json(#[from] serde_json::Error),
}

pub fn collect_redactions(schema_json: &str) -> Vec<JsonPath> {
    try_collect_redactions(schema_json).expect("schema traversal failed")
}

pub fn try_collect_redactions(
    schema_json: &str,
) -> Result<Vec<JsonPath>, SchemaIntrospectionError> {
    let value: Value = serde_json::from_str(schema_json)?;
    let mut hits = Vec::new();
    walk(&value, "$", &mut |map, path| {
        if map
            .get("x-redact")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
        {
            hits.push(JsonPath::new(path));
        }
    });
    Ok(hits)
}

pub fn collect_default_annotations(
    schema_json: &str,
) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
    let value: Value = serde_json::from_str(schema_json)?;
    let mut hits = Vec::new();
    walk(&value, "$", &mut |map, path| {
        if let Some(defaulted) = map.get("x-default-applied").and_then(|v| v.as_str()) {
            hits.push((JsonPath::new(path), defaulted.to_string()));
        }
    });
    Ok(hits)
}

pub fn collect_capability_hints(
    schema_json: &str,
) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
    let value: Value = serde_json::from_str(schema_json)?;
    let mut hits = Vec::new();
    walk(&value, "$", &mut |map, path| {
        if let Some(cap) = map.get("x-capability").and_then(|v| v.as_str()) {
            hits.push((JsonPath::new(path), cap.to_string()));
        }
    });
    Ok(hits)
}

fn walk(
    value: &Value,
    path: &str,
    visitor: &mut dyn FnMut(&serde_json::Map<String, Value>, String),
) {
    if let Value::Object(map) = value {
        visitor(map, path.to_string());

        if let Some(Value::Object(props)) = map.get("properties") {
            for (key, child) in props {
                let child_path = push(path, key);
                walk(child, &child_path, visitor);
            }
        }

        if let Some(Value::Object(pattern_props)) = map.get("patternProperties") {
            for (key, child) in pattern_props {
                let next = format!("{path}.patternProperties[{key}]");
                walk(child, &next, visitor);
            }
        }

        if let Some(items) = map.get("items") {
            let next = format!("{path}[*]");
            walk(items, &next, visitor);
        }

        if let Some(Value::Array(all_of)) = map.get("allOf") {
            for (idx, child) in all_of.iter().enumerate() {
                let next = format!("{path}.allOf[{idx}]");
                walk(child, &next, visitor);
            }
        }

        if let Some(Value::Array(any_of)) = map.get("anyOf") {
            for (idx, child) in any_of.iter().enumerate() {
                let next = format!("{path}.anyOf[{idx}]");
                walk(child, &next, visitor);
            }
        }

        if let Some(Value::Array(one_of)) = map.get("oneOf") {
            for (idx, child) in one_of.iter().enumerate() {
                let next = format!("{path}.oneOf[{idx}]");
                walk(child, &next, visitor);
            }
        }
    }
}

fn push(base: &str, segment: &str) -> String {
    if base == "$" {
        format!("$.{segment}")
    } else {
        format!("{base}.{segment}")
    }
}