greentic_component/
schema.rs1use 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}