use std::collections::{BTreeMap, BTreeSet};
use oas3::{
Spec,
spec::{ObjectOrReference, ObjectSchema, Schema},
};
#[derive(Debug)]
pub(crate) struct SchemaGraph {
schemas: BTreeMap<String, ObjectSchema>,
dependencies: BTreeMap<String, BTreeSet<String>>,
cyclic_schemas: BTreeSet<String>,
spec: Spec,
}
impl SchemaGraph {
pub(crate) fn new(spec: Spec) -> anyhow::Result<Self> {
let mut graph = Self {
schemas: BTreeMap::new(),
dependencies: BTreeMap::new(),
cyclic_schemas: BTreeSet::new(),
spec,
};
if let Some(components) = &graph.spec.components {
for (name, schema_ref) in &components.schemas {
if let Ok(schema) = schema_ref.resolve(&graph.spec) {
graph.schemas.insert(name.clone(), schema);
}
}
}
Ok(graph)
}
pub(crate) fn get_schema(&self, name: &str) -> Option<&ObjectSchema> {
self.schemas.get(name)
}
pub(crate) fn schema_names(&self) -> Vec<&String> {
self.schemas.keys().collect()
}
pub(crate) fn spec(&self) -> &Spec {
&self.spec
}
pub(crate) fn extract_ref_name(ref_string: &str) -> Option<String> {
ref_string.strip_prefix("#/components/schemas/").map(|s| s.to_string())
}
fn extract_ref_from_obj_ref(obj_ref: &ObjectOrReference<ObjectSchema>) -> Option<String> {
match obj_ref {
ObjectOrReference::Ref { ref_path, .. } => Self::extract_ref_name(ref_path),
ObjectOrReference::Object(_) => None,
}
}
pub(crate) fn build_dependencies(&mut self) {
let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
for schema_name in schema_names {
let mut deps = BTreeSet::new();
if let Some(schema) = self.schemas.get(&schema_name) {
self.collect_dependencies(schema, &mut deps);
}
self.dependencies.insert(schema_name, deps);
}
}
fn collect_dependencies(&self, schema: &ObjectSchema, deps: &mut BTreeSet<String>) {
for prop_schema in schema.properties.values() {
self.extract_refs_from_schema_ref(prop_schema, deps);
}
for one_of_schema in &schema.one_of {
self.extract_refs_from_schema_ref(one_of_schema, deps);
}
for any_of_schema in &schema.any_of {
self.extract_refs_from_schema_ref(any_of_schema, deps);
}
for all_of_schema in &schema.all_of {
self.extract_refs_from_schema_ref(all_of_schema, deps);
}
if let Some(ref items_box) = schema.items
&& let Schema::Object(ref schema_ref) = **items_box
{
self.extract_refs_from_schema_ref(schema_ref, deps);
}
}
fn extract_refs_from_schema_ref(&self, schema_ref: &ObjectOrReference<ObjectSchema>, deps: &mut BTreeSet<String>) {
if let Some(ref_name) = Self::extract_ref_from_obj_ref(schema_ref) {
deps.insert(ref_name);
}
if let oas3::spec::ObjectOrReference::Object(inline_schema) = schema_ref {
self.collect_dependencies(inline_schema, deps);
}
}
pub(crate) fn detect_cycles(&mut self) -> Vec<Vec<String>> {
let mut visited = BTreeSet::new();
let mut rec_stack = BTreeSet::new();
let mut cycles = Vec::new();
let mut path = Vec::new();
let schema_names: Vec<String> = self.schemas.keys().cloned().collect();
for schema_name in schema_names {
if !visited.contains(&schema_name) {
self.dfs_detect_cycle(&schema_name, &mut visited, &mut rec_stack, &mut path, &mut cycles);
}
}
for cycle in &cycles {
for schema_name in cycle {
self.cyclic_schemas.insert(schema_name.clone());
}
}
cycles
}
fn dfs_detect_cycle(
&self,
node: &str,
visited: &mut BTreeSet<String>,
rec_stack: &mut BTreeSet<String>,
path: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
path.push(node.to_string());
if let Some(deps) = self.dependencies.get(node) {
for dep in deps {
if !visited.contains(dep) {
self.dfs_detect_cycle(dep, visited, rec_stack, path, cycles);
} else if rec_stack.contains(dep) {
if let Some(cycle_start) = path.iter().position(|n| n == dep) {
let cycle: Vec<String> = path[cycle_start..].to_vec();
cycles.push(cycle);
}
}
}
}
path.pop();
rec_stack.remove(node);
}
pub(crate) fn is_cyclic(&self, schema_name: &str) -> bool {
self.cyclic_schemas.contains(schema_name)
}
}