use std::collections::BTreeSet;
use oas3::Map as OasMap;
use oas3::spec::{
MediaType as OasMediaType, ObjectOrReference, Operation as OasOperation,
Parameter as OasParameter, Schema as OasSchema,
};
use super::super::helpers::json_media_type;
use super::super::reference::schema_component_ref;
use super::super::resolve::ResolvedDocument;
use super::operation::{inferred_operation_id, operation_satay_skip};
use crate::error::ValidationError;
use crate::model::HttpMethod;
#[derive(Debug, Clone, Copy)]
enum MediaScope {
Json,
All,
}
pub(super) fn excluded_component_schemas(
document: &ResolvedDocument<'_>,
) -> Result<BTreeSet<String>, ValidationError> {
let Some(components) = document.spec.components.as_ref() else {
return Ok(BTreeSet::new());
};
let all_names = components
.schemas
.keys()
.cloned()
.collect::<BTreeSet<String>>();
let mut retained_seeds = BTreeSet::new();
let mut skipped_seeds = BTreeSet::new();
if let Some(paths) = document.spec.paths.as_ref() {
for (path, path_item) in paths {
let path_item =
document.resolve_path_item(path_item, &format!("path item `{path}`"))?;
let mut has_present = false;
let mut has_retained = false;
let mut retained_ops: Vec<&OasOperation> = vec![];
let mut skipped_ops: Vec<&OasOperation> = vec![];
for (method, operation) in [
(HttpMethod::Get, path_item.get.as_ref()),
(HttpMethod::Post, path_item.post.as_ref()),
(HttpMethod::Put, path_item.put.as_ref()),
(HttpMethod::Patch, path_item.patch.as_ref()),
(HttpMethod::Delete, path_item.delete.as_ref()),
(HttpMethod::Head, path_item.head.as_ref()),
(HttpMethod::Options, path_item.options.as_ref()),
(HttpMethod::Trace, path_item.trace.as_ref()),
] {
let Some(operation) = operation else {
continue;
};
has_present = true;
let operation_id = operation
.operation_id
.clone()
.unwrap_or_else(|| inferred_operation_id(method, path));
if operation_satay_skip(operation, &operation_id)? {
skipped_ops.push(operation);
} else {
retained_ops.push(operation);
has_retained = true;
}
}
let path_params_kept = !has_present || has_retained;
let params_bucket = if path_params_kept {
&mut retained_seeds
} else {
&mut skipped_seeds
};
let params_scope = if path_params_kept {
MediaScope::Json
} else {
MediaScope::All
};
for parameter in &path_item.parameters {
collect_parameter_schema_refs(document, parameter, params_scope, params_bucket)?;
}
for operation in retained_ops {
collect_operation_schema_refs(
document,
operation,
MediaScope::Json,
&mut retained_seeds,
)?;
}
for operation in skipped_ops {
collect_operation_schema_refs(
document,
operation,
MediaScope::All,
&mut skipped_seeds,
)?;
}
}
}
if skipped_seeds.is_empty() {
return Ok(BTreeSet::new());
}
let mut all_seeds = retained_seeds.clone();
all_seeds.extend(skipped_seeds.iter().cloned());
let reachable = closure(document, all_seeds);
let unreferenced = all_names
.difference(&reachable)
.cloned()
.collect::<BTreeSet<String>>();
let mut kept_roots = retained_seeds;
kept_roots.extend(unreferenced);
let kept = closure(document, kept_roots);
Ok(all_names.difference(&kept).cloned().collect())
}
fn collect_schema_ref_names(schema: &OasSchema, out: &mut BTreeSet<String>) {
for reference in schema.references() {
if let Ok(reference) = schema_component_ref(reference) {
out.insert(reference.name().to_owned());
}
}
}
fn closure(document: &ResolvedDocument<'_>, roots: BTreeSet<String>) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut worklist = roots.into_iter().collect::<Vec<String>>();
while let Some(name) = worklist.pop() {
if !seen.insert(name.clone()) {
continue;
}
let Some(schema) = document
.spec
.components
.as_ref()
.and_then(|components| components.schemas.get(&name))
else {
continue;
};
let mut edges = BTreeSet::new();
collect_schema_ref_names(schema, &mut edges);
for edge in edges {
if !seen.contains(&edge) {
worklist.push(edge);
}
}
}
seen
}
fn collect_parameter_schema_refs<'a>(
document: &ResolvedDocument<'a>,
parameter: &'a ObjectOrReference<OasParameter>,
scope: MediaScope,
out: &mut BTreeSet<String>,
) -> Result<(), ValidationError> {
let parameter = document.resolve(parameter, "reachability parameter")?;
if let Some(schema) = parameter.schema.as_ref() {
collect_schema_ref_names(schema, out);
}
if let Some(content) = parameter.content.as_ref() {
collect_content_schema_refs(content, scope, out);
}
Ok(())
}
fn collect_content_schema_refs(
content: &OasMap<String, OasMediaType>,
scope: MediaScope,
out: &mut BTreeSet<String>,
) {
match scope {
MediaScope::Json => {
if let Some((_, media)) = json_media_type(content)
&& let Some(schema) = media.schema.as_ref()
{
collect_schema_ref_names(schema, out);
}
}
MediaScope::All => {
for media in content.values() {
if let Some(schema) = media.schema.as_ref() {
collect_schema_ref_names(schema, out);
}
}
}
}
}
fn collect_operation_schema_refs<'a>(
document: &ResolvedDocument<'a>,
operation: &'a OasOperation,
scope: MediaScope,
out: &mut BTreeSet<String>,
) -> Result<(), ValidationError> {
for parameter in &operation.parameters {
collect_parameter_schema_refs(document, parameter, scope, out)?;
}
if let Some(request_body) = operation.request_body.as_ref() {
let request_body = document.resolve(request_body, "reachability requestBody")?;
collect_content_schema_refs(&request_body.content, scope, out);
}
if let Some(responses) = operation.responses.as_ref() {
for (status, response) in responses {
if matches!(scope, MediaScope::Json) && status == "default" {
continue;
}
let response = document.resolve(response, "reachability response")?;
collect_content_schema_refs(&response.content, scope, out);
}
}
Ok(())
}