Skip to main content

skiff_cli/openapi/
refs.rs

1//! OpenAPI `$ref` resolution (Python `resolve_refs`).
2
3use serde_json::{Map, Value};
4use std::collections::HashSet;
5
6pub fn resolve_refs(spec: Value) -> Value {
7    let root = spec.clone();
8    resolve_node(spec, &root, &HashSet::new())
9}
10
11fn resolve_node(node: Value, root: &Value, seen: &HashSet<String>) -> Value {
12    match node {
13        Value::Object(map) => {
14            if let Some(Value::String(ref_path)) = map.get("$ref") {
15                if seen.contains(ref_path) {
16                    return Value::Object(map);
17                }
18                if let Some(rest) = ref_path.strip_prefix("#/") {
19                    let mut target = root;
20                    for part in rest.split('/') {
21                        match target.get(part) {
22                            Some(next) => target = next,
23                            None => return Value::Object(map),
24                        }
25                    }
26                    let mut next_seen = seen.clone();
27                    next_seen.insert(ref_path.clone());
28                    return resolve_node(target.clone(), root, &next_seen);
29                }
30                return Value::Object(map);
31            }
32            let mut out = Map::new();
33            for (k, v) in map {
34                out.insert(k, resolve_node(v, root, seen));
35            }
36            Value::Object(out)
37        }
38        Value::Array(arr) => Value::Array(
39            arr.into_iter()
40                .map(|item| resolve_node(item, root, seen))
41                .collect(),
42        ),
43        other => other,
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use serde_json::json;
51
52    #[test]
53    fn simple_ref() {
54        let spec = json!({
55            "components": {
56                "parameters": {
57                    "LimitParam": {
58                        "name": "limit",
59                        "in": "query",
60                        "schema": {"type": "integer"}
61                    }
62                }
63            },
64            "paths": {
65                "/pets": {
66                    "get": {
67                        "parameters": [{"$ref": "#/components/parameters/LimitParam"}]
68                    }
69                }
70            }
71        });
72        let resolved = resolve_refs(spec);
73        let params = &resolved["paths"]["/pets"]["get"]["parameters"];
74        assert_eq!(params.as_array().unwrap().len(), 1);
75        assert_eq!(params[0]["name"], "limit");
76        assert!(params[0].get("$ref").is_none());
77    }
78
79    #[test]
80    fn circular_ref_safe() {
81        let spec = json!({
82            "a": {"$ref": "#/b"},
83            "b": {"$ref": "#/a"},
84        });
85        let resolved = resolve_refs(spec);
86        assert!(resolved["a"].get("$ref").is_some() || resolved["b"].get("$ref").is_some());
87    }
88}