use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use inflections::Inflect;
use oas3::spec::{ObjectOrReference, ObjectSchema, SchemaType, SchemaTypeSet};
use super::{cache::SharedSchemaCache, type_resolver::TypeResolver};
use crate::{
generator::schema_graph::SchemaGraph,
reserved::{FORBIDDEN_IDENTIFIERS, to_rust_type_name},
};
pub(crate) struct InlineTypeScanner<'a> {
graph: &'a SchemaGraph,
#[allow(dead_code)]
type_resolver: TypeResolver<'a>,
}
#[derive(Default)]
pub(crate) struct ScanResult {
pub(crate) names: BTreeMap<String, String>,
pub(crate) enum_names: HashMap<Vec<String>, String>,
}
impl<'a> InlineTypeScanner<'a> {
pub(crate) fn new(graph: &'a SchemaGraph, type_resolver: TypeResolver<'a>) -> Self {
Self { graph, type_resolver }
}
pub(crate) fn scan_and_compute_names(&self) -> anyhow::Result<ScanResult> {
let mut candidates: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut enum_candidates: HashMap<Vec<String>, BTreeSet<String>> = HashMap::new();
for schema_name in self.graph.schema_names() {
if let Some(schema) = self.graph.get_schema(schema_name) {
if Self::is_inline_target(schema)
&& let Some(values) = Self::extract_enum_values(schema)
{
let rust_name = to_rust_type_name(schema_name);
enum_candidates.entry(values).or_default().insert(rust_name);
}
Self::collect_inline_candidates(schema_name, schema, &mut candidates, &mut enum_candidates)?;
}
}
let mut final_names = BTreeMap::new();
let mut final_enum_names = HashMap::new();
let mut used_names = self.get_existing_names();
for (values, name_candidates) in enum_candidates {
let best_name = Self::compute_best_name(&name_candidates, &used_names);
used_names.insert(best_name.clone());
final_enum_names.insert(values, best_name);
}
for (hash, name_candidates) in candidates {
let best_name = Self::compute_best_name(&name_candidates, &used_names);
used_names.insert(best_name.clone());
final_names.insert(hash, best_name);
}
Ok(ScanResult {
names: final_names,
enum_names: final_enum_names,
})
}
fn get_existing_names(&self) -> BTreeSet<String> {
self
.graph
.schema_names()
.iter()
.map(|name| to_rust_type_name(name))
.collect()
}
fn collect_inline_candidates(
parent_name: &str,
schema: &ObjectSchema,
candidates: &mut BTreeMap<String, BTreeSet<String>>,
enum_candidates: &mut HashMap<Vec<String>, BTreeSet<String>>,
) -> anyhow::Result<()> {
for (prop_name, prop_schema_ref) in &schema.properties {
let prop_schema = match prop_schema_ref {
ObjectOrReference::Ref { .. } => continue,
ObjectOrReference::Object(s) => s,
};
if Self::is_inline_target(prop_schema) {
let hash = SharedSchemaCache::hash_schema(prop_schema)?;
let candidate_name = format!("{}{}", parent_name, prop_name.to_pascal_case());
let rust_name = to_rust_type_name(&candidate_name);
candidates.entry(hash).or_default().insert(rust_name.clone());
if let Some(values) = Self::extract_enum_values(prop_schema) {
enum_candidates.entry(values).or_default().insert(rust_name);
}
}
if !prop_schema.properties.is_empty() {
let next_parent = format!("{}{}", parent_name, prop_name.to_pascal_case());
Self::collect_inline_candidates(&next_parent, prop_schema, candidates, enum_candidates)?;
}
}
for sub_ref in &schema.all_of {
if let ObjectOrReference::Object(sub) = sub_ref {
Self::collect_inline_candidates(parent_name, sub, candidates, enum_candidates)?;
}
}
for sub_ref in schema.one_of.iter().chain(schema.any_of.iter()) {
if let ObjectOrReference::Object(_sub) = sub_ref {
}
}
Ok(())
}
fn extract_enum_values(schema: &ObjectSchema) -> Option<Vec<String>> {
if !schema.enum_values.is_empty() {
let mut values: Vec<String> = schema
.enum_values
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
values.sort();
if !values.is_empty() {
return Some(values);
}
}
let has_string = schema.any_of.iter().chain(schema.one_of.iter()).any(|v| {
matches!(v, ObjectOrReference::Object(s) if s.schema_type == Some(SchemaTypeSet::Single(SchemaType::String)) && s.enum_values.is_empty())
});
if has_string {
let mut values = HashSet::new();
for variant in schema.any_of.iter().chain(schema.one_of.iter()) {
if let ObjectOrReference::Object(s) = variant {
for val in &s.enum_values {
if let Some(str_val) = val.as_str() {
values.insert(str_val.to_string());
}
}
if let Some(const_val) = s.const_value.as_ref().and_then(|v| v.as_str()) {
values.insert(const_val.to_string());
}
}
}
if !values.is_empty() {
let mut sorted: Vec<_> = values.into_iter().collect();
sorted.sort();
return Some(sorted);
}
}
None
}
fn is_inline_target(schema: &ObjectSchema) -> bool {
if !schema.enum_values.is_empty() {
return true;
}
if !schema.one_of.is_empty() || !schema.any_of.is_empty() {
return true;
}
if !schema.properties.is_empty() && schema.additional_properties.is_none() {
return true;
}
false
}
fn compute_best_name(candidates: &BTreeSet<String>, used_names: &BTreeSet<String>) -> String {
let candidate_vec: Vec<&String> = candidates.iter().collect();
if candidate_vec.is_empty() {
return "UnknownType".to_string(); }
if candidate_vec.len() == 1 {
let name = candidate_vec[0];
if !used_names.contains(name) || candidates.contains(name) {
return name.clone();
}
return Self::ensure_unique(name, used_names);
}
for candidate in &candidate_vec {
if used_names.contains(*candidate) {
return (*candidate).clone();
}
}
let lcs = Self::longest_common_suffix(&candidate_vec);
if Self::is_valid_common_name(&lcs) {
if !used_names.contains(&lcs) || candidates.contains(&lcs) {
return lcs;
}
let unique_lcs = Self::ensure_unique(&lcs, used_names);
return unique_lcs;
}
let first = candidate_vec[0];
if !used_names.contains(first) || candidates.contains(first) {
return first.clone();
}
Self::ensure_unique(first, used_names)
}
fn longest_common_suffix(strings: &[&String]) -> String {
if strings.is_empty() {
return String::new();
}
let first = strings[0];
let mut longest_suffix = String::new();
for i in 1..=first.len() {
let suffix = &first[first.len() - i..];
if strings.iter().all(|s| s.ends_with(suffix)) {
longest_suffix = suffix.to_string();
} else {
break;
}
}
longest_suffix
}
fn is_valid_common_name(name: &str) -> bool {
if name.len() < 4 {
return false;
}
if name == "Enum" || name == "Struct" || name == "Type" {
return false;
}
if !name.chars().next().unwrap().is_uppercase() {
return false;
}
if FORBIDDEN_IDENTIFIERS.contains(name) {
return false;
}
true
}
fn ensure_unique(base_name: &str, used_names: &BTreeSet<String>) -> String {
if !used_names.contains(base_name) {
return base_name.to_string();
}
let mut i = 2;
loop {
let new_name = format!("{base_name}{i}");
if !used_names.contains(&new_name) {
return new_name;
}
i += 1;
}
}
}