Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 72 additions & 30 deletions crates/rustapi-openapi/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -228,9 +239,7 @@ where
}

for param in &item.parameters {
if let Some(s) = &param.schema {
visit(s);
}
visit_parameter(param, visit);
}
}

Expand All @@ -239,28 +248,61 @@ where
F: FnMut(&SchemaRef),
{
for param in &op.parameters {
if let Some(s) = &param.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<F>(param: &Parameter, visit: &mut F)
where
F: FnMut(&SchemaRef),
{
if let Some(s) = &param.schema {
visit(s);
}
}

fn visit_response<F>(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<F>(body: &RequestBody, visit: &mut F)
where
F: FnMut(&SchemaRef),
{
for media in body.content.values() {
visit_media_type(media, visit);
}
}

fn visit_header<F>(header: &Header, visit: &mut F)
where
F: FnMut(&SchemaRef),
{
if let Some(s) = &header.schema {
visit(s);
}
}

fn visit_media_type<F>(media: &MediaType, visit: &mut F)
where
F: FnMut(&SchemaRef),
{
if let Some(s) = &media.schema {
visit(s);
}
}

Expand Down
30 changes: 30 additions & 0 deletions crates/rustapi-openapi/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Comment on lines +241 to +269
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test only validates components.parameters, but the PR also adds validation for components.responses, components.request_bodies, components.headers, and components.callbacks. Consider adding test cases for these other component types to ensure comprehensive test coverage of the new validation logic.

Copilot uses AI. Check for mistakes.
}
Loading