jsonschema_annotator/schema/
parser.rs1use schemars::Schema;
2use serde_json::Value;
3
4use super::annotation::{Annotation, AnnotationMap};
5use super::refs::resolve_refs;
6
7fn format_default_value(value: &Value) -> String {
9 match value {
10 Value::Null => "null".to_string(),
11 Value::Bool(b) => b.to_string(),
12 Value::Number(n) => n.to_string(),
13 Value::String(s) => format!("\"{}\"", s),
14 Value::Array(arr) => {
15 let items: Vec<String> = arr.iter().map(format_default_value).collect();
16 format!("[{}]", items.join(", "))
17 }
18 Value::Object(obj) => {
19 let items: Vec<String> = obj
20 .iter()
21 .map(|(k, v)| format!("{}: {}", k, format_default_value(v)))
22 .collect();
23 format!("{{{}}}", items.join(", "))
24 }
25 }
26}
27
28pub fn extract_annotations(schema: &Schema) -> AnnotationMap {
33 let resolved = resolve_refs(schema);
34 let mut annotations = AnnotationMap::new();
35 let mut path = Vec::new();
36
37 walk_schema(resolved.as_value(), &mut path, &mut annotations);
38
39 annotations
40}
41
42fn walk_schema(value: &Value, current_path: &mut Vec<String>, annotations: &mut AnnotationMap) {
43 let Some(obj) = value.as_object() else {
44 return;
45 };
46
47 let title = obj.get("title").and_then(|v| v.as_str());
49 let desc = obj.get("description").and_then(|v| v.as_str());
50 let default = obj.get("default").map(format_default_value);
51
52 if title.is_some() || desc.is_some() || default.is_some() {
53 let mut ann = Annotation::new(current_path.join("."));
54 if let Some(t) = title {
55 ann = ann.with_title(t);
56 }
57 if let Some(d) = desc {
58 ann = ann.with_description(d);
59 }
60 if let Some(d) = default {
61 ann = ann.with_default(d);
62 }
63 annotations.insert(ann);
64 }
65
66 if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
68 for (key, val) in props {
69 current_path.push(key.clone());
70 walk_schema(val, current_path, annotations);
71 current_path.pop();
72 }
73 }
74
75 if let Some(items) = obj.get("items") {
77 walk_schema(items, current_path, annotations);
78 }
79
80 if let Some(additional) = obj.get("additionalProperties") {
82 if additional.is_object() {
83 walk_schema(additional, current_path, annotations);
84 }
85 }
86
87 for keyword in ["oneOf", "allOf", "anyOf"] {
89 if let Some(schemas) = obj.get(keyword).and_then(|v| v.as_array()) {
90 for schema in schemas {
91 walk_schema(schema, current_path, annotations);
92 }
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use serde_json::json;
101
102 #[test]
103 fn test_extract_simple() {
104 let schema_json = json!({
105 "properties": {
106 "name": {
107 "title": "Name",
108 "description": "User's full name"
109 },
110 "age": {
111 "title": "Age"
112 }
113 }
114 });
115
116 let schema: Schema = schema_json.try_into().unwrap();
117 let annotations = extract_annotations(&schema);
118
119 assert_eq!(annotations.len(), 2);
120
121 let name = annotations.get("name").unwrap();
122 assert_eq!(name.title, Some("Name".to_string()));
123 assert_eq!(name.description, Some("User's full name".to_string()));
124
125 let age = annotations.get("age").unwrap();
126 assert_eq!(age.title, Some("Age".to_string()));
127 assert_eq!(age.description, None);
128 }
129
130 #[test]
131 fn test_extract_nested() {
132 let schema_json = json!({
133 "properties": {
134 "server": {
135 "title": "Server",
136 "description": "Server configuration",
137 "properties": {
138 "host": {
139 "title": "Host",
140 "description": "Server hostname"
141 },
142 "port": {
143 "title": "Port"
144 }
145 }
146 }
147 }
148 });
149
150 let schema: Schema = schema_json.try_into().unwrap();
151 let annotations = extract_annotations(&schema);
152
153 assert_eq!(annotations.len(), 3);
154
155 let server = annotations.get("server").unwrap();
156 assert_eq!(server.title, Some("Server".to_string()));
157
158 let host = annotations.get("server.host").unwrap();
159 assert_eq!(host.title, Some("Host".to_string()));
160 assert_eq!(host.description, Some("Server hostname".to_string()));
161
162 let port = annotations.get("server.port").unwrap();
163 assert_eq!(port.title, Some("Port".to_string()));
164 }
165
166 #[test]
167 fn test_extract_with_refs() {
168 let schema_json = json!({
169 "$defs": {
170 "Address": {
171 "title": "Address",
172 "description": "A physical address",
173 "properties": {
174 "city": {
175 "title": "City"
176 }
177 }
178 }
179 },
180 "properties": {
181 "home": {"$ref": "#/$defs/Address"},
182 "work": {"$ref": "#/$defs/Address"}
183 }
184 });
185
186 let schema: Schema = schema_json.try_into().unwrap();
187 let annotations = extract_annotations(&schema);
188
189 let home = annotations.get("home").unwrap();
191 assert_eq!(home.title, Some("Address".to_string()));
192
193 let home_city = annotations.get("home.city").unwrap();
194 assert_eq!(home_city.title, Some("City".to_string()));
195
196 let work = annotations.get("work").unwrap();
197 assert_eq!(work.title, Some("Address".to_string()));
198 }
199
200 #[test]
201 fn test_extract_root_annotation() {
202 let schema_json = json!({
203 "title": "Config",
204 "description": "Application configuration",
205 "properties": {
206 "debug": {
207 "title": "Debug Mode"
208 }
209 }
210 });
211
212 let schema: Schema = schema_json.try_into().unwrap();
213 let annotations = extract_annotations(&schema);
214
215 let root = annotations.get("").unwrap();
217 assert_eq!(root.title, Some("Config".to_string()));
218 assert_eq!(root.description, Some("Application configuration".to_string()));
219
220 let debug = annotations.get("debug").unwrap();
221 assert_eq!(debug.title, Some("Debug Mode".to_string()));
222 }
223
224 #[test]
225 fn test_extract_no_annotations() {
226 let schema_json = json!({
227 "properties": {
228 "name": {"type": "string"},
229 "age": {"type": "number"}
230 }
231 });
232
233 let schema: Schema = schema_json.try_into().unwrap();
234 let annotations = extract_annotations(&schema);
235
236 assert!(annotations.is_empty());
237 }
238
239 #[test]
240 fn test_extract_array_items() {
241 let schema_json = json!({
242 "properties": {
243 "users": {
244 "title": "Users",
245 "description": "List of users",
246 "items": {
247 "properties": {
248 "name": {
249 "title": "User Name"
250 }
251 }
252 }
253 }
254 }
255 });
256
257 let schema: Schema = schema_json.try_into().unwrap();
258 let annotations = extract_annotations(&schema);
259
260 let users = annotations.get("users").unwrap();
261 assert_eq!(users.title, Some("Users".to_string()));
262
263 let user_name = annotations.get("users.name").unwrap();
265 assert_eq!(user_name.title, Some("User Name".to_string()));
266 }
267
268 #[test]
269 fn test_extract_oneof() {
270 let schema_json = json!({
271 "properties": {
272 "value": {
273 "title": "Value",
274 "oneOf": [
275 {
276 "properties": {
277 "string_val": {
278 "title": "String Value",
279 "description": "A string value"
280 }
281 }
282 },
283 {
284 "properties": {
285 "number_val": {
286 "title": "Number Value"
287 }
288 }
289 }
290 ]
291 }
292 }
293 });
294
295 let schema: Schema = schema_json.try_into().unwrap();
296 let annotations = extract_annotations(&schema);
297
298 let value = annotations.get("value").unwrap();
299 assert_eq!(value.title, Some("Value".to_string()));
300
301 let string_val = annotations.get("value.string_val").unwrap();
302 assert_eq!(string_val.title, Some("String Value".to_string()));
303 assert_eq!(string_val.description, Some("A string value".to_string()));
304
305 let number_val = annotations.get("value.number_val").unwrap();
306 assert_eq!(number_val.title, Some("Number Value".to_string()));
307 }
308
309 #[test]
310 fn test_extract_allof() {
311 let schema_json = json!({
312 "allOf": [
313 {
314 "properties": {
315 "base": {
316 "title": "Base Property"
317 }
318 }
319 },
320 {
321 "properties": {
322 "extended": {
323 "title": "Extended Property"
324 }
325 }
326 }
327 ]
328 });
329
330 let schema: Schema = schema_json.try_into().unwrap();
331 let annotations = extract_annotations(&schema);
332
333 let base = annotations.get("base").unwrap();
334 assert_eq!(base.title, Some("Base Property".to_string()));
335
336 let extended = annotations.get("extended").unwrap();
337 assert_eq!(extended.title, Some("Extended Property".to_string()));
338 }
339
340 #[test]
341 fn test_extract_anyof() {
342 let schema_json = json!({
343 "properties": {
344 "config": {
345 "title": "Config",
346 "anyOf": [
347 {
348 "properties": {
349 "simple": {
350 "title": "Simple Mode"
351 }
352 }
353 },
354 {
355 "properties": {
356 "advanced": {
357 "title": "Advanced Mode",
358 "description": "For power users"
359 }
360 }
361 }
362 ]
363 }
364 }
365 });
366
367 let schema: Schema = schema_json.try_into().unwrap();
368 let annotations = extract_annotations(&schema);
369
370 let config = annotations.get("config").unwrap();
371 assert_eq!(config.title, Some("Config".to_string()));
372
373 let simple = annotations.get("config.simple").unwrap();
374 assert_eq!(simple.title, Some("Simple Mode".to_string()));
375
376 let advanced = annotations.get("config.advanced").unwrap();
377 assert_eq!(advanced.title, Some("Advanced Mode".to_string()));
378 assert_eq!(advanced.description, Some("For power users".to_string()));
379 }
380
381 #[test]
382 fn test_extract_default_values() {
383 let schema_json = json!({
384 "properties": {
385 "port": {
386 "title": "Port",
387 "description": "The port number",
388 "default": 8080
389 },
390 "host": {
391 "title": "Host",
392 "default": "localhost"
393 },
394 "enabled": {
395 "title": "Enabled",
396 "default": true
397 },
398 "tags": {
399 "title": "Tags",
400 "default": ["web", "api"]
401 }
402 }
403 });
404
405 let schema: Schema = schema_json.try_into().unwrap();
406 let annotations = extract_annotations(&schema);
407
408 let port = annotations.get("port").unwrap();
409 assert_eq!(port.title, Some("Port".to_string()));
410 assert_eq!(port.default, Some("8080".to_string()));
411
412 let host = annotations.get("host").unwrap();
413 assert_eq!(host.default, Some("\"localhost\"".to_string()));
414
415 let enabled = annotations.get("enabled").unwrap();
416 assert_eq!(enabled.default, Some("true".to_string()));
417
418 let tags = annotations.get("tags").unwrap();
419 assert_eq!(tags.default, Some("[\"web\", \"api\"]".to_string()));
420 }
421
422 #[test]
423 fn test_extract_only_default() {
424 let schema_json = json!({
426 "properties": {
427 "timeout": {
428 "default": 30
429 }
430 }
431 });
432
433 let schema: Schema = schema_json.try_into().unwrap();
434 let annotations = extract_annotations(&schema);
435
436 let timeout = annotations.get("timeout").unwrap();
437 assert_eq!(timeout.title, None);
438 assert_eq!(timeout.description, None);
439 assert_eq!(timeout.default, Some("30".to_string()));
440 }
441}