greentic-component 0.5.2

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)?;
    Ok(collect_redactions_from_value(&value))
}

pub fn collect_default_annotations(
    schema_json: &str,
) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
    let value: Value = serde_json::from_str(schema_json)?;
    Ok(collect_default_annotations_from_value(&value))
}

pub fn collect_capability_hints(
    schema_json: &str,
) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
    let value: Value = serde_json::from_str(schema_json)?;
    Ok(collect_capability_hints_from_value(&value))
}

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

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

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

        if let Some(Value::Object(pattern_props)) = map.get("patternProperties") {
            for (key, child) in pattern_props {
                let len = path.len();
                path.push_str(".patternProperties[");
                path.push_str(key);
                path.push(']');
                walk_inner(child, path, visitor);
                path.truncate(len);
            }
        }

        if let Some(items) = map.get("items") {
            let len = path.len();
            path.push_str("[*]");
            walk_inner(items, path, visitor);
            path.truncate(len);
        }

        if let Some(Value::Array(all_of)) = map.get("allOf") {
            for (idx, child) in all_of.iter().enumerate() {
                let len = path.len();
                path.push_str(".allOf[");
                path.push_str(&idx.to_string());
                path.push(']');
                walk_inner(child, path, visitor);
                path.truncate(len);
            }
        }

        if let Some(Value::Array(any_of)) = map.get("anyOf") {
            for (idx, child) in any_of.iter().enumerate() {
                let len = path.len();
                path.push_str(".anyOf[");
                path.push_str(&idx.to_string());
                path.push(']');
                walk_inner(child, path, visitor);
                path.truncate(len);
            }
        }

        if let Some(Value::Array(one_of)) = map.get("oneOf") {
            for (idx, child) in one_of.iter().enumerate() {
                let len = path.len();
                path.push_str(".oneOf[");
                path.push_str(&idx.to_string());
                path.push(']');
                walk_inner(child, path, visitor);
                path.truncate(len);
            }
        }
    }
}

pub(crate) fn collect_redactions_from_value(value: &Value) -> Vec<JsonPath> {
    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.to_string()));
        }
    });
    hits
}

pub(crate) fn collect_default_annotations_from_value(value: &Value) -> Vec<(JsonPath, String)> {
    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.to_string()), defaulted.to_string()));
        }
    });
    hits
}

pub(crate) fn collect_redactions_and_defaults_from_value(
    value: &Value,
) -> (Vec<JsonPath>, Vec<(JsonPath, String)>) {
    let mut redactions = Vec::new();
    let mut defaults = Vec::new();
    walk(value, "$", &mut |map, path| {
        if map
            .get("x-redact")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
        {
            redactions.push(JsonPath::new(path.to_string()));
        }
        if let Some(defaulted) = map.get("x-default-applied").and_then(|v| v.as_str()) {
            defaults.push((JsonPath::new(path.to_string()), defaulted.to_string()));
        }
    });
    (redactions, defaults)
}

pub(crate) fn collect_capability_hints_from_value(value: &Value) -> Vec<(JsonPath, String)> {
    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.to_string()), cap.to_string()));
        }
    });
    hits
}

fn push(path: &mut String, segment: &str) {
    path.push('.');
    path.push_str(segment);
}