greentic_component/
schema.rs

1use serde_json::Value;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct JsonPath(String);
6
7impl JsonPath {
8    pub fn new(path: impl Into<String>) -> Self {
9        Self(path.into())
10    }
11
12    pub fn as_str(&self) -> &str {
13        &self.0
14    }
15}
16
17impl std::fmt::Display for JsonPath {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.write_str(&self.0)
20    }
21}
22
23#[derive(Debug, Error)]
24pub enum SchemaIntrospectionError {
25    #[error("schema json parse failed: {0}")]
26    Json(#[from] serde_json::Error),
27}
28
29pub fn collect_redactions(schema_json: &str) -> Vec<JsonPath> {
30    try_collect_redactions(schema_json).expect("schema traversal failed")
31}
32
33pub fn try_collect_redactions(
34    schema_json: &str,
35) -> Result<Vec<JsonPath>, SchemaIntrospectionError> {
36    let value: Value = serde_json::from_str(schema_json)?;
37    let mut hits = Vec::new();
38    walk(&value, "$", &mut |map, path| {
39        if map
40            .get("x-redact")
41            .and_then(|v| v.as_bool())
42            .unwrap_or(false)
43        {
44            hits.push(JsonPath::new(path));
45        }
46    });
47    Ok(hits)
48}
49
50pub fn collect_default_annotations(
51    schema_json: &str,
52) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
53    let value: Value = serde_json::from_str(schema_json)?;
54    let mut hits = Vec::new();
55    walk(&value, "$", &mut |map, path| {
56        if let Some(defaulted) = map.get("x-default-applied").and_then(|v| v.as_str()) {
57            hits.push((JsonPath::new(path), defaulted.to_string()));
58        }
59    });
60    Ok(hits)
61}
62
63pub fn collect_capability_hints(
64    schema_json: &str,
65) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
66    let value: Value = serde_json::from_str(schema_json)?;
67    let mut hits = Vec::new();
68    walk(&value, "$", &mut |map, path| {
69        if let Some(cap) = map.get("x-capability").and_then(|v| v.as_str()) {
70            hits.push((JsonPath::new(path), cap.to_string()));
71        }
72    });
73    Ok(hits)
74}
75
76fn walk(
77    value: &Value,
78    path: &str,
79    visitor: &mut dyn FnMut(&serde_json::Map<String, Value>, String),
80) {
81    if let Value::Object(map) = value {
82        visitor(map, path.to_string());
83
84        if let Some(Value::Object(props)) = map.get("properties") {
85            for (key, child) in props {
86                let child_path = push(path, key);
87                walk(child, &child_path, visitor);
88            }
89        }
90
91        if let Some(Value::Object(pattern_props)) = map.get("patternProperties") {
92            for (key, child) in pattern_props {
93                let next = format!("{path}.patternProperties[{key}]");
94                walk(child, &next, visitor);
95            }
96        }
97
98        if let Some(items) = map.get("items") {
99            let next = format!("{path}[*]");
100            walk(items, &next, visitor);
101        }
102
103        if let Some(Value::Array(all_of)) = map.get("allOf") {
104            for (idx, child) in all_of.iter().enumerate() {
105                let next = format!("{path}.allOf[{idx}]");
106                walk(child, &next, visitor);
107            }
108        }
109
110        if let Some(Value::Array(any_of)) = map.get("anyOf") {
111            for (idx, child) in any_of.iter().enumerate() {
112                let next = format!("{path}.anyOf[{idx}]");
113                walk(child, &next, visitor);
114            }
115        }
116
117        if let Some(Value::Array(one_of)) = map.get("oneOf") {
118            for (idx, child) in one_of.iter().enumerate() {
119                let next = format!("{path}.oneOf[{idx}]");
120                walk(child, &next, visitor);
121            }
122        }
123    }
124}
125
126fn push(base: &str, segment: &str) -> String {
127    if base == "$" {
128        format!("$.{segment}")
129    } else {
130        format!("{base}.{segment}")
131    }
132}