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 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}