use std::collections::{BTreeMap, BTreeSet};
use oas3::{
Spec,
spec::{Discriminator, ObjectOrReference, ObjectSchema, Operation, Schema, SchemaTypeSet},
};
use petgraph::{algo::kosaraju_scc, graphmap::DiGraphMap, visit::Dfs};
use crate::{
generator::{
ast::TypeRef,
metrics::{GenerationStats, GenerationWarning},
naming::{
identifiers::to_rust_type_name,
name_index::{ScanResult, TypeNameIndex},
},
operation_registry::OperationRegistry,
},
utils::{
SchemaExt, UnionFingerprints, extract_schema_ref_name, extract_union_fingerprint, parse_schema_ref_path,
schema_ext::SchemaExtIters,
},
};
#[derive(Debug, Clone)]
pub(crate) struct DiscriminatorMapping {
pub field_name: String,
pub field_value: String,
}
#[derive(Debug, Clone)]
pub(crate) struct MergedSchema {
pub schema: ObjectSchema,
pub discriminator_parent: Option<String>,
}
#[derive(Default)]
struct MergeAccumulator {
properties: BTreeMap<String, ObjectOrReference<ObjectSchema>>,
required: BTreeSet<String>,
discriminator: Option<Discriminator>,
schema_type: Option<SchemaTypeSet>,
additional_properties: Option<Schema>,
discriminator_parent: Option<String>,
}
impl MergeAccumulator {
fn merge_from(&mut self, source: &ObjectSchema) {
for (name, prop) in &source.properties {
self.properties.insert(name.clone(), prop.clone());
}
self.required.extend(source.required.iter().cloned());
if source.discriminator.is_some() {
self.discriminator.clone_from(&source.discriminator);
}
if source.schema_type.is_some() {
self.schema_type.clone_from(&source.schema_type);
}
if self.additional_properties.is_none() && source.additional_properties.is_some() {
self.additional_properties.clone_from(&source.additional_properties);
}
}
fn merge_optional_from(&mut self, source: &ObjectSchema) {
for (name, prop) in &source.properties {
self.properties.entry(name.clone()).or_insert_with(|| prop.clone());
}
}
fn into_schema(self, base: &ObjectSchema) -> ObjectSchema {
let mut result = base.clone();
result.properties = self.properties;
result.required = self.required.into_iter().collect();
result.discriminator = self.discriminator;
if self.schema_type.is_some() {
result.schema_type = self.schema_type;
}
result.all_of.clear();
if result.additional_properties.is_none() {
result.additional_properties = self.additional_properties;
}
result
}
}
#[derive(Debug)]
pub(crate) struct SchemaRegistry {
schemas: BTreeMap<String, ObjectSchema>,
merged_schemas: BTreeMap<String, MergedSchema>,
discriminator_parents: BTreeMap<String, String>,
dependencies: BTreeMap<String, BTreeSet<String>>,
cyclic_schemas: BTreeSet<String>,
discriminator_cache: BTreeMap<String, DiscriminatorMapping>,
inheritance_depths: BTreeMap<String, usize>,
spec: Spec,
}
impl SchemaRegistry {
pub(crate) fn new(spec: &Spec, stats: &mut GenerationStats) -> Self {
let mut schemas = BTreeMap::new();
if let Some(components) = &spec.components {
for (name, schema_ref) in &components.schemas {
match schema_ref.resolve(spec) {
Ok(schema) => {
schemas.insert(name.clone(), schema);
}
Err(error) => {
stats.record_warning(GenerationWarning::SchemaConversionFailed {
schema_name: name.clone(),
error: error.to_string(),
});
}
}
}
}
Self {
schemas: schemas.clone(),
merged_schemas: BTreeMap::new(),
discriminator_parents: BTreeMap::new(),
dependencies: BTreeMap::new(),
cyclic_schemas: BTreeSet::new(),
discriminator_cache: Self::build_discriminator_cache(&schemas, stats),
inheritance_depths: BTreeMap::new(),
spec: spec.clone(),
}
}
pub(crate) fn initialize(
&mut self,
operation_registry: &OperationRegistry,
include_all: bool,
union_fingerprints: &UnionFingerprints,
) -> (Vec<Vec<String>>, Option<BTreeSet<String>>) {
self.build_dependencies(union_fingerprints);
let cycle_details = self.detect_cycles();
let reachable = if include_all {
None
} else {
Some(self.reachable(operation_registry, union_fingerprints))
};
(cycle_details, reachable)
}
pub(crate) fn spec(&self) -> &Spec {
&self.spec
}
pub(crate) fn get(&self, name: &str) -> Option<&ObjectSchema> {
self.schemas.get(name)
}
pub(crate) fn contains(&self, name: &str) -> bool {
self.schemas.contains_key(name)
}
pub(crate) fn keys(&self) -> Vec<&String> {
self.schemas.keys().collect()
}
pub(crate) fn schemas(&self) -> &BTreeMap<String, ObjectSchema> {
&self.schemas
}
pub(crate) fn merged(&self, name: &str) -> Option<&MergedSchema> {
self.merged_schemas.get(name)
}
pub(crate) fn resolved(&self, name: &str) -> Option<&ObjectSchema> {
self.merged(name).map(|m| &m.schema).or_else(|| self.schemas.get(name))
}
pub(crate) fn parent(&self, name: &str) -> Option<&str> {
self.discriminator_parents.get(name).map(String::as_str)
}
pub(crate) fn mapping(&self, schema_name: &str) -> Option<&DiscriminatorMapping> {
self.discriminator_cache.get(schema_name)
}
pub(crate) fn effective_mapping(&self, schema: &ObjectSchema) -> Option<BTreeMap<String, String>> {
let discriminator = schema.discriminator.as_ref()?;
if let Some(mapping) = &discriminator.mapping {
return Some(mapping.clone());
}
let synthesized = schema.union_variants().try_fold(BTreeMap::new(), |mut acc, variant| {
let ObjectOrReference::Ref { ref_path, .. } = variant else {
return Some(acc);
};
let name = parse_schema_ref_path(ref_path)?;
let dm = self.discriminator_cache.get(&name)?;
(dm.field_name == discriminator.property_name).then(|| {
acc.insert(dm.field_value.clone(), ref_path.clone());
acc
})
})?;
(!synthesized.is_empty()).then_some(synthesized)
}
pub(crate) fn is_cyclic(&self, schema_name: &str) -> bool {
self.cyclic_schemas.contains(schema_name)
}
pub(crate) fn type_ref(&self, schema_name: &str) -> TypeRef {
let mut type_ref = TypeRef::new(to_rust_type_name(schema_name));
if self.is_cyclic(schema_name) {
type_ref = type_ref.with_boxed();
}
type_ref
}
pub(crate) fn merge_all_of(&self, schema: &ObjectSchema) -> ObjectSchema {
self.merge_schema(schema).schema
}
pub(crate) fn merge_inline(&self, schema: &ObjectSchema) -> anyhow::Result<ObjectSchema> {
if schema.all_of.is_empty() {
return Ok(schema.clone());
}
let mut acc = MergeAccumulator::default();
for all_of_ref in &schema.all_of {
match all_of_ref {
ObjectOrReference::Ref { ref_path, .. } => {
if let Some(name) = parse_schema_ref_path(ref_path)
&& let Some(merged) = self.merged_schemas.get(&name)
{
acc.merge_from(&merged.schema);
continue;
}
let resolved = all_of_ref
.resolve(&self.spec)
.map_err(|e| anyhow::anyhow!("Schema resolution failed for inline allOf reference: {e}"))?;
acc.merge_from(&resolved);
}
ObjectOrReference::Object(inline) => {
let inner_merged = self.merge_inline(inline)?;
acc.merge_from(&inner_merged);
}
}
}
acc.merge_from(schema);
if acc.additional_properties.is_none() {
acc.additional_properties.clone_from(&schema.additional_properties);
}
Ok(acc.into_schema(schema))
}
#[allow(clippy::self_only_used_in_recursion)]
pub(crate) fn collect(&self, schema: &ObjectSchema, union_fingerprints: &UnionFingerprints) -> BTreeSet<String> {
let mut refs = BTreeSet::new();
for prop_schema in schema.properties.values() {
if let Some(ref_name) = extract_schema_ref_name(prop_schema) {
refs.insert(ref_name);
}
if let ObjectOrReference::Object(inline) = prop_schema {
refs.extend(self.collect(inline, union_fingerprints));
}
}
for schema_ref in schema.union_variants().chain(&schema.all_of) {
if let Some(ref_name) = extract_schema_ref_name(schema_ref) {
refs.insert(ref_name);
}
if let ObjectOrReference::Object(inline) = schema_ref {
refs.extend(self.collect(inline, union_fingerprints));
}
}
for variants in [&schema.one_of, &schema.any_of] {
let fingerprint = extract_union_fingerprint(variants);
if !fingerprint.is_empty()
&& let Some(name) = union_fingerprints.get(&fingerprint)
{
refs.insert(name.clone());
}
}
if let Some(ref items_box) = schema.items
&& let Schema::Object(ref schema_ref) = **items_box
{
if let Some(ref_name) = extract_schema_ref_name(schema_ref) {
refs.insert(ref_name);
}
if let ObjectOrReference::Object(inline) = &**schema_ref {
refs.extend(self.collect(inline, union_fingerprints));
}
}
if let Some(Schema::Object(ref schema_ref)) = schema.additional_properties {
if let Some(ref_name) = extract_schema_ref_name(schema_ref) {
refs.insert(ref_name);
}
if let ObjectOrReference::Object(inline) = &**schema_ref {
refs.extend(self.collect(inline, union_fingerprints));
}
}
refs
}
pub(crate) fn collect_ref(
&self,
schema_ref: &ObjectOrReference<ObjectSchema>,
union_fingerprints: &UnionFingerprints,
) -> BTreeSet<String> {
let mut refs = BTreeSet::new();
if let Some(ref_name) = extract_schema_ref_name(schema_ref) {
refs.insert(ref_name);
}
if let ObjectOrReference::Object(inline_schema) = schema_ref {
refs.extend(self.collect(inline_schema, union_fingerprints));
}
refs
}
pub(crate) fn detect_cycles(&mut self) -> Vec<Vec<String>> {
let mut graph = DiGraphMap::<&str, ()>::new();
for (node, deps) in &self.dependencies {
graph.add_node(node.as_str());
for dep in deps {
graph.add_edge(node.as_str(), dep.as_str(), ());
}
}
let cycles: Vec<Vec<String>> = kosaraju_scc(&graph)
.into_iter()
.filter(|scc| scc.len() > 1 || graph.contains_edge(scc[0], scc[0]))
.map(|scc| scc.into_iter().map(String::from).collect())
.collect();
self.cyclic_schemas.extend(cycles.iter().flatten().cloned());
cycles
}
pub(crate) fn reachable(
&self,
operation_registry: &OperationRegistry,
union_fingerprints: &UnionFingerprints,
) -> BTreeSet<String> {
let initial_refs = operation_registry
.operations()
.flat_map(|entry| self.collect_refs_from_operation(&entry.operation, union_fingerprints))
.collect::<BTreeSet<_>>();
let graph = DiGraphMap::<&str, ()>::from_edges(
self
.dependencies
.iter()
.flat_map(|(node, deps)| deps.iter().map(move |dep| (node.as_str(), dep.as_str()))),
);
let mut expanded = initial_refs.clone();
for start in &initial_refs {
if graph.contains_node(start.as_str()) {
let mut dfs = Dfs::new(&graph, start.as_str());
while let Some(node) = dfs.next(&graph) {
expanded.insert(node.to_string());
}
}
}
expanded
}
pub(crate) fn scan_and_compute_names(&self) -> anyhow::Result<ScanResult> {
let index = TypeNameIndex::new(&self.schemas, &self.spec);
index.scan_and_compute_names()
}
pub(crate) fn build_dependencies(&mut self, union_fingerprints: &UnionFingerprints) {
for (name, schema) in &self.schemas {
self
.dependencies
.insert(name.clone(), self.collect(schema, union_fingerprints));
}
self.compute_inheritance_depths();
self.build_merged_schemas();
self.build_discriminator_parents();
}
fn compute_inheritance_depths(&mut self) {
fn compute_depth(registry: &mut SchemaRegistry, name: &str) -> usize {
if let Some(&depth) = registry.inheritance_depths.get(name) {
return depth;
}
let parent_names = registry
.schemas
.get(name)
.map(|s| s.all_of.iter().filter_map(extract_schema_ref_name).collect::<Vec<_>>())
.unwrap_or_default();
let depth = if parent_names.is_empty() {
0
} else {
parent_names
.iter()
.map(|p| compute_depth(registry, p))
.max()
.unwrap_or(0)
+ 1
};
registry.inheritance_depths.insert(name.to_string(), depth);
depth
}
let names = self.schemas.keys().cloned().collect::<Vec<_>>();
for name in names {
compute_depth(self, &name);
}
}
fn build_merged_schemas(&mut self) {
let mut sorted_names = self.schemas.keys().cloned().collect::<Vec<_>>();
sorted_names.sort_by_key(|name| self.inheritance_depths.get(name).copied().unwrap_or(0));
for name in sorted_names {
let Some(schema) = self.schemas.get(&name).cloned() else {
continue;
};
let merged = self.merge_schema(&schema);
self.merged_schemas.insert(name, merged);
}
}
fn resolve_schema_ref<'a>(&'a self, schema_ref: &'a ObjectOrReference<ObjectSchema>) -> Option<&'a ObjectSchema> {
match schema_ref {
ObjectOrReference::Ref { ref_path, .. } => {
let name = parse_schema_ref_path(ref_path)?;
self
.merged_schemas
.get(&name)
.map(|m| &m.schema)
.or_else(|| self.schemas.get(&name))
}
ObjectOrReference::Object(s) => Some(s),
}
}
fn find_additional_properties(&self, schema: &ObjectSchema) -> Option<Schema> {
schema
.all_of
.iter()
.resolve_all(&self.spec)
.find_map(|parent| parent.additional_properties.clone())
}
fn build_discriminator_cache(
schemas: &BTreeMap<String, ObjectSchema>,
stats: &mut GenerationStats,
) -> BTreeMap<String, DiscriminatorMapping> {
let mut cache = BTreeMap::new();
for (parent_name, schema) in schemas {
let Some(d) = &schema.discriminator else {
continue;
};
if let Some(mapping) = &d.mapping {
for (val, ref_path) in mapping {
if let Some(schema_name) = parse_schema_ref_path(ref_path) {
cache.insert(
schema_name,
DiscriminatorMapping {
field_name: d.property_name.clone(),
field_value: val.clone(),
},
);
}
}
continue;
}
Self::synthesize_implicit_mappings(parent_name, schema, d, schemas, stats, &mut cache);
}
cache
}
fn synthesize_implicit_mappings(
parent_name: &str,
schema: &ObjectSchema,
discriminator: &Discriminator,
schemas: &BTreeMap<String, ObjectSchema>,
stats: &mut GenerationStats,
cache: &mut BTreeMap<String, DiscriminatorMapping>,
) {
let mut staged = BTreeMap::new();
let mut seen_values = BTreeSet::new();
for variant_name in schema.union_variants().filter_map(extract_schema_ref_name) {
let warn = |msg: String| GenerationWarning::DiscriminatorMappingFailed {
schema_name: parent_name.to_owned(),
message: msg,
};
let Some(variant_schema) = schemas.get(&variant_name) else {
stats.record_warning(warn(format!(
"cannot build implicit discriminator mapping: variant '{variant_name}' not found in schemas"
)));
return;
};
let Some(const_value) = Self::extract_const_discriminator_value(variant_schema, &discriminator.property_name)
else {
stats.record_warning(warn(format!(
"cannot build implicit discriminator mapping: variant '{variant_name}' has no string const value for property '{}'",
discriminator.property_name
)));
return;
};
if !seen_values.insert(const_value.clone()) {
stats.record_warning(warn(format!(
"cannot build implicit discriminator mapping: duplicate const value '{const_value}' on variant '{variant_name}'"
)));
return;
}
staged.insert(
variant_name,
DiscriminatorMapping {
field_name: discriminator.property_name.clone(),
field_value: const_value,
},
);
}
cache.extend(staged);
}
fn extract_const_discriminator_value(schema: &ObjectSchema, property_name: &str) -> Option<String> {
let prop_ref = schema.properties.get(property_name)?;
let ObjectOrReference::Object(prop_schema) = prop_ref else {
return None;
};
prop_schema.const_value.as_ref()?.as_str().map(String::from)
}
fn build_discriminator_parents(&mut self) {
self.discriminator_parents = self
.merged_schemas
.iter()
.filter_map(|(child_name, merged)| {
merged
.discriminator_parent
.as_ref()
.filter(|_| self.discriminator_cache.contains_key(child_name))
.map(|parent_name| (child_name.clone(), parent_name.clone()))
})
.collect();
}
fn merge_schema(&self, schema: &ObjectSchema) -> MergedSchema {
if schema.all_of.is_empty() {
return MergedSchema {
schema: schema.clone(),
discriminator_parent: None,
};
}
let mut acc = MergeAccumulator::default();
for all_of_ref in &schema.all_of {
if let Some(parent_name) = extract_schema_ref_name(all_of_ref)
&& let Some(parent) = self.resolve_schema_ref(all_of_ref)
&& parent.is_discriminated_base_type()
{
acc.discriminator_parent = Some(parent_name.clone());
}
if let Some(parent) = self.resolve_schema_ref(all_of_ref) {
acc.merge_from(parent);
}
}
for schema_ref in schema.any_of.iter().chain(&schema.one_of) {
if let Some(source) = self.resolve_schema_ref(schema_ref) {
acc.merge_optional_from(source);
}
}
acc.merge_from(schema);
if acc.additional_properties.is_none() {
acc.additional_properties = self.find_additional_properties(schema);
}
let discriminator_parent = acc.discriminator_parent.take();
MergedSchema {
schema: acc.into_schema(schema),
discriminator_parent,
}
}
fn collect_refs_from_operation(
&self,
operation: &Operation,
union_fingerprints: &UnionFingerprints,
) -> BTreeSet<String> {
let mut refs = BTreeSet::new();
for param in &operation.parameters {
if let Ok(resolved_param) = param.resolve(&self.spec)
&& let Some(ref schema_ref) = resolved_param.schema
{
refs.extend(self.collect_ref(schema_ref, union_fingerprints));
}
}
if let Some(ref request_body_ref) = operation.request_body
&& let Ok(request_body) = request_body_ref.resolve(&self.spec)
{
for media_type in request_body.content.values() {
if let Some(ref schema_ref) = media_type.schema {
refs.extend(self.collect_ref(schema_ref, union_fingerprints));
}
}
}
if let Some(ref responses) = operation.responses {
for response_ref in responses.values() {
if let Ok(response) = response_ref.resolve(&self.spec) {
for media_type in response.content.values() {
if let Some(ref schema_ref) = media_type.schema {
refs.extend(self.collect_ref(schema_ref, union_fingerprints));
}
}
}
}
}
refs
}
}