Skip to main content

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    Ok(collect_redactions_from_value(&value))
38}
39
40pub fn collect_default_annotations(
41    schema_json: &str,
42) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
43    let value: Value = serde_json::from_str(schema_json)?;
44    Ok(collect_default_annotations_from_value(&value))
45}
46
47pub fn collect_capability_hints(
48    schema_json: &str,
49) -> Result<Vec<(JsonPath, String)>, SchemaIntrospectionError> {
50    let value: Value = serde_json::from_str(schema_json)?;
51    Ok(collect_capability_hints_from_value(&value))
52}
53
54fn walk(value: &Value, path: &str, visitor: &mut dyn FnMut(&serde_json::Map<String, Value>, &str)) {
55    let mut path_buf = path.to_string();
56    walk_inner(value, &mut path_buf, visitor);
57}
58
59fn walk_inner(
60    value: &Value,
61    path: &mut String,
62    visitor: &mut dyn FnMut(&serde_json::Map<String, Value>, &str),
63) {
64    if let Value::Object(map) = value {
65        visitor(map, path);
66
67        if let Some(Value::Object(props)) = map.get("properties") {
68            for (key, child) in props {
69                let len = path.len();
70                push(path, key);
71                walk_inner(child, path, visitor);
72                path.truncate(len);
73            }
74        }
75
76        if let Some(Value::Object(pattern_props)) = map.get("patternProperties") {
77            for (key, child) in pattern_props {
78                let len = path.len();
79                path.push_str(".patternProperties[");
80                path.push_str(key);
81                path.push(']');
82                walk_inner(child, path, visitor);
83                path.truncate(len);
84            }
85        }
86
87        if let Some(items) = map.get("items") {
88            let len = path.len();
89            path.push_str("[*]");
90            walk_inner(items, path, visitor);
91            path.truncate(len);
92        }
93
94        if let Some(Value::Array(all_of)) = map.get("allOf") {
95            for (idx, child) in all_of.iter().enumerate() {
96                let len = path.len();
97                path.push_str(".allOf[");
98                path.push_str(&idx.to_string());
99                path.push(']');
100                walk_inner(child, path, visitor);
101                path.truncate(len);
102            }
103        }
104
105        if let Some(Value::Array(any_of)) = map.get("anyOf") {
106            for (idx, child) in any_of.iter().enumerate() {
107                let len = path.len();
108                path.push_str(".anyOf[");
109                path.push_str(&idx.to_string());
110                path.push(']');
111                walk_inner(child, path, visitor);
112                path.truncate(len);
113            }
114        }
115
116        if let Some(Value::Array(one_of)) = map.get("oneOf") {
117            for (idx, child) in one_of.iter().enumerate() {
118                let len = path.len();
119                path.push_str(".oneOf[");
120                path.push_str(&idx.to_string());
121                path.push(']');
122                walk_inner(child, path, visitor);
123                path.truncate(len);
124            }
125        }
126    }
127}
128
129pub(crate) fn collect_redactions_from_value(value: &Value) -> Vec<JsonPath> {
130    let mut hits = Vec::new();
131    walk(value, "$", &mut |map, path| {
132        if map
133            .get("x-redact")
134            .and_then(|v| v.as_bool())
135            .unwrap_or(false)
136        {
137            hits.push(JsonPath::new(path.to_string()));
138        }
139    });
140    hits
141}
142
143pub(crate) fn collect_default_annotations_from_value(value: &Value) -> Vec<(JsonPath, String)> {
144    let mut hits = Vec::new();
145    walk(value, "$", &mut |map, path| {
146        if let Some(defaulted) = map.get("x-default-applied").and_then(|v| v.as_str()) {
147            hits.push((JsonPath::new(path.to_string()), defaulted.to_string()));
148        }
149    });
150    hits
151}
152
153pub(crate) fn collect_redactions_and_defaults_from_value(
154    value: &Value,
155) -> (Vec<JsonPath>, Vec<(JsonPath, String)>) {
156    let mut redactions = Vec::new();
157    let mut defaults = Vec::new();
158    walk(value, "$", &mut |map, path| {
159        if map
160            .get("x-redact")
161            .and_then(|v| v.as_bool())
162            .unwrap_or(false)
163        {
164            redactions.push(JsonPath::new(path.to_string()));
165        }
166        if let Some(defaulted) = map.get("x-default-applied").and_then(|v| v.as_str()) {
167            defaults.push((JsonPath::new(path.to_string()), defaulted.to_string()));
168        }
169    });
170    (redactions, defaults)
171}
172
173pub(crate) fn collect_capability_hints_from_value(value: &Value) -> Vec<(JsonPath, String)> {
174    let mut hits = Vec::new();
175    walk(value, "$", &mut |map, path| {
176        if let Some(cap) = map.get("x-capability").and_then(|v| v.as_str()) {
177            hits.push((JsonPath::new(path.to_string()), cap.to_string()));
178        }
179    });
180    hits
181}
182
183fn push(path: &mut String, segment: &str) {
184    path.push('.');
185    path.push_str(segment);
186}