Skip to main content

oag_core/parse/
ref_resolve.rs

1use std::collections::HashSet;
2
3use indexmap::IndexMap;
4
5use super::components::Components;
6use super::media_type::MediaType;
7use super::operation::{Operation, PathItem};
8use super::parameter::{Parameter, ParameterOrRef};
9use super::request_body::{RequestBody, RequestBodyOrRef};
10use super::response::{Response, ResponseOrRef};
11use super::schema::{Schema, SchemaOrRef};
12use super::spec::OpenApiSpec;
13use crate::error::ResolveError;
14
15/// Resolves all `$ref` pointers in an OpenAPI spec, producing a spec
16/// with no remaining references. Detects circular references.
17pub struct RefResolver<'a> {
18    components: Option<&'a Components>,
19    visited: HashSet<String>,
20}
21
22impl<'a> RefResolver<'a> {
23    pub fn new(spec: &'a OpenApiSpec) -> Self {
24        Self {
25            components: spec.components.as_ref(),
26            visited: HashSet::new(),
27        }
28    }
29
30    /// Resolve the entire spec in place, returning a spec with no `$ref` nodes.
31    pub fn resolve_spec(&mut self, spec: &OpenApiSpec) -> Result<OpenApiSpec, ResolveError> {
32        let mut resolved = spec.clone();
33
34        // Resolve all paths
35        for (_path, item) in &mut resolved.paths {
36            self.resolve_path_item(item)?;
37        }
38
39        // Resolve component schemas
40        if let Some(ref mut components) = resolved.components {
41            let schema_names: Vec<String> = components.schemas.keys().cloned().collect();
42            for name in schema_names {
43                let schema = components.schemas.get(&name).unwrap().clone();
44                let resolved_schema = self.resolve_schema_or_ref(&schema)?;
45                components.schemas.insert(name, resolved_schema);
46            }
47        }
48
49        Ok(resolved)
50    }
51
52    fn resolve_path_item(&mut self, item: &mut PathItem) -> Result<(), ResolveError> {
53        // Resolve path-level parameters
54        let mut resolved_params = Vec::new();
55        for p in &item.parameters {
56            resolved_params.push(self.resolve_parameter_or_ref(p)?);
57        }
58        item.parameters = resolved_params;
59
60        // Resolve each operation
61        macro_rules! resolve_op {
62            ($op:expr) => {
63                if let Some(ref mut op) = $op {
64                    self.resolve_operation(op)?;
65                }
66            };
67        }
68        resolve_op!(item.get);
69        resolve_op!(item.post);
70        resolve_op!(item.put);
71        resolve_op!(item.delete);
72        resolve_op!(item.patch);
73        resolve_op!(item.options);
74        resolve_op!(item.head);
75        resolve_op!(item.trace);
76        Ok(())
77    }
78
79    fn resolve_operation(&mut self, op: &mut Operation) -> Result<(), ResolveError> {
80        // Resolve parameters
81        let mut resolved_params = Vec::new();
82        for p in &op.parameters {
83            resolved_params.push(self.resolve_parameter_or_ref(p)?);
84        }
85        op.parameters = resolved_params;
86
87        // Resolve request body
88        if let Some(ref body) = op.request_body {
89            let resolved = self.resolve_request_body_or_ref(body)?;
90            op.request_body = Some(resolved);
91        }
92
93        // Resolve responses
94        let mut resolved_responses = IndexMap::new();
95        for (status, resp) in &op.responses {
96            resolved_responses.insert(status.clone(), self.resolve_response_or_ref(resp)?);
97        }
98        op.responses = resolved_responses;
99
100        Ok(())
101    }
102
103    pub fn resolve_schema_or_ref(
104        &mut self,
105        schema_or_ref: &SchemaOrRef,
106    ) -> Result<SchemaOrRef, ResolveError> {
107        match schema_or_ref {
108            SchemaOrRef::Ref { ref_path } => {
109                if self.visited.contains(ref_path) {
110                    // Circular reference — return as-is to avoid infinite loop.
111                    // The IR transform layer handles these.
112                    return Ok(schema_or_ref.clone());
113                }
114                self.visited.insert(ref_path.clone());
115                let resolved = self.lookup_schema(ref_path)?;
116                let result =
117                    self.resolve_schema_or_ref(&SchemaOrRef::Schema(Box::new(resolved)))?;
118                self.visited.remove(ref_path);
119                Ok(result)
120            }
121            SchemaOrRef::Schema(schema) => {
122                let resolved = self.resolve_schema(schema)?;
123                Ok(SchemaOrRef::Schema(Box::new(resolved)))
124            }
125        }
126    }
127
128    fn resolve_schema(&mut self, schema: &Schema) -> Result<Schema, ResolveError> {
129        let mut resolved = schema.clone();
130
131        // Resolve properties
132        let mut resolved_props = IndexMap::new();
133        for (name, prop) in &schema.properties {
134            resolved_props.insert(name.clone(), self.resolve_schema_or_ref(prop)?);
135        }
136        resolved.properties = resolved_props;
137
138        // Resolve items
139        if let Some(ref items) = schema.items {
140            resolved.items = Some(Box::new(self.resolve_schema_or_ref(items)?));
141        }
142
143        // Resolve allOf, oneOf, anyOf
144        resolved.all_of = schema
145            .all_of
146            .iter()
147            .map(|s| self.resolve_schema_or_ref(s))
148            .collect::<Result<Vec<_>, _>>()?;
149        resolved.one_of = schema
150            .one_of
151            .iter()
152            .map(|s| self.resolve_schema_or_ref(s))
153            .collect::<Result<Vec<_>, _>>()?;
154        resolved.any_of = schema
155            .any_of
156            .iter()
157            .map(|s| self.resolve_schema_or_ref(s))
158            .collect::<Result<Vec<_>, _>>()?;
159
160        // Resolve additionalProperties
161        if let Some(super::schema::AdditionalProperties::Schema(ref s)) =
162            schema.additional_properties
163        {
164            resolved.additional_properties = Some(super::schema::AdditionalProperties::Schema(
165                Box::new(self.resolve_schema_or_ref(s)?),
166            ));
167        }
168
169        Ok(resolved)
170    }
171
172    fn resolve_parameter_or_ref(
173        &mut self,
174        param: &ParameterOrRef,
175    ) -> Result<ParameterOrRef, ResolveError> {
176        match param {
177            ParameterOrRef::Ref { ref_path } => {
178                let resolved = self.lookup_parameter(ref_path)?;
179                Ok(ParameterOrRef::Parameter(resolved))
180            }
181            ParameterOrRef::Parameter(p) => {
182                let mut resolved = p.clone();
183                if let Some(ref s) = p.schema {
184                    resolved.schema = Some(self.resolve_schema_or_ref(s)?);
185                }
186                Ok(ParameterOrRef::Parameter(resolved))
187            }
188        }
189    }
190
191    fn resolve_request_body_or_ref(
192        &mut self,
193        body: &RequestBodyOrRef,
194    ) -> Result<RequestBodyOrRef, ResolveError> {
195        match body {
196            RequestBodyOrRef::Ref { ref_path } => {
197                let resolved = self.lookup_request_body(ref_path)?;
198                let mut rb = resolved;
199                self.resolve_media_types(&mut rb.content)?;
200                Ok(RequestBodyOrRef::RequestBody(rb))
201            }
202            RequestBodyOrRef::RequestBody(rb) => {
203                let mut resolved = rb.clone();
204                self.resolve_media_types(&mut resolved.content)?;
205                Ok(RequestBodyOrRef::RequestBody(resolved))
206            }
207        }
208    }
209
210    fn resolve_response_or_ref(
211        &mut self,
212        resp: &ResponseOrRef,
213    ) -> Result<ResponseOrRef, ResolveError> {
214        match resp {
215            ResponseOrRef::Ref { ref_path } => {
216                let resolved = self.lookup_response(ref_path)?;
217                let mut r = resolved;
218                self.resolve_media_types(&mut r.content)?;
219                Ok(ResponseOrRef::Response(r))
220            }
221            ResponseOrRef::Response(r) => {
222                let mut resolved = r.clone();
223                self.resolve_media_types(&mut resolved.content)?;
224                Ok(ResponseOrRef::Response(resolved))
225            }
226        }
227    }
228
229    fn resolve_media_types(
230        &mut self,
231        content: &mut IndexMap<String, MediaType>,
232    ) -> Result<(), ResolveError> {
233        let keys: Vec<String> = content.keys().cloned().collect();
234        for key in keys {
235            let mt = content.get(&key).unwrap().clone();
236            let mut resolved_mt = mt;
237            if let Some(ref s) = resolved_mt.schema {
238                resolved_mt.schema = Some(self.resolve_schema_or_ref(s)?);
239            }
240            if let Some(ref s) = resolved_mt.item_schema {
241                resolved_mt.item_schema = Some(self.resolve_schema_or_ref(s)?);
242            }
243            content.insert(key, resolved_mt);
244        }
245        Ok(())
246    }
247
248    // Lookup helpers
249
250    fn lookup_schema(&self, ref_path: &str) -> Result<Schema, ResolveError> {
251        let name = parse_ref_name(ref_path, "schemas")?;
252        self.components
253            .and_then(|c| c.schemas.get(name))
254            .and_then(|s| match s {
255                SchemaOrRef::Schema(schema) => Some(schema.as_ref().clone()),
256                SchemaOrRef::Ref { ref_path: inner } => {
257                    // Transitive ref — just extract the name and look up again
258                    let inner_name = parse_ref_name(inner, "schemas").ok()?;
259                    self.components
260                        .and_then(|c| c.schemas.get(inner_name))
261                        .and_then(|s2| match s2 {
262                            SchemaOrRef::Schema(schema) => Some(schema.as_ref().clone()),
263                            _ => None,
264                        })
265                }
266            })
267            .ok_or_else(|| ResolveError::RefTargetNotFound(ref_path.to_string()))
268    }
269
270    fn lookup_parameter(&self, ref_path: &str) -> Result<Parameter, ResolveError> {
271        let name = parse_ref_name(ref_path, "parameters")?;
272        self.components
273            .and_then(|c| c.parameters.get(name))
274            .and_then(|p| match p {
275                ParameterOrRef::Parameter(param) => Some(param.clone()),
276                _ => None,
277            })
278            .ok_or_else(|| ResolveError::RefTargetNotFound(ref_path.to_string()))
279    }
280
281    fn lookup_request_body(&self, ref_path: &str) -> Result<RequestBody, ResolveError> {
282        let name = parse_ref_name(ref_path, "requestBodies")?;
283        self.components
284            .and_then(|c| c.request_bodies.get(name))
285            .and_then(|rb| match rb {
286                RequestBodyOrRef::RequestBody(body) => Some(body.clone()),
287                _ => None,
288            })
289            .ok_or_else(|| ResolveError::RefTargetNotFound(ref_path.to_string()))
290    }
291
292    fn lookup_response(&self, ref_path: &str) -> Result<Response, ResolveError> {
293        let name = parse_ref_name(ref_path, "responses")?;
294        self.components
295            .and_then(|c| c.responses.get(name))
296            .and_then(|r| match r {
297                ResponseOrRef::Response(resp) => Some(resp.clone()),
298                _ => None,
299            })
300            .ok_or_else(|| ResolveError::RefTargetNotFound(ref_path.to_string()))
301    }
302}
303
304/// Parse a `$ref` path like `#/components/schemas/Foo` and extract the name.
305fn parse_ref_name<'a>(ref_path: &'a str, expected_section: &str) -> Result<&'a str, ResolveError> {
306    let stripped = ref_path
307        .strip_prefix("#/components/")
308        .ok_or_else(|| ResolveError::InvalidRefFormat(ref_path.to_string()))?;
309    let (section, name) = stripped
310        .split_once('/')
311        .ok_or_else(|| ResolveError::InvalidRefFormat(ref_path.to_string()))?;
312    if section != expected_section {
313        return Err(ResolveError::InvalidRefFormat(format!(
314            "expected section '{}', got '{}' in {}",
315            expected_section, section, ref_path
316        )));
317    }
318    Ok(name)
319}