jsonschema_annotator/schema/
parser.rs1use schemars::Schema;
2use serde_json::Value;
3
4use super::annotation::{Annotation, AnnotationMap};
5use super::refs::resolve_refs;
6
7pub fn extract_annotations(schema: &Schema) -> AnnotationMap {
12 let resolved = resolve_refs(schema);
13 let mut annotations = AnnotationMap::new();
14 let mut path = Vec::new();
15
16 walk_schema(resolved.as_value(), &mut path, &mut annotations);
17
18 annotations
19}
20
21fn walk_schema(value: &Value, current_path: &mut Vec<String>, annotations: &mut AnnotationMap) {
22 let Some(obj) = value.as_object() else {
23 return;
24 };
25
26 let title = obj.get("title").and_then(|v| v.as_str());
28 let desc = obj.get("description").and_then(|v| v.as_str());
29
30 if title.is_some() || desc.is_some() {
31 let mut ann = Annotation::new(current_path.join("."));
32 if let Some(t) = title {
33 ann = ann.with_title(t);
34 }
35 if let Some(d) = desc {
36 ann = ann.with_description(d);
37 }
38 annotations.insert(ann);
39 }
40
41 if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
43 for (key, val) in props {
44 current_path.push(key.clone());
45 walk_schema(val, current_path, annotations);
46 current_path.pop();
47 }
48 }
49
50 if let Some(items) = obj.get("items") {
52 walk_schema(items, current_path, annotations);
53 }
54
55 if let Some(additional) = obj.get("additionalProperties") {
57 if additional.is_object() {
58 walk_schema(additional, current_path, annotations);
59 }
60 }
61
62 for keyword in ["oneOf", "allOf", "anyOf"] {
64 if let Some(schemas) = obj.get(keyword).and_then(|v| v.as_array()) {
65 for schema in schemas {
66 walk_schema(schema, current_path, annotations);
67 }
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use serde_json::json;
76
77 #[test]
78 fn test_extract_simple() {
79 let schema_json = json!({
80 "properties": {
81 "name": {
82 "title": "Name",
83 "description": "User's full name"
84 },
85 "age": {
86 "title": "Age"
87 }
88 }
89 });
90
91 let schema: Schema = schema_json.try_into().unwrap();
92 let annotations = extract_annotations(&schema);
93
94 assert_eq!(annotations.len(), 2);
95
96 let name = annotations.get("name").unwrap();
97 assert_eq!(name.title, Some("Name".to_string()));
98 assert_eq!(name.description, Some("User's full name".to_string()));
99
100 let age = annotations.get("age").unwrap();
101 assert_eq!(age.title, Some("Age".to_string()));
102 assert_eq!(age.description, None);
103 }
104
105 #[test]
106 fn test_extract_nested() {
107 let schema_json = json!({
108 "properties": {
109 "server": {
110 "title": "Server",
111 "description": "Server configuration",
112 "properties": {
113 "host": {
114 "title": "Host",
115 "description": "Server hostname"
116 },
117 "port": {
118 "title": "Port"
119 }
120 }
121 }
122 }
123 });
124
125 let schema: Schema = schema_json.try_into().unwrap();
126 let annotations = extract_annotations(&schema);
127
128 assert_eq!(annotations.len(), 3);
129
130 let server = annotations.get("server").unwrap();
131 assert_eq!(server.title, Some("Server".to_string()));
132
133 let host = annotations.get("server.host").unwrap();
134 assert_eq!(host.title, Some("Host".to_string()));
135 assert_eq!(host.description, Some("Server hostname".to_string()));
136
137 let port = annotations.get("server.port").unwrap();
138 assert_eq!(port.title, Some("Port".to_string()));
139 }
140
141 #[test]
142 fn test_extract_with_refs() {
143 let schema_json = json!({
144 "$defs": {
145 "Address": {
146 "title": "Address",
147 "description": "A physical address",
148 "properties": {
149 "city": {
150 "title": "City"
151 }
152 }
153 }
154 },
155 "properties": {
156 "home": {"$ref": "#/$defs/Address"},
157 "work": {"$ref": "#/$defs/Address"}
158 }
159 });
160
161 let schema: Schema = schema_json.try_into().unwrap();
162 let annotations = extract_annotations(&schema);
163
164 let home = annotations.get("home").unwrap();
166 assert_eq!(home.title, Some("Address".to_string()));
167
168 let home_city = annotations.get("home.city").unwrap();
169 assert_eq!(home_city.title, Some("City".to_string()));
170
171 let work = annotations.get("work").unwrap();
172 assert_eq!(work.title, Some("Address".to_string()));
173 }
174
175 #[test]
176 fn test_extract_root_annotation() {
177 let schema_json = json!({
178 "title": "Config",
179 "description": "Application configuration",
180 "properties": {
181 "debug": {
182 "title": "Debug Mode"
183 }
184 }
185 });
186
187 let schema: Schema = schema_json.try_into().unwrap();
188 let annotations = extract_annotations(&schema);
189
190 let root = annotations.get("").unwrap();
192 assert_eq!(root.title, Some("Config".to_string()));
193 assert_eq!(root.description, Some("Application configuration".to_string()));
194
195 let debug = annotations.get("debug").unwrap();
196 assert_eq!(debug.title, Some("Debug Mode".to_string()));
197 }
198
199 #[test]
200 fn test_extract_no_annotations() {
201 let schema_json = json!({
202 "properties": {
203 "name": {"type": "string"},
204 "age": {"type": "number"}
205 }
206 });
207
208 let schema: Schema = schema_json.try_into().unwrap();
209 let annotations = extract_annotations(&schema);
210
211 assert!(annotations.is_empty());
212 }
213
214 #[test]
215 fn test_extract_array_items() {
216 let schema_json = json!({
217 "properties": {
218 "users": {
219 "title": "Users",
220 "description": "List of users",
221 "items": {
222 "properties": {
223 "name": {
224 "title": "User Name"
225 }
226 }
227 }
228 }
229 }
230 });
231
232 let schema: Schema = schema_json.try_into().unwrap();
233 let annotations = extract_annotations(&schema);
234
235 let users = annotations.get("users").unwrap();
236 assert_eq!(users.title, Some("Users".to_string()));
237
238 let user_name = annotations.get("users.name").unwrap();
240 assert_eq!(user_name.title, Some("User Name".to_string()));
241 }
242
243 #[test]
244 fn test_extract_oneof() {
245 let schema_json = json!({
246 "properties": {
247 "value": {
248 "title": "Value",
249 "oneOf": [
250 {
251 "properties": {
252 "string_val": {
253 "title": "String Value",
254 "description": "A string value"
255 }
256 }
257 },
258 {
259 "properties": {
260 "number_val": {
261 "title": "Number Value"
262 }
263 }
264 }
265 ]
266 }
267 }
268 });
269
270 let schema: Schema = schema_json.try_into().unwrap();
271 let annotations = extract_annotations(&schema);
272
273 let value = annotations.get("value").unwrap();
274 assert_eq!(value.title, Some("Value".to_string()));
275
276 let string_val = annotations.get("value.string_val").unwrap();
277 assert_eq!(string_val.title, Some("String Value".to_string()));
278 assert_eq!(string_val.description, Some("A string value".to_string()));
279
280 let number_val = annotations.get("value.number_val").unwrap();
281 assert_eq!(number_val.title, Some("Number Value".to_string()));
282 }
283
284 #[test]
285 fn test_extract_allof() {
286 let schema_json = json!({
287 "allOf": [
288 {
289 "properties": {
290 "base": {
291 "title": "Base Property"
292 }
293 }
294 },
295 {
296 "properties": {
297 "extended": {
298 "title": "Extended Property"
299 }
300 }
301 }
302 ]
303 });
304
305 let schema: Schema = schema_json.try_into().unwrap();
306 let annotations = extract_annotations(&schema);
307
308 let base = annotations.get("base").unwrap();
309 assert_eq!(base.title, Some("Base Property".to_string()));
310
311 let extended = annotations.get("extended").unwrap();
312 assert_eq!(extended.title, Some("Extended Property".to_string()));
313 }
314
315 #[test]
316 fn test_extract_anyof() {
317 let schema_json = json!({
318 "properties": {
319 "config": {
320 "title": "Config",
321 "anyOf": [
322 {
323 "properties": {
324 "simple": {
325 "title": "Simple Mode"
326 }
327 }
328 },
329 {
330 "properties": {
331 "advanced": {
332 "title": "Advanced Mode",
333 "description": "For power users"
334 }
335 }
336 }
337 ]
338 }
339 }
340 });
341
342 let schema: Schema = schema_json.try_into().unwrap();
343 let annotations = extract_annotations(&schema);
344
345 let config = annotations.get("config").unwrap();
346 assert_eq!(config.title, Some("Config".to_string()));
347
348 let simple = annotations.get("config.simple").unwrap();
349 assert_eq!(simple.title, Some("Simple Mode".to_string()));
350
351 let advanced = annotations.get("config.advanced").unwrap();
352 assert_eq!(advanced.title, Some("Advanced Mode".to_string()));
353 assert_eq!(advanced.description, Some("For power users".to_string()));
354 }
355}