Skip to main content

oag_core/parse/
ref_resolve.rs

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