From 0015d47b92f13195d31d8c4784529f21090f1459 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 03:42:50 +0000 Subject: [PATCH] fix(openapi): validate references in all spec components Co-authored-by: Tuntii <121901995+Tuntii@users.noreply.github.com> --- crates/rustapi-openapi/src/spec.rs | 102 ++++++++++++++++++++-------- crates/rustapi-openapi/src/tests.rs | 30 ++++++++ 2 files changed, 102 insertions(+), 30 deletions(-) diff --git a/crates/rustapi-openapi/src/spec.rs b/crates/rustapi-openapi/src/spec.rs index 19ab6b5..7f2900c 100644 --- a/crates/rustapi-openapi/src/spec.rs +++ b/crates/rustapi-openapi/src/spec.rs @@ -164,27 +164,38 @@ impl OpenApiSpec { // Ignore other refs for now (e.g. external or non-schema refs) }; - // Visitor pattern to traverse the spec - let mut visit_schema = |schema: &SchemaRef| { - visit_schema_ref(schema, &mut check_ref); - }; - // 1. Visit Paths for path_item in self.paths.values() { - visit_path_item(path_item, &mut visit_schema); + visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref)); } // 2. Visit Webhooks for path_item in self.webhooks.values() { - visit_path_item(path_item, &mut visit_schema); + visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref)); } - // 3. Visit Components (including schemas referencing other schemas) + // 3. Visit Components if let Some(components) = &self.components { for schema in components.schemas.values() { visit_json_schema(schema, &mut check_ref); } - // TODO: Visit other components like parameters, headers, etc. if they can contain refs + for resp in components.responses.values() { + visit_response(resp, &mut |s| visit_schema_ref(s, &mut check_ref)); + } + for param in components.parameters.values() { + visit_parameter(param, &mut |s| visit_schema_ref(s, &mut check_ref)); + } + for body in components.request_bodies.values() { + visit_request_body(body, &mut |s| visit_schema_ref(s, &mut check_ref)); + } + for header in components.headers.values() { + visit_header(header, &mut |s| visit_schema_ref(s, &mut check_ref)); + } + for callback_map in components.callbacks.values() { + for item in callback_map.values() { + visit_path_item(item, &mut |s| visit_schema_ref(s, &mut check_ref)); + } + } } if missing_refs.is_empty() { @@ -228,9 +239,7 @@ where } for param in &item.parameters { - if let Some(s) = ¶m.schema { - visit(s); - } + visit_parameter(param, visit); } } @@ -239,28 +248,61 @@ where F: FnMut(&SchemaRef), { for param in &op.parameters { - if let Some(s) = ¶m.schema { - visit(s); - } + visit_parameter(param, visit); } if let Some(body) = &op.request_body { - for media in body.content.values() { - if let Some(s) = &media.schema { - visit(s); - } - } + visit_request_body(body, visit); } for resp in op.responses.values() { - for media in resp.content.values() { - if let Some(s) = &media.schema { - visit(s); - } - } - for header in resp.headers.values() { - if let Some(s) = &header.schema { - visit(s); - } - } + visit_response(resp, visit); + } +} + +fn visit_parameter(param: &Parameter, visit: &mut F) +where + F: FnMut(&SchemaRef), +{ + if let Some(s) = ¶m.schema { + visit(s); + } +} + +fn visit_response(resp: &ResponseSpec, visit: &mut F) +where + F: FnMut(&SchemaRef), +{ + for media in resp.content.values() { + visit_media_type(media, visit); + } + for header in resp.headers.values() { + visit_header(header, visit); + } +} + +fn visit_request_body(body: &RequestBody, visit: &mut F) +where + F: FnMut(&SchemaRef), +{ + for media in body.content.values() { + visit_media_type(media, visit); + } +} + +fn visit_header(header: &Header, visit: &mut F) +where + F: FnMut(&SchemaRef), +{ + if let Some(s) = &header.schema { + visit(s); + } +} + +fn visit_media_type(media: &MediaType, visit: &mut F) +where + F: FnMut(&SchemaRef), +{ + if let Some(s) = &media.schema { + visit(s); } } diff --git a/crates/rustapi-openapi/src/tests.rs b/crates/rustapi-openapi/src/tests.rs index 54d853b..a46d5aa 100644 --- a/crates/rustapi-openapi/src/tests.rs +++ b/crates/rustapi-openapi/src/tests.rs @@ -237,4 +237,34 @@ mod tests { assert_eq!(missing.len(), 1); assert_eq!(missing[0], "#/components/schemas/NonExistent"); } + + #[test] + fn test_ref_integrity_components_invalid() { + let mut spec = OpenApiSpec::new("Test", "1.0"); + let mut components = crate::spec::Components::default(); + + components.parameters.insert( + "badParam".to_string(), + crate::spec::Parameter { + name: "badParam".to_string(), + location: "query".to_string(), + required: false, + description: None, + deprecated: None, + schema: Some(SchemaRef::Ref { + reference: "#/components/schemas/NonExistent".to_string(), + }), + }, + ); + + spec.components = Some(components); + + let result = spec.validate_integrity(); + assert!( + result.is_err(), + "Should detect missing ref in components.parameters" + ); + let missing = result.unwrap_err(); + assert!(missing.contains(&"#/components/schemas/NonExistent".to_string())); + } }