1use serde_json::{json, Value};
7use tracing::warn;
8
9fn decode_pointer_token(token: &str) -> std::borrow::Cow<'_, str> {
14 if token.contains('~') {
15 std::borrow::Cow::Owned(token.replace("~1", "/").replace("~0", "~"))
16 } else {
17 std::borrow::Cow::Borrowed(token)
18 }
19}
20
21pub fn resolve_ref(ref_string: &str, openapi_doc: &Value) -> Value {
26 if !ref_string.starts_with("#/") {
27 warn!(
28 ref_string,
29 "resolve_ref: ignoring non-local $ref (must start with '#/')"
30 );
31 return json!({});
32 }
33
34 let parts: Vec<&str> = ref_string[2..].split('/').collect();
35 let mut current = openapi_doc;
36
37 for part in parts {
38 let decoded = decode_pointer_token(part);
39 match current.get(decoded.as_ref()) {
40 Some(next) => current = next,
41 None => {
42 warn!(
43 ref_string,
44 part, "resolve_ref: path segment not found in document"
45 );
46 return json!({});
47 }
48 }
49 }
50
51 if current.is_object() {
52 current.clone()
53 } else {
54 warn!(
55 ref_string,
56 "resolve_ref: resolved value is not an object — returning empty schema"
57 );
58 json!({})
59 }
60}
61
62pub fn resolve_schema(schema: &Value, openapi_doc: Option<&Value>) -> Value {
64 if let (Some(doc), Some(ref_str)) = (openapi_doc, schema.get("$ref").and_then(|v| v.as_str())) {
65 resolve_ref(ref_str, doc)
66 } else {
67 schema.clone()
68 }
69}
70
71pub fn deep_resolve_refs(schema: &Value, openapi_doc: &Value, depth: usize) -> Value {
79 if depth > 16 {
80 warn!(depth, "deep_resolve_refs: depth limit reached — returning schema as-is to prevent infinite recursion");
81 return schema.clone();
82 }
83
84 if let Some(ref_str) = schema.get("$ref").and_then(|v| v.as_str()) {
86 let resolved = resolve_ref(ref_str, openapi_doc);
87 return deep_resolve_refs(&resolved, openapi_doc, depth + 1);
88 }
89
90 let mut result = schema.clone();
91
92 if let Some(obj) = result.as_object_mut() {
93 for key in &["allOf", "anyOf", "oneOf"] {
95 if let Some(Value::Array(items)) = obj.get(*key).cloned() {
96 let resolved: Vec<Value> = items
97 .iter()
98 .map(|item| deep_resolve_refs(item, openapi_doc, depth + 1))
99 .collect();
100 obj.insert(key.to_string(), Value::Array(resolved));
101 }
102 }
103
104 if let Some(items) = obj.get("items").cloned() {
106 if items.is_object() {
107 obj.insert(
108 "items".to_string(),
109 deep_resolve_refs(&items, openapi_doc, depth + 1),
110 );
111 } else if let Value::Array(arr) = items {
112 let resolved: Vec<Value> = arr
113 .iter()
114 .map(|item| deep_resolve_refs(item, openapi_doc, depth + 1))
115 .collect();
116 obj.insert("items".to_string(), Value::Array(resolved));
117 }
118 }
119
120 if let Some(Value::Array(prefix)) = obj.get("prefixItems").cloned() {
122 let resolved: Vec<Value> = prefix
123 .iter()
124 .map(|item| deep_resolve_refs(item, openapi_doc, depth + 1))
125 .collect();
126 obj.insert("prefixItems".to_string(), Value::Array(resolved));
127 }
128
129 if let Some(Value::Object(props)) = obj.get("properties").cloned() {
131 let resolved: serde_json::Map<String, Value> = props
132 .into_iter()
133 .map(|(k, v)| (k, deep_resolve_refs(&v, openapi_doc, depth + 1)))
134 .collect();
135 obj.insert("properties".to_string(), Value::Object(resolved));
136 }
137
138 if let Some(Value::Object(pat_props)) = obj.get("patternProperties").cloned() {
140 let resolved: serde_json::Map<String, Value> = pat_props
141 .into_iter()
142 .map(|(k, v)| (k, deep_resolve_refs(&v, openapi_doc, depth + 1)))
143 .collect();
144 obj.insert("patternProperties".to_string(), Value::Object(resolved));
145 }
146
147 if let Some(add_props) = obj.get("additionalProperties").cloned() {
149 if add_props.is_object() {
150 obj.insert(
151 "additionalProperties".to_string(),
152 deep_resolve_refs(&add_props, openapi_doc, depth + 1),
153 );
154 }
155 }
156
157 for key in &["not", "if", "then", "else"] {
159 if let Some(sub) = obj.get(*key).cloned() {
160 if sub.is_object() {
161 obj.insert(
162 key.to_string(),
163 deep_resolve_refs(&sub, openapi_doc, depth + 1),
164 );
165 }
166 }
167 }
168 }
169
170 result
171}
172
173pub fn extract_input_schema(operation: &Value, openapi_doc: Option<&Value>) -> Value {
178 let mut properties = serde_json::Map::new();
179 let mut required: Vec<Value> = Vec::new();
180
181 if let Some(Value::Array(params)) = operation.get("parameters") {
183 for param in params {
184 let in_value = param.get("in").and_then(|v| v.as_str()).unwrap_or("");
185 if in_value == "query" || in_value == "path" {
186 if let Some(name) = param.get("name").and_then(|v| v.as_str()) {
187 let param_schema = param
188 .get("schema")
189 .cloned()
190 .unwrap_or_else(|| json!({"type": "string"}));
191 let resolved = resolve_schema(¶m_schema, openapi_doc);
192 properties.insert(name.to_string(), resolved);
193
194 if param
195 .get("required")
196 .and_then(|v| v.as_bool())
197 .unwrap_or(false)
198 {
199 required.push(Value::String(name.to_string()));
200 }
201 }
202 }
203 }
204 }
205
206 let body_content = operation
210 .get("requestBody")
211 .and_then(|rb| rb.get("content"));
212 let body_schema_opt = body_content.and_then(|c| c.as_object()).and_then(|m| {
213 m.iter()
214 .find(|(k, _)| {
215 k.starts_with("application/json") || k.as_str() == "application/vnd.api+json"
216 })
217 .and_then(|(_, v)| v.get("schema"))
218 });
219 if let Some(body_schema) = body_schema_opt {
220 let resolved = resolve_schema(body_schema, openapi_doc);
221 if let Some(props) = resolved.get("properties").and_then(|p| p.as_object()) {
222 for (k, v) in props {
223 properties.insert(k.clone(), v.clone());
224 }
225 }
226 if let Some(req) = resolved.get("required").and_then(|r| r.as_array()) {
227 required.extend(req.iter().cloned());
228 }
229 }
230
231 if let Some(doc) = openapi_doc {
233 let resolved_props: serde_json::Map<String, Value> = properties
234 .into_iter()
235 .map(|(k, v)| (k, deep_resolve_refs(&v, doc, 0)))
236 .collect();
237 properties = resolved_props;
238 }
239
240 let mut seen = std::collections::HashSet::new();
242 required.retain(|v| {
243 let key = v.as_str().unwrap_or("").to_string();
244 seen.insert(key)
245 });
246
247 json!({
248 "type": "object",
249 "properties": Value::Object(properties),
250 "required": Value::Array(required),
251 })
252}
253
254pub fn extract_output_schema(operation: &Value, openapi_doc: Option<&Value>) -> Value {
262 let responses = match operation.get("responses") {
263 Some(r) => r,
264 None => return json!({"type": "object", "properties": {}}),
265 };
266
267 let responses_obj = match responses.as_object() {
269 Some(obj) => obj,
270 None => return json!({"type": "object", "properties": {}}),
271 };
272 let mut success_codes: Vec<&str> = responses_obj
273 .keys()
274 .filter_map(|k| {
275 let k_str = k.as_str();
276 if k_str.len() == 3
277 && k_str.starts_with('2')
278 && k_str.chars().skip(1).all(|c| c.is_ascii_digit())
279 {
280 Some(k_str)
281 } else {
282 None
283 }
284 })
285 .collect();
286 success_codes.sort();
287
288 for status_code in &success_codes {
289 let json_content = responses
290 .get(*status_code)
291 .and_then(|r| r.get("content"))
292 .and_then(|c| c.as_object())
293 .and_then(|m| {
294 m.iter()
295 .find(|(k, _)| {
296 k.starts_with("application/json")
297 || k.as_str() == "application/vnd.api+json"
298 })
299 .map(|(_, v)| v)
300 });
301 if let Some(schema) = json_content.and_then(|jc| jc.get("schema")) {
302 let mut resolved = resolve_schema(schema, openapi_doc);
303 if let Some(doc) = openapi_doc {
304 resolved = deep_resolve_refs(&resolved, doc, 0);
305 }
306 return resolved;
307 }
308 }
309
310 json!({"type": "object", "properties": {}})
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_resolve_ref_rfc6901_slash_in_key() {
319 let doc = json!({
321 "schemas/v2": {"type": "string"}
322 });
323 let result = resolve_ref("#/schemas~1v2", &doc);
324 assert_eq!(result["type"], "string");
325 }
326
327 #[test]
328 fn test_resolve_ref_rfc6901_tilde_in_key() {
329 let doc = json!({
331 "a~b": {"type": "number"}
332 });
333 let result = resolve_ref("#/a~0b", &doc);
334 assert_eq!(result["type"], "number");
335 }
336
337 #[test]
338 fn test_resolve_ref_rfc6901_combined_escapes() {
339 let doc = json!({
341 "~1": {"type": "boolean"}
342 });
343 let result = resolve_ref("#/~01", &doc);
344 assert_eq!(result["type"], "boolean");
345 }
346
347 #[test]
348 fn test_deep_resolve_refs_additional_properties() {
349 let doc = json!({
350 "components": {
351 "schemas": {
352 "Tag": {"type": "string"}
353 }
354 }
355 });
356 let schema = json!({
357 "type": "object",
358 "additionalProperties": {"$ref": "#/components/schemas/Tag"}
359 });
360 let result = deep_resolve_refs(&schema, &doc, 0);
361 assert_eq!(result["additionalProperties"]["type"], "string");
362 }
363
364 #[test]
365 fn test_deep_resolve_refs_not_keyword() {
366 let doc = json!({
367 "components": {
368 "schemas": {
369 "Forbidden": {"type": "string"}
370 }
371 }
372 });
373 let schema = json!({
374 "not": {"$ref": "#/components/schemas/Forbidden"}
375 });
376 let result = deep_resolve_refs(&schema, &doc, 0);
377 assert_eq!(result["not"]["type"], "string");
378 }
379
380 #[test]
381 fn test_deep_resolve_refs_if_then_else() {
382 let doc = json!({
383 "components": {
384 "schemas": {
385 "Condition": {"type": "boolean"},
386 "TrueCase": {"type": "string"},
387 "FalseCase": {"type": "number"}
388 }
389 }
390 });
391 let schema = json!({
392 "if": {"$ref": "#/components/schemas/Condition"},
393 "then": {"$ref": "#/components/schemas/TrueCase"},
394 "else": {"$ref": "#/components/schemas/FalseCase"}
395 });
396 let result = deep_resolve_refs(&schema, &doc, 0);
397 assert_eq!(result["if"]["type"], "boolean");
398 assert_eq!(result["then"]["type"], "string");
399 assert_eq!(result["else"]["type"], "number");
400 }
401
402 #[test]
403 fn test_extract_input_schema_deduplicates_required() {
404 let doc = json!({
406 "components": {
407 "schemas": {
408 "Body": {
409 "type": "object",
410 "properties": {"id": {"type": "integer"}},
411 "required": ["id"]
412 }
413 }
414 }
415 });
416 let op = json!({
417 "parameters": [
418 {"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}
419 ],
420 "requestBody": {
421 "content": {
422 "application/json": {
423 "schema": {"$ref": "#/components/schemas/Body"}
424 }
425 }
426 }
427 });
428 let result = extract_input_schema(&op, Some(&doc));
429 let req = result["required"].as_array().unwrap();
430 let id_count = req.iter().filter(|v| v.as_str() == Some("id")).count();
431 assert_eq!(id_count, 1, "required list should deduplicate; got {req:?}");
432 }
433
434 #[test]
435 fn test_resolve_ref_basic() {
436 let doc = json!({
437 "components": {
438 "schemas": {
439 "User": {"type": "object", "properties": {"name": {"type": "string"}}}
440 }
441 }
442 });
443 let result = resolve_ref("#/components/schemas/User", &doc);
444 assert_eq!(result["type"], "object");
445 assert!(result["properties"]["name"].is_object());
446 }
447
448 #[test]
449 fn test_resolve_ref_not_found() {
450 let doc = json!({});
451 let result = resolve_ref("#/components/schemas/Missing", &doc);
452 assert_eq!(result, json!({}));
453 }
454
455 #[test]
456 fn test_resolve_ref_non_hash() {
457 let doc = json!({});
458 let result = resolve_ref("external.json#/foo", &doc);
459 assert_eq!(result, json!({}));
460 }
461
462 #[test]
463 fn test_resolve_schema_with_ref() {
464 let doc = json!({
465 "components": {"schemas": {"Foo": {"type": "string"}}}
466 });
467 let schema = json!({"$ref": "#/components/schemas/Foo"});
468 let result = resolve_schema(&schema, Some(&doc));
469 assert_eq!(result["type"], "string");
470 }
471
472 #[test]
473 fn test_resolve_schema_no_ref() {
474 let schema = json!({"type": "integer"});
475 let result = resolve_schema(&schema, None);
476 assert_eq!(result["type"], "integer");
477 }
478
479 #[test]
480 fn test_extract_input_schema_parameters() {
481 let op = json!({
482 "parameters": [
483 {"name": "user_id", "in": "path", "required": true, "schema": {"type": "integer"}},
484 {"name": "limit", "in": "query", "schema": {"type": "integer"}}
485 ]
486 });
487 let result = extract_input_schema(&op, None);
488 assert!(result["properties"]["user_id"].is_object());
489 assert!(result["properties"]["limit"].is_object());
490 let req = result["required"].as_array().unwrap();
491 assert!(req.contains(&Value::String("user_id".into())));
492 assert!(!req.contains(&Value::String("limit".into())));
493 }
494
495 #[test]
496 fn test_extract_input_schema_request_body() {
497 let op = json!({
498 "requestBody": {
499 "content": {
500 "application/json": {
501 "schema": {
502 "type": "object",
503 "properties": {"title": {"type": "string"}},
504 "required": ["title"]
505 }
506 }
507 }
508 }
509 });
510 let result = extract_input_schema(&op, None);
511 assert_eq!(result["properties"]["title"]["type"], "string");
512 let req = result["required"].as_array().unwrap();
513 assert!(req.contains(&Value::String("title".into())));
514 }
515
516 #[test]
517 fn test_extract_input_schema_with_ref() {
518 let doc = json!({
519 "components": {
520 "schemas": {
521 "TaskInput": {
522 "type": "object",
523 "properties": {"name": {"type": "string"}},
524 "required": ["name"]
525 }
526 }
527 }
528 });
529 let op = json!({
530 "requestBody": {
531 "content": {
532 "application/json": {
533 "schema": {"$ref": "#/components/schemas/TaskInput"}
534 }
535 }
536 }
537 });
538 let result = extract_input_schema(&op, Some(&doc));
539 assert_eq!(result["properties"]["name"]["type"], "string");
540 }
541
542 #[test]
543 fn test_extract_output_schema_200() {
544 let op = json!({
545 "responses": {
546 "200": {
547 "content": {
548 "application/json": {
549 "schema": {"type": "object", "properties": {"id": {"type": "integer"}}}
550 }
551 }
552 }
553 }
554 });
555 let result = extract_output_schema(&op, None);
556 assert_eq!(result["properties"]["id"]["type"], "integer");
557 }
558
559 #[test]
560 fn test_extract_output_schema_fallback() {
561 let op = json!({"responses": {"404": {}}});
562 let result = extract_output_schema(&op, None);
563 assert_eq!(result["type"], "object");
564 }
565
566 #[test]
567 fn test_deep_resolve_nested_ref() {
568 let doc = json!({
569 "components": {
570 "schemas": {
571 "Address": {"type": "object", "properties": {"city": {"type": "string"}}},
572 "User": {
573 "type": "object",
574 "properties": {
575 "address": {"$ref": "#/components/schemas/Address"}
576 }
577 }
578 }
579 }
580 });
581 let schema = json!({"$ref": "#/components/schemas/User"});
582 let result = deep_resolve_refs(&schema, &doc, 0);
583 assert_eq!(
584 result["properties"]["address"]["properties"]["city"]["type"],
585 "string"
586 );
587 }
588
589 #[test]
590 fn test_deep_resolve_depth_limit() {
591 let doc = json!({
593 "components": {
594 "schemas": {
595 "Recursive": {
596 "type": "object",
597 "properties": {
598 "child": {"$ref": "#/components/schemas/Recursive"}
599 }
600 }
601 }
602 }
603 });
604 let schema = json!({"$ref": "#/components/schemas/Recursive"});
605 let _ = deep_resolve_refs(&schema, &doc, 0);
607 }
608
609 #[test]
610 fn test_resolve_ref_to_non_dict() {
611 let doc = json!({
613 "components": {
614 "schemas": {
615 "JustAString": "hello"
616 }
617 }
618 });
619 let result = resolve_ref("#/components/schemas/JustAString", &doc);
620 assert_eq!(result, json!({}));
621
622 let doc2 = json!({
624 "components": {
625 "schemas": {
626 "JustANumber": 42
627 }
628 }
629 });
630 let result2 = resolve_ref("#/components/schemas/JustANumber", &doc2);
631 assert_eq!(result2, json!({}));
632 }
633
634 #[test]
635 fn test_resolve_ref_through_missing_path() {
636 let doc = json!({
638 "components": {}
639 });
640 let result = resolve_ref("#/components/schemas/Missing", &doc);
641 assert_eq!(result, json!({}));
642 }
643
644 #[test]
645 fn test_resolve_schema_no_openapi_doc() {
646 let schema = json!({"$ref": "#/components/schemas/Foo", "type": "string"});
648 let result = resolve_schema(&schema, None);
649 assert_eq!(result, schema);
650 }
651
652 #[test]
653 fn test_deep_resolve_refs_in_allof() {
654 let doc = json!({
655 "components": {
656 "schemas": {
657 "Base": {"type": "object", "properties": {"id": {"type": "integer"}}},
658 "Extra": {"type": "object", "properties": {"tag": {"type": "string"}}}
659 }
660 }
661 });
662 let schema = json!({
663 "allOf": [
664 {"$ref": "#/components/schemas/Base"},
665 {"$ref": "#/components/schemas/Extra"}
666 ]
667 });
668 let result = deep_resolve_refs(&schema, &doc, 0);
669 let all_of = result["allOf"].as_array().unwrap();
670 assert_eq!(all_of[0]["properties"]["id"]["type"], "integer");
671 assert_eq!(all_of[1]["properties"]["tag"]["type"], "string");
672 }
673
674 #[test]
675 fn test_deep_resolve_refs_in_anyof() {
676 let doc = json!({
677 "components": {
678 "schemas": {
679 "Cat": {"type": "object", "properties": {"purrs": {"type": "boolean"}}},
680 "Dog": {"type": "object", "properties": {"barks": {"type": "boolean"}}}
681 }
682 }
683 });
684 let schema = json!({
685 "anyOf": [
686 {"$ref": "#/components/schemas/Cat"},
687 {"$ref": "#/components/schemas/Dog"}
688 ]
689 });
690 let result = deep_resolve_refs(&schema, &doc, 0);
691 let any_of = result["anyOf"].as_array().unwrap();
692 assert_eq!(any_of[0]["properties"]["purrs"]["type"], "boolean");
693 assert_eq!(any_of[1]["properties"]["barks"]["type"], "boolean");
694 }
695
696 #[test]
697 fn test_deep_resolve_refs_in_items() {
698 let doc = json!({
699 "components": {
700 "schemas": {
701 "Item": {"type": "object", "properties": {"name": {"type": "string"}}}
702 }
703 }
704 });
705 let schema = json!({
706 "type": "array",
707 "items": {"$ref": "#/components/schemas/Item"}
708 });
709 let result = deep_resolve_refs(&schema, &doc, 0);
710 assert_eq!(result["items"]["properties"]["name"]["type"], "string");
711 }
712
713 #[test]
714 fn test_deep_resolve_no_mutation() {
715 let doc = json!({
716 "components": {
717 "schemas": {
718 "Addr": {"type": "object", "properties": {"city": {"type": "string"}}}
719 }
720 }
721 });
722 let doc_before = doc.clone();
723 let schema = json!({
724 "type": "object",
725 "properties": {
726 "address": {"$ref": "#/components/schemas/Addr"}
727 }
728 });
729 let _result = deep_resolve_refs(&schema, &doc, 0);
730 assert_eq!(doc, doc_before, "openapi_doc must not be mutated");
731 }
732
733 #[test]
734 fn test_extract_input_schema_empty_operation() {
735 let op = json!({});
736 let result = extract_input_schema(&op, None);
737 assert_eq!(result["type"], "object");
738 assert!(result["properties"].as_object().unwrap().is_empty());
739 assert!(result["required"].as_array().unwrap().is_empty());
740 }
741
742 #[test]
743 fn test_extract_input_schema_ref_in_param() {
744 let doc = json!({
745 "components": {
746 "schemas": {
747 "IdType": {"type": "integer", "format": "int64"}
748 }
749 }
750 });
751 let op = json!({
752 "parameters": [
753 {
754 "name": "user_id",
755 "in": "path",
756 "required": true,
757 "schema": {"$ref": "#/components/schemas/IdType"}
758 }
759 ]
760 });
761 let result = extract_input_schema(&op, Some(&doc));
762 assert_eq!(result["properties"]["user_id"]["type"], "integer");
763 assert_eq!(result["properties"]["user_id"]["format"], "int64");
764 }
765
766 #[test]
767 fn test_extract_input_schema_nested_ref_in_body() {
768 let doc = json!({
769 "components": {
770 "schemas": {
771 "Address": {"type": "object", "properties": {"zip": {"type": "string"}}}
772 }
773 }
774 });
775 let op = json!({
776 "requestBody": {
777 "content": {
778 "application/json": {
779 "schema": {
780 "type": "object",
781 "properties": {
782 "address": {"$ref": "#/components/schemas/Address"}
783 }
784 }
785 }
786 }
787 }
788 });
789 let result = extract_input_schema(&op, Some(&doc));
790 assert_eq!(
791 result["properties"]["address"]["properties"]["zip"]["type"],
792 "string"
793 );
794 }
795
796 #[test]
797 fn test_extract_output_schema_201() {
798 let op = json!({
799 "responses": {
800 "201": {
801 "content": {
802 "application/json": {
803 "schema": {
804 "type": "object",
805 "properties": {"id": {"type": "integer"}}
806 }
807 }
808 }
809 }
810 }
811 });
812 let result = extract_output_schema(&op, None);
813 assert_eq!(result["properties"]["id"]["type"], "integer");
814 }
815
816 #[test]
817 fn test_extract_output_schema_200_preferred() {
818 let op = json!({
819 "responses": {
820 "200": {
821 "content": {
822 "application/json": {
823 "schema": {
824 "type": "object",
825 "properties": {"from200": {"type": "string"}}
826 }
827 }
828 }
829 },
830 "201": {
831 "content": {
832 "application/json": {
833 "schema": {
834 "type": "object",
835 "properties": {"from201": {"type": "string"}}
836 }
837 }
838 }
839 }
840 }
841 });
842 let result = extract_output_schema(&op, None);
843 assert!(
844 result["properties"]
845 .as_object()
846 .unwrap()
847 .contains_key("from200"),
848 "200 should be preferred over 201"
849 );
850 assert!(
851 !result["properties"]
852 .as_object()
853 .unwrap()
854 .contains_key("from201"),
855 "201 should not be used when 200 exists"
856 );
857 }
858
859 #[test]
860 fn test_extract_output_schema_array_with_ref_items() {
861 let doc = json!({
862 "components": {
863 "schemas": {
864 "Item": {"type": "object", "properties": {"name": {"type": "string"}}}
865 }
866 }
867 });
868 let op = json!({
869 "responses": {
870 "200": {
871 "content": {
872 "application/json": {
873 "schema": {
874 "type": "array",
875 "items": {"$ref": "#/components/schemas/Item"}
876 }
877 }
878 }
879 }
880 }
881 });
882 let result = extract_output_schema(&op, Some(&doc));
883 assert_eq!(result["type"], "array");
884 assert_eq!(result["items"]["properties"]["name"]["type"], "string");
885 }
886
887 #[test]
888 fn test_extract_output_schema_allof() {
889 let doc = json!({
890 "components": {
891 "schemas": {
892 "Base": {"type": "object", "properties": {"id": {"type": "integer"}}},
893 "Meta": {"type": "object", "properties": {"created": {"type": "string"}}}
894 }
895 }
896 });
897 let op = json!({
898 "responses": {
899 "200": {
900 "content": {
901 "application/json": {
902 "schema": {
903 "allOf": [
904 {"$ref": "#/components/schemas/Base"},
905 {"$ref": "#/components/schemas/Meta"}
906 ]
907 }
908 }
909 }
910 }
911 }
912 });
913 let result = extract_output_schema(&op, Some(&doc));
914 let all_of = result["allOf"].as_array().unwrap();
915 assert_eq!(all_of[0]["properties"]["id"]["type"], "integer");
916 assert_eq!(all_of[1]["properties"]["created"]["type"], "string");
917 }
918
919 #[test]
920 fn test_extract_output_schema_nested_ref() {
921 let doc = json!({
922 "components": {
923 "schemas": {
924 "Inner": {"type": "object", "properties": {"val": {"type": "number"}}}
925 }
926 }
927 });
928 let op = json!({
929 "responses": {
930 "200": {
931 "content": {
932 "application/json": {
933 "schema": {
934 "type": "object",
935 "properties": {
936 "nested": {"$ref": "#/components/schemas/Inner"}
937 }
938 }
939 }
940 }
941 }
942 }
943 });
944 let result = extract_output_schema(&op, Some(&doc));
945 assert_eq!(
946 result["properties"]["nested"]["properties"]["val"]["type"],
947 "number"
948 );
949 }
950
951 #[test]
952 fn test_extract_output_schema_empty_responses() {
953 let op = json!({"operationId": "noResponses"});
955 let result = extract_output_schema(&op, None);
956 assert_eq!(result["type"], "object");
957 assert!(result["properties"].as_object().unwrap().is_empty());
958 }
959
960 #[test]
961 fn test_extract_output_schema_202() {
962 let op = json!({
965 "responses": {
966 "202": {
967 "content": {
968 "application/json": {
969 "schema": {
970 "type": "object",
971 "properties": {"job_id": {"type": "string"}}
972 }
973 }
974 }
975 }
976 }
977 });
978 let result = extract_output_schema(&op, None);
979 assert_eq!(
980 result["properties"]["job_id"]["type"], "string",
981 "202 response schema should be extracted; got: {result:?}"
982 );
983 }
984
985 #[test]
986 fn test_extract_output_schema_203() {
987 let op = json!({
989 "responses": {
990 "203": {
991 "content": {
992 "application/json": {
993 "schema": {
994 "type": "object",
995 "properties": {"cached": {"type": "boolean"}}
996 }
997 }
998 }
999 }
1000 }
1001 });
1002 let result = extract_output_schema(&op, None);
1003 assert_eq!(
1004 result["properties"]["cached"]["type"], "boolean",
1005 "203 response schema should be extracted; got: {result:?}"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_extract_input_schema_vnd_api_json() {
1011 let op = json!({
1014 "requestBody": {
1015 "content": {
1016 "application/vnd.api+json": {
1017 "schema": {
1018 "type": "object",
1019 "properties": {"data": {"type": "object"}},
1020 "required": ["data"]
1021 }
1022 }
1023 }
1024 }
1025 });
1026 let result = extract_input_schema(&op, None);
1027 assert!(
1028 result["properties"]["data"].is_object(),
1029 "vnd.api+json schema properties should be extracted; got: {result:?}"
1030 );
1031 let req = result["required"].as_array().unwrap();
1032 assert!(
1033 req.contains(&Value::String("data".into())),
1034 "required field from vnd.api+json schema should be present; got: {req:?}"
1035 );
1036 }
1037
1038 #[test]
1039 fn test_extract_input_schema_json_preferred_over_vnd_api_json() {
1040 let op = json!({
1042 "requestBody": {
1043 "content": {
1044 "application/json": {
1045 "schema": {
1046 "type": "object",
1047 "properties": {"from_json": {"type": "string"}}
1048 }
1049 },
1050 "application/vnd.api+json": {
1051 "schema": {
1052 "type": "object",
1053 "properties": {"from_vnd": {"type": "string"}}
1054 }
1055 }
1056 }
1057 }
1058 });
1059 let result = extract_input_schema(&op, None);
1060 assert!(result["properties"]["from_json"].is_object());
1061 assert!(!result["properties"]
1062 .as_object()
1063 .unwrap()
1064 .contains_key("from_vnd"));
1065 }
1066
1067 #[test]
1068 fn test_extract_output_schema_200_preferred_over_202() {
1069 let op = json!({
1071 "responses": {
1072 "200": {
1073 "content": {
1074 "application/json": {
1075 "schema": {
1076 "type": "object",
1077 "properties": {"from200": {"type": "string"}}
1078 }
1079 }
1080 }
1081 },
1082 "202": {
1083 "content": {
1084 "application/json": {
1085 "schema": {
1086 "type": "object",
1087 "properties": {"from202": {"type": "string"}}
1088 }
1089 }
1090 }
1091 }
1092 }
1093 });
1094 let result = extract_output_schema(&op, None);
1095 assert!(result["properties"]
1096 .as_object()
1097 .unwrap()
1098 .contains_key("from200"));
1099 assert!(!result["properties"]
1100 .as_object()
1101 .unwrap()
1102 .contains_key("from202"));
1103 }
1104
1105 #[test]
1106 fn test_deep_resolve_depth_limit_at_exactly_16() {
1107 let doc = json!({
1110 "components": {
1111 "schemas": {
1112 "Leaf": {"type": "string"}
1113 }
1114 }
1115 });
1116 let schema = json!({"$ref": "#/components/schemas/Leaf"});
1117 let at_15 = deep_resolve_refs(&schema, &doc, 15);
1119 assert_eq!(at_15["type"], "string", "depth 15 should resolve the $ref");
1120 let at_16 = deep_resolve_refs(&schema, &doc, 16);
1122 assert_eq!(
1123 at_16["type"], "string",
1124 "depth 16 should resolve the $ref (boundary fix)"
1125 );
1126 let at_17 = deep_resolve_refs(&schema, &doc, 17);
1128 assert!(
1129 at_17.get("$ref").is_some(),
1130 "depth 17 must return schema unchanged"
1131 );
1132 }
1133
1134 #[test]
1135 fn test_extract_output_schema_204() {
1136 let op = json!({
1139 "responses": {
1140 "204": {
1141 "content": {
1142 "application/json": {
1143 "schema": {
1144 "type": "object",
1145 "properties": {"accepted": {"type": "boolean"}}
1146 }
1147 }
1148 }
1149 }
1150 }
1151 });
1152 let result = extract_output_schema(&op, None);
1153 assert_eq!(
1154 result["properties"]["accepted"]["type"], "boolean",
1155 "204 response schema should be extracted; got: {result:?}"
1156 );
1157 }
1158
1159 #[test]
1160 fn test_extract_output_schema_vnd_api_json_output() {
1161 let op = json!({
1164 "responses": {
1165 "200": {
1166 "content": {
1167 "application/vnd.api+json": {
1168 "schema": {
1169 "type": "object",
1170 "properties": {"data": {"type": "object"}}
1171 }
1172 }
1173 }
1174 }
1175 }
1176 });
1177 let result = extract_output_schema(&op, None);
1178 assert!(
1179 result["properties"]["data"].is_object(),
1180 "vnd.api+json output schema properties should be extracted; got: {result:?}"
1181 );
1182 }
1183
1184 #[test]
1185 fn test_extract_output_schema_json_preferred_over_vnd_api_json_output() {
1186 let op = json!({
1188 "responses": {
1189 "200": {
1190 "content": {
1191 "application/json": {
1192 "schema": {
1193 "type": "object",
1194 "properties": {"from_json": {"type": "string"}}
1195 }
1196 },
1197 "application/vnd.api+json": {
1198 "schema": {
1199 "type": "object",
1200 "properties": {"from_vnd": {"type": "string"}}
1201 }
1202 }
1203 }
1204 }
1205 }
1206 });
1207 let result = extract_output_schema(&op, None);
1208 assert!(result["properties"]["from_json"].is_object());
1209 assert!(!result["properties"]
1210 .as_object()
1211 .unwrap()
1212 .contains_key("from_vnd"));
1213 }
1214
1215 #[test]
1216 fn test_deep_resolve_16_levels_of_nesting() {
1217 let mut schemas = serde_json::Map::new();
1224 schemas.insert("Leaf".into(), json!({"type": "string"}));
1225 for i in (0..16usize).rev() {
1227 let target = if i == 15 {
1228 "Leaf".to_string()
1229 } else {
1230 format!("L{}", i + 1)
1231 };
1232 schemas.insert(
1233 format!("L{i}"),
1234 json!({"$ref": format!("#/components/schemas/{target}")}),
1235 );
1236 }
1237 let doc = json!({"components": {"schemas": schemas}});
1238 let schema = json!({"$ref": "#/components/schemas/L0"});
1239 let result = deep_resolve_refs(&schema, &doc, 0);
1240 assert_eq!(
1241 result["type"], "string",
1242 "16-level deep $ref chain should be fully resolved; got: {result:?}"
1243 );
1244 }
1245
1246 #[test]
1247 fn test_extract_input_schema_json_with_charset_param() {
1248 let op = json!({
1251 "requestBody": {
1252 "content": {
1253 "application/json; charset=utf-8": {
1254 "schema": {
1255 "type": "object",
1256 "properties": {"name": {"type": "string"}},
1257 "required": ["name"]
1258 }
1259 }
1260 }
1261 }
1262 });
1263 let result = extract_input_schema(&op, None);
1264 assert!(
1265 result["properties"]["name"].is_object(),
1266 "application/json; charset=utf-8 input schema should be extracted; got: {result:?}"
1267 );
1268 let req = result["required"].as_array().unwrap();
1269 assert!(
1270 req.contains(&serde_json::Value::String("name".into())),
1271 "required field from charset-parameterized content-type should be present"
1272 );
1273 }
1274
1275 #[test]
1276 fn test_extract_output_schema_json_with_charset_param() {
1277 let op = json!({
1279 "responses": {
1280 "200": {
1281 "content": {
1282 "application/json; charset=utf-8": {
1283 "schema": {
1284 "type": "object",
1285 "properties": {"result": {"type": "boolean"}}
1286 }
1287 }
1288 }
1289 }
1290 }
1291 });
1292 let result = extract_output_schema(&op, None);
1293 assert!(
1294 result["properties"]["result"].is_object(),
1295 "application/json; charset=utf-8 output schema should be extracted; got: {result:?}"
1296 );
1297 }
1298}