Skip to content

Commit eea4bce

Browse files
authored
Merge pull request #101 from Tuntii/validate-spec-references-2490534079871699615
Validate references in all OpenAPI spec components
2 parents 71e9122 + 0015d47 commit eea4bce

2 files changed

Lines changed: 102 additions & 30 deletions

File tree

crates/rustapi-openapi/src/spec.rs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -164,27 +164,38 @@ impl OpenApiSpec {
164164
// Ignore other refs for now (e.g. external or non-schema refs)
165165
};
166166

167-
// Visitor pattern to traverse the spec
168-
let mut visit_schema = |schema: &SchemaRef| {
169-
visit_schema_ref(schema, &mut check_ref);
170-
};
171-
172167
// 1. Visit Paths
173168
for path_item in self.paths.values() {
174-
visit_path_item(path_item, &mut visit_schema);
169+
visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
175170
}
176171

177172
// 2. Visit Webhooks
178173
for path_item in self.webhooks.values() {
179-
visit_path_item(path_item, &mut visit_schema);
174+
visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
180175
}
181176

182-
// 3. Visit Components (including schemas referencing other schemas)
177+
// 3. Visit Components
183178
if let Some(components) = &self.components {
184179
for schema in components.schemas.values() {
185180
visit_json_schema(schema, &mut check_ref);
186181
}
187-
// TODO: Visit other components like parameters, headers, etc. if they can contain refs
182+
for resp in components.responses.values() {
183+
visit_response(resp, &mut |s| visit_schema_ref(s, &mut check_ref));
184+
}
185+
for param in components.parameters.values() {
186+
visit_parameter(param, &mut |s| visit_schema_ref(s, &mut check_ref));
187+
}
188+
for body in components.request_bodies.values() {
189+
visit_request_body(body, &mut |s| visit_schema_ref(s, &mut check_ref));
190+
}
191+
for header in components.headers.values() {
192+
visit_header(header, &mut |s| visit_schema_ref(s, &mut check_ref));
193+
}
194+
for callback_map in components.callbacks.values() {
195+
for item in callback_map.values() {
196+
visit_path_item(item, &mut |s| visit_schema_ref(s, &mut check_ref));
197+
}
198+
}
188199
}
189200

190201
if missing_refs.is_empty() {
@@ -228,9 +239,7 @@ where
228239
}
229240

230241
for param in &item.parameters {
231-
if let Some(s) = &param.schema {
232-
visit(s);
233-
}
242+
visit_parameter(param, visit);
234243
}
235244
}
236245

@@ -239,28 +248,61 @@ where
239248
F: FnMut(&SchemaRef),
240249
{
241250
for param in &op.parameters {
242-
if let Some(s) = &param.schema {
243-
visit(s);
244-
}
251+
visit_parameter(param, visit);
245252
}
246253
if let Some(body) = &op.request_body {
247-
for media in body.content.values() {
248-
if let Some(s) = &media.schema {
249-
visit(s);
250-
}
251-
}
254+
visit_request_body(body, visit);
252255
}
253256
for resp in op.responses.values() {
254-
for media in resp.content.values() {
255-
if let Some(s) = &media.schema {
256-
visit(s);
257-
}
258-
}
259-
for header in resp.headers.values() {
260-
if let Some(s) = &header.schema {
261-
visit(s);
262-
}
263-
}
257+
visit_response(resp, visit);
258+
}
259+
}
260+
261+
fn visit_parameter<F>(param: &Parameter, visit: &mut F)
262+
where
263+
F: FnMut(&SchemaRef),
264+
{
265+
if let Some(s) = &param.schema {
266+
visit(s);
267+
}
268+
}
269+
270+
fn visit_response<F>(resp: &ResponseSpec, visit: &mut F)
271+
where
272+
F: FnMut(&SchemaRef),
273+
{
274+
for media in resp.content.values() {
275+
visit_media_type(media, visit);
276+
}
277+
for header in resp.headers.values() {
278+
visit_header(header, visit);
279+
}
280+
}
281+
282+
fn visit_request_body<F>(body: &RequestBody, visit: &mut F)
283+
where
284+
F: FnMut(&SchemaRef),
285+
{
286+
for media in body.content.values() {
287+
visit_media_type(media, visit);
288+
}
289+
}
290+
291+
fn visit_header<F>(header: &Header, visit: &mut F)
292+
where
293+
F: FnMut(&SchemaRef),
294+
{
295+
if let Some(s) = &header.schema {
296+
visit(s);
297+
}
298+
}
299+
300+
fn visit_media_type<F>(media: &MediaType, visit: &mut F)
301+
where
302+
F: FnMut(&SchemaRef),
303+
{
304+
if let Some(s) = &media.schema {
305+
visit(s);
264306
}
265307
}
266308

crates/rustapi-openapi/src/tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,34 @@ mod tests {
237237
assert_eq!(missing.len(), 1);
238238
assert_eq!(missing[0], "#/components/schemas/NonExistent");
239239
}
240+
241+
#[test]
242+
fn test_ref_integrity_components_invalid() {
243+
let mut spec = OpenApiSpec::new("Test", "1.0");
244+
let mut components = crate::spec::Components::default();
245+
246+
components.parameters.insert(
247+
"badParam".to_string(),
248+
crate::spec::Parameter {
249+
name: "badParam".to_string(),
250+
location: "query".to_string(),
251+
required: false,
252+
description: None,
253+
deprecated: None,
254+
schema: Some(SchemaRef::Ref {
255+
reference: "#/components/schemas/NonExistent".to_string(),
256+
}),
257+
},
258+
);
259+
260+
spec.components = Some(components);
261+
262+
let result = spec.validate_integrity();
263+
assert!(
264+
result.is_err(),
265+
"Should detect missing ref in components.parameters"
266+
);
267+
let missing = result.unwrap_err();
268+
assert!(missing.contains(&"#/components/schemas/NonExistent".to_string()));
269+
}
240270
}

0 commit comments

Comments
 (0)