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 resolve_non_component_ref_returns_none() {
101        let yaml = r#"
102openapi: "3.0.3"
103info:
104  title: Test
105  version: "1.0.0"
106paths: {}
107components:
108  schemas:
109    Pet:
110      type: object
111      properties:
112        name:
113          type: string
114"#;
115        let spec = parse(yaml).unwrap();
116        // Non-standard $ref path should return None
117        assert!(resolve_ref("#/paths/get", spec.components.as_ref()).is_none());
118        assert!(resolve_ref("#/definitions/Pet", spec.components.as_ref()).is_none());
119        assert!(resolve_ref("", spec.components.as_ref()).is_none());
120    }
121
122    #[test]
123    fn detect_circular_ref_through_array_items() {
124        let yaml = r##"
125openapi: "3.0.3"
126info:
127  title: Test
128  version: "1.0.0"
129paths: {}
130components:
131  schemas:
132    TreeNode:
133      type: object
134      properties:
135        value:
136          type: string
137        children:
138          type: array
139          items:
140            $ref: "#/components/schemas/TreeNode"
141"##;
142        let spec = parse(yaml).unwrap();
143        let schema = spec
144            .components
145            .as_ref()
146            .unwrap()
147            .schemas
148            .get("TreeNode")
149            .unwrap();
150        let mut visited = HashSet::new();
151        assert!(detect_circular_refs(
152            schema,
153            spec.components.as_ref(),
154            &mut visited
155        ));
156    }
157
158    #[test]
159    fn detect_circular_ref_through_allof() {
160        let yaml = r##"
161openapi: "3.0.3"
162info:
163  title: Test
164  version: "1.0.0"
165paths: {}
166components:
167  schemas:
168    Base:
169      allOf:
170        - type: object
171          properties:
172            name:
173              type: string
174        - $ref: "#/components/schemas/Base"
175"##;
176        let spec = parse(yaml).unwrap();
177        let schema = spec
178            .components
179            .as_ref()
180            .unwrap()
181            .schemas
182            .get("Base")
183            .unwrap();
184        let mut visited = HashSet::new();
185        assert!(detect_circular_refs(
186            schema,
187            spec.components.as_ref(),
188            &mut visited
189        ));
190    }
191
192    #[test]
193    fn detect_circular_ref_through_oneof() {
194        let yaml = r##"
195openapi: "3.0.3"
196info:
197  title: Test
198  version: "1.0.0"
199paths: {}
200components:
201  schemas:
202    Expression:
203      oneOf:
204        - type: object
205          properties:
206            value:
207              type: number
208        - $ref: "#/components/schemas/Expression"
209"##;
210        let spec = parse(yaml).unwrap();
211        let schema = spec
212            .components
213            .as_ref()
214            .unwrap()
215            .schemas
216            .get("Expression")
217            .unwrap();
218        let mut visited = HashSet::new();
219        assert!(detect_circular_refs(
220            schema,
221            spec.components.as_ref(),
222            &mut visited
223        ));
224    }
225
226    #[test]
227    fn non_circular_ref_returns_false() {
228        let yaml = r##"
229openapi: "3.0.3"
230info:
231  title: Test
232  version: "1.0.0"
233paths: {}
234components:
235  schemas:
236    Owner:
237      type: object
238      properties:
239        name:
240          type: string
241    Pet:
242      type: object
243      properties:
244        owner:
245          $ref: "#/components/schemas/Owner"
246"##;
247        let spec = parse(yaml).unwrap();
248        let schema = spec
249            .components
250            .as_ref()
251            .unwrap()
252            .schemas
253            .get("Pet")
254            .unwrap();
255        let mut visited = HashSet::new();
256        assert!(!detect_circular_refs(
257            schema,
258            spec.components.as_ref(),
259            &mut visited
260        ));
261    }
262
263    #[test]
264    fn unresolvable_ref_returns_false() {
265        let yaml = r##"
266openapi: "3.0.3"
267info:
268  title: Test
269  version: "1.0.0"
270paths: {}
271components:
272  schemas:
273    Broken:
274      type: object
275      properties:
276        missing:
277          $ref: "#/components/schemas/DoesNotExist"
278"##;
279        let spec = parse(yaml).unwrap();
280        let schema = spec
281            .components
282            .as_ref()
283            .unwrap()
284            .schemas
285            .get("Broken")
286            .unwrap();
287        let mut visited = HashSet::new();
288        assert!(!detect_circular_refs(
289            schema,
290            spec.components.as_ref(),
291            &mut visited
292        ));
293    }
294
295    #[test]
296    fn detect_self_reference() {
297        let yaml = r##"
298openapi: "3.0.3"
299info:
300  title: Test
301  version: "1.0.0"
302paths: {}
303components:
304  schemas:
305    Node:
306      type: object
307      properties:
308        child:
309          $ref: "#/components/schemas/Node"
310"##;
311        let spec = parse(yaml).unwrap();
312        let schema = spec
313            .components
314            .as_ref()
315            .unwrap()
316            .schemas
317            .get("Node")
318            .unwrap();
319        let mut visited = HashSet::new();
320        assert!(detect_circular_refs(
321            schema,
322            spec.components.as_ref(),
323            &mut visited
324        ));
325    }
326}