oa_forge_parser/
resolver.rs1use std::collections::HashSet;
2
3use crate::openapi::{Components, SchemaOrRef};
4
5pub 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
22pub 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 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}