Skip to main content

oa_forge_parser/
resolver.rs

1use std::collections::HashSet;
2
3use crate::openapi::{Components, SchemaOrRef};
4
5/// Resolve a $ref path like "#/components/schemas/Pet" to the referenced schema.
6pub fn resolve_ref<'a>(
7    ref_path: &str,
8    components: Option<&'a Components>,
9) -> Option<&'a SchemaOrRef> {
10    let parts: Vec<&str> = ref_path
11        .trim_start_matches('#')
12        .trim_start_matches('/')
13        .split('/')
14        .collect();
15
16    match parts.as_slice() {
17        ["components", "schemas", name] => components?.schemas.get(*name),
18        _ => None,
19    }
20}
21
22/// Detect circular references in a schema graph using DFS.
23pub fn detect_circular_refs(
24    schema: &SchemaOrRef,
25    components: Option<&Components>,
26    visited: &mut HashSet<String>,
27) -> bool {
28    match schema {
29        SchemaOrRef::Ref { ref_path } => {
30            if visited.contains(ref_path) {
31                return true;
32            }
33            visited.insert(ref_path.clone());
34            if let Some(resolved) = resolve_ref(ref_path, components) {
35                let is_circular = detect_circular_refs(resolved, components, visited);
36                visited.remove(ref_path);
37                is_circular
38            } else {
39                visited.remove(ref_path);
40                false
41            }
42        }
43        SchemaOrRef::Schema(schema) => {
44            for prop in schema.properties.values() {
45                if detect_circular_refs(prop, components, visited) {
46                    return true;
47                }
48            }
49            if let Some(items) = &schema.items
50                && detect_circular_refs(items, components, visited)
51            {
52                return true;
53            }
54            if let Some(all_of) = &schema.all_of {
55                for s in all_of {
56                    if detect_circular_refs(s, components, visited) {
57                        return true;
58                    }
59                }
60            }
61            if let Some(one_of) = &schema.one_of {
62                for s in one_of {
63                    if detect_circular_refs(s, components, visited) {
64                        return true;
65                    }
66                }
67            }
68            false
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::parse;
77
78    #[test]
79    fn resolve_simple_ref() {
80        let yaml = r#"
81openapi: "3.0.3"
82info:
83  title: Test
84  version: "1.0.0"
85paths: {}
86components:
87  schemas:
88    Pet:
89      type: object
90      properties:
91        name:
92          type: string
93"#;
94        let spec = parse(yaml).unwrap();
95        let result = resolve_ref("#/components/schemas/Pet", spec.components.as_ref());
96        assert!(result.is_some());
97    }
98
99    #[test]
100    fn detect_self_reference() {
101        let yaml = r##"
102openapi: "3.0.3"
103info:
104  title: Test
105  version: "1.0.0"
106paths: {}
107components:
108  schemas:
109    Node:
110      type: object
111      properties:
112        child:
113          $ref: "#/components/schemas/Node"
114"##;
115        let spec = parse(yaml).unwrap();
116        let schema = spec
117            .components
118            .as_ref()
119            .unwrap()
120            .schemas
121            .get("Node")
122            .unwrap();
123        let mut visited = HashSet::new();
124        assert!(detect_circular_refs(
125            schema,
126            spec.components.as_ref(),
127            &mut visited
128        ));
129    }
130}