use std::{collections::BTreeSet, hash::Hash};
use indexmap::{IndexMap, IndexSet};
use inflections::Inflect;
use oas3::{Spec, spec::ObjectSchema};
use super::identifiers::{FORBIDDEN_IDENTIFIERS, ensure_unique, to_rust_type_name};
use crate::{
generator::{
converter::{hashing::CanonicalSchema, union_types::variants_to_cache_key},
naming::constants::KNOWN_ENUM_VARIANT,
},
utils::{SchemaExt, SchemaInspect, SchemaMap},
};
const RESERVED_TYPE_NAMES: &[&str] = &["Enum", "Struct", "Type", "Object"];
type NameCandidates = IndexSet<(String, bool)>;
#[derive(Default, Clone, Debug)]
pub struct SchemaPrecomputed {
pub enum_cache_key: Option<Vec<String>>,
}
#[derive(Default)]
pub struct ScanResult {
pub names: IndexMap<CanonicalSchema, String>,
pub enum_names: IndexMap<Vec<String>, String>,
pub schema_metadata: IndexMap<CanonicalSchema, SchemaPrecomputed>,
}
#[derive(Default)]
struct CandidateIndex {
schemas: IndexMap<CanonicalSchema, NameCandidates>,
enums: IndexMap<Vec<String>, NameCandidates>,
metadata: IndexMap<CanonicalSchema, SchemaPrecomputed>,
}
impl CandidateIndex {
fn merge(mut self, other: Self) -> Self {
for (key, candidates) in other.schemas {
self.schemas.entry(key).or_default().extend(candidates);
}
for (key, candidates) in other.enums {
self.enums.entry(key).or_default().extend(candidates);
}
for (key, meta) in other.metadata {
self.metadata.entry(key).or_insert(meta);
}
self
}
fn add_schema_candidate_with_metadata(
&mut self,
canonical: CanonicalSchema,
name: String,
is_from_schema: bool,
enum_cache_key: Option<Vec<String>>,
) {
self
.schemas
.entry(canonical.clone())
.or_default()
.insert((name, is_from_schema));
self
.metadata
.entry(canonical)
.or_insert_with(|| SchemaPrecomputed { enum_cache_key });
}
fn add_enum_candidate(&mut self, values: Vec<String>, name: String, is_from_schema: bool) {
self.enums.entry(values).or_default().insert((name, is_from_schema));
}
}
pub struct TypeNameIndex<'a> {
schemas: &'a SchemaMap,
spec: &'a Spec,
}
impl<'a> TypeNameIndex<'a> {
pub fn new(schemas: &'a SchemaMap, spec: &'a Spec) -> Self {
Self { schemas, spec }
}
pub fn scan_and_compute_names(&self) -> anyhow::Result<ScanResult> {
let candidates = self.collect_all_candidates()?;
let mut used_names = self.existing_rust_names();
let enum_names = resolve_names(candidates.enums, &mut used_names);
let names = resolve_names(candidates.schemas, &mut used_names);
Ok(ScanResult {
names,
enum_names,
schema_metadata: candidates.metadata,
})
}
fn collect_all_candidates(&self) -> anyhow::Result<CandidateIndex> {
self
.schemas
.iter()
.try_fold(CandidateIndex::default(), |acc, (name, schema)| {
let mut index = self.collect_top_level_candidates(name, schema);
let inline = self.collect_inline_candidates(name, schema)?;
index = index.merge(inline);
Ok(acc.merge(index))
})
}
fn collect_top_level_candidates(&self, schema_name: &str, schema: &ObjectSchema) -> CandidateIndex {
let mut index = CandidateIndex::default();
if schema.should_register_as_enum() {
let entries = schema.extract_enum_entries(self.spec);
let mut rust_name = to_rust_type_name(schema_name);
if schema.has_relaxed_anyof_enum() {
rust_name.push_str(KNOWN_ENUM_VARIANT);
}
index.add_enum_candidate(variants_to_cache_key(&entries), rust_name, true);
}
index
}
fn collect_inline_candidates(&self, parent_name: &str, schema: &ObjectSchema) -> anyhow::Result<CandidateIndex> {
let mut index = CandidateIndex::default();
for (prop_name, prop_schema_ref) in &schema.properties {
let Some(prop_schema) = prop_schema_ref.as_inline() else {
continue;
};
let next_parent = format!("{parent_name}{}", prop_name.to_pascal_case());
if prop_schema.requires_type_definition() {
let canonical = CanonicalSchema::from_schema(prop_schema)?;
let rust_name = to_rust_type_name(&next_parent);
let enum_cache_key = if prop_schema.should_register_as_enum() {
let entries = prop_schema.extract_enum_entries(self.spec);
let key = variants_to_cache_key(&entries);
index.add_enum_candidate(key.clone(), rust_name.clone(), false);
Some(key)
} else {
None
};
index.add_schema_candidate_with_metadata(canonical, rust_name, false, enum_cache_key);
}
if !prop_schema.properties.is_empty() {
index = index.merge(self.collect_inline_candidates(&next_parent, prop_schema)?);
}
}
for sub in schema.all_of.iter().filter_map(SchemaInspect::as_inline) {
index = index.merge(self.collect_inline_candidates(parent_name, sub)?);
}
Ok(index)
}
fn existing_rust_names(&self) -> BTreeSet<String> {
self.schemas.keys().map(|name| to_rust_type_name(name)).collect()
}
}
fn resolve_names<K: Eq + Hash>(
candidates: IndexMap<K, NameCandidates>,
used_names: &mut BTreeSet<String>,
) -> IndexMap<K, String> {
candidates
.into_iter()
.map(|(key, name_candidates)| {
let best = compute_best_name(&name_candidates, used_names);
used_names.insert(best.clone());
(key, best)
})
.collect()
}
pub fn compute_best_name(candidates: &NameCandidates, used_names: &BTreeSet<String>) -> String {
if let Some((name, _)) = candidates.iter().find(|(_, is_from_schema)| *is_from_schema) {
return name.clone();
}
let candidate_vec = candidates.iter().map(|(n, _)| n).collect::<Vec<&String>>();
match candidate_vec.as_slice() {
[] => "UnknownType".to_string(),
[single] => ensure_unique(single, used_names),
_ => {
let lcs = longest_common_suffix(&candidate_vec);
if is_valid_common_name(&lcs) {
ensure_unique(&lcs, used_names)
} else {
ensure_unique(candidate_vec[0], used_names)
}
}
}
}
pub fn longest_common_suffix(strings: &[&String]) -> String {
let [first, rest @ ..] = strings else {
return String::new();
};
let first_chars = first.chars().collect::<Vec<char>>();
let rest_chars = rest
.iter()
.map(|s| s.chars().collect::<Vec<char>>())
.collect::<Vec<Vec<char>>>();
let min_len = std::iter::once(first_chars.len())
.chain(rest_chars.iter().map(Vec::len))
.min()
.unwrap_or(0);
let suffix_len = (1..=min_len)
.take_while(|&offset| {
let ch = first_chars[first_chars.len() - offset];
rest_chars.iter().all(|other| other[other.len() - offset] == ch)
})
.count();
first_chars[first_chars.len() - suffix_len..].iter().collect()
}
pub fn is_valid_common_name(name: &str) -> bool {
name.len() >= 4
&& !RESERVED_TYPE_NAMES.contains(&name)
&& name.starts_with(char::is_uppercase)
&& !FORBIDDEN_IDENTIFIERS.contains(name)
}