Skip to content

Commit 7715cb5

Browse files
Merge pull request #53 from gpu-cli/fix/schema-name-collisions
fix: disambiguate colliding schema type names
2 parents 0f83ca5 + 8dae7a7 commit 7715cb5

3 files changed

Lines changed: 471 additions & 19 deletions

File tree

src/analysis.rs

Lines changed: 370 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,8 @@ impl SchemaAnalyzer {
10901090
/// Construct an analyzer with a caller-supplied [`TypeMapper`]
10911091
/// (built from `GeneratorConfig.types`). The CLI / library entry
10921092
/// points use this so user TOML config drives type generation.
1093-
pub fn with_type_mapper(openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1093+
pub fn with_type_mapper(mut openapi_spec: Value, type_mapper: TypeMapper) -> Result<Self> {
1094+
disambiguate_component_schema_names(&mut openapi_spec);
10941095
let spec: OpenApiSpec =
10951096
serde_json::from_value(openapi_spec.clone()).map_err(GeneratorError::ParseError)?;
10961097
let schemas = Self::extract_schemas(&spec)?;
@@ -1342,6 +1343,8 @@ impl SchemaAnalyzer {
13421343
}
13431344
}
13441345

1346+
disambiguate_analyzed_schema_names(&mut analysis, &self.schemas);
1347+
13451348
// Snapshot the type-mapper's used-features set so the
13461349
// generator can decide which helper modules to emit
13471350
// (e.g. base64_serde for `format: byte`).
@@ -5870,3 +5873,369 @@ impl SchemaAnalyzer {
58705873
}
58715874
}
58725875
}
5876+
5877+
fn disambiguate_component_schema_names(openapi_spec: &mut Value) {
5878+
let Some(schemas) = openapi_spec
5879+
.pointer_mut("/components/schemas")
5880+
.and_then(Value::as_object_mut)
5881+
else {
5882+
return;
5883+
};
5884+
5885+
let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
5886+
for name in schemas.keys() {
5887+
names_by_rust_name
5888+
.entry(crate::generator::rust_type_name(name))
5889+
.or_default()
5890+
.push(name.clone());
5891+
}
5892+
5893+
// Reserve every identifier already represented by the document so a
5894+
// suffix never steals another component's canonical Rust name.
5895+
let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
5896+
let mut aliases = BTreeMap::<String, String>::new();
5897+
5898+
for (rust_name, mut names) in names_by_rust_name {
5899+
if names.len() < 2 {
5900+
continue;
5901+
}
5902+
5903+
// Prefer an already-canonical component key (for example `Alert`
5904+
// over `alert`), then use lexical order for deterministic results.
5905+
names.sort_by_key(|name| (name != &rust_name, name.clone()));
5906+
for source_name in names.into_iter().skip(1) {
5907+
let mut suffix = 2;
5908+
let replacement = loop {
5909+
let candidate = format!("{rust_name}{suffix}");
5910+
if claimed_rust_names.insert(candidate.clone()) {
5911+
break candidate;
5912+
}
5913+
suffix += 1;
5914+
};
5915+
5916+
eprintln!(
5917+
"⚠️ schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
5918+
);
5919+
aliases.insert(source_name, replacement);
5920+
}
5921+
}
5922+
5923+
if aliases.is_empty() {
5924+
return;
5925+
}
5926+
5927+
let original_schemas = std::mem::take(schemas);
5928+
for (name, schema) in original_schemas {
5929+
schemas.insert(aliases.get(&name).cloned().unwrap_or(name), schema);
5930+
}
5931+
5932+
rewrite_component_schema_references(openapi_spec, &aliases);
5933+
}
5934+
5935+
fn disambiguate_analyzed_schema_names(
5936+
analysis: &mut SchemaAnalysis,
5937+
component_schemas: &BTreeMap<String, Schema>,
5938+
) {
5939+
let mut names_by_rust_name = BTreeMap::<String, Vec<String>>::new();
5940+
for name in analysis.schemas.keys() {
5941+
names_by_rust_name
5942+
.entry(crate::generator::rust_type_name(name))
5943+
.or_default()
5944+
.push(name.clone());
5945+
}
5946+
5947+
let mut claimed_rust_names = names_by_rust_name.keys().cloned().collect::<HashSet<_>>();
5948+
let mut aliases = BTreeMap::<String, String>::new();
5949+
5950+
for (rust_name, mut names) in names_by_rust_name {
5951+
if names.len() < 2 {
5952+
continue;
5953+
}
5954+
names.sort_by_key(|name| {
5955+
(
5956+
!component_schemas.contains_key(name),
5957+
name != &rust_name,
5958+
name.clone(),
5959+
)
5960+
});
5961+
5962+
for source_name in names.into_iter().skip(1) {
5963+
let mut suffix = 2;
5964+
let replacement = loop {
5965+
let candidate = format!("{rust_name}{suffix}");
5966+
if claimed_rust_names.insert(candidate.clone()) {
5967+
break candidate;
5968+
}
5969+
suffix += 1;
5970+
};
5971+
eprintln!(
5972+
"⚠️ generated schema `{source_name}` maps to the existing Rust type `{rust_name}` — disambiguated to `{replacement}`"
5973+
);
5974+
aliases.insert(source_name, replacement);
5975+
}
5976+
}
5977+
5978+
if aliases.is_empty() {
5979+
return;
5980+
}
5981+
5982+
let original_schemas = std::mem::take(&mut analysis.schemas);
5983+
for (name, mut schema) in original_schemas {
5984+
schema.name = renamed_schema_name(&schema.name, &aliases);
5985+
schema.dependencies = schema
5986+
.dependencies
5987+
.into_iter()
5988+
.map(|name| renamed_schema_name(&name, &aliases))
5989+
.collect();
5990+
rewrite_schema_type_names(&mut schema.schema_type, &aliases);
5991+
analysis
5992+
.schemas
5993+
.insert(renamed_schema_name(&name, &aliases), schema);
5994+
}
5995+
5996+
let original_edges = std::mem::take(&mut analysis.dependencies.edges);
5997+
for (name, dependencies) in original_edges {
5998+
analysis.dependencies.edges.insert(
5999+
renamed_schema_name(&name, &aliases),
6000+
dependencies
6001+
.into_iter()
6002+
.map(|name| renamed_schema_name(&name, &aliases))
6003+
.collect(),
6004+
);
6005+
}
6006+
analysis.dependencies.recursive_schemas = analysis
6007+
.dependencies
6008+
.recursive_schemas
6009+
.iter()
6010+
.map(|name| renamed_schema_name(name, &aliases))
6011+
.collect();
6012+
6013+
analysis.patterns.tagged_enum_schemas = analysis
6014+
.patterns
6015+
.tagged_enum_schemas
6016+
.iter()
6017+
.map(|name| renamed_schema_name(name, &aliases))
6018+
.collect();
6019+
analysis.patterns.untagged_enum_schemas = analysis
6020+
.patterns
6021+
.untagged_enum_schemas
6022+
.iter()
6023+
.map(|name| renamed_schema_name(name, &aliases))
6024+
.collect();
6025+
analysis.patterns.type_mappings = std::mem::take(&mut analysis.patterns.type_mappings)
6026+
.into_iter()
6027+
.map(|(name, mappings)| {
6028+
(
6029+
renamed_schema_name(&name, &aliases),
6030+
mappings
6031+
.into_iter()
6032+
.map(|(value, schema_name)| {
6033+
(value, renamed_schema_name(&schema_name, &aliases))
6034+
})
6035+
.collect(),
6036+
)
6037+
})
6038+
.collect();
6039+
6040+
for operation in analysis.operations.values_mut() {
6041+
if let Some(request_body) = &mut operation.request_body {
6042+
rewrite_request_body_schema_name(request_body, &aliases);
6043+
}
6044+
for schema_name in operation.response_schemas.values_mut() {
6045+
*schema_name = renamed_schema_name(schema_name, &aliases);
6046+
}
6047+
for parameter in &mut operation.parameters {
6048+
if let Some(schema_name) = &mut parameter.schema_ref {
6049+
*schema_name = renamed_schema_name(schema_name, &aliases);
6050+
}
6051+
if let Some(serialization) = &mut parameter.query_serialization {
6052+
rewrite_query_serialization_schema_names(serialization, &aliases);
6053+
}
6054+
}
6055+
}
6056+
6057+
for responses in analysis.operation_responses.values_mut() {
6058+
for response in responses.values_mut() {
6059+
if let Some(schema_name) = &mut response.schema_name {
6060+
*schema_name = renamed_schema_name(schema_name, &aliases);
6061+
}
6062+
if let Some(OperationResponseBody::Json { schema_name, .. }) = &mut response.body {
6063+
*schema_name = renamed_schema_name(schema_name, &aliases);
6064+
}
6065+
}
6066+
}
6067+
}
6068+
6069+
fn renamed_schema_name(name: &str, aliases: &BTreeMap<String, String>) -> String {
6070+
aliases
6071+
.get(name)
6072+
.cloned()
6073+
.unwrap_or_else(|| name.to_string())
6074+
}
6075+
6076+
fn rewrite_schema_type_names(schema_type: &mut SchemaType, aliases: &BTreeMap<String, String>) {
6077+
match schema_type {
6078+
SchemaType::Object {
6079+
properties,
6080+
additional_properties,
6081+
..
6082+
} => {
6083+
for property in properties.values_mut() {
6084+
rewrite_schema_type_names(&mut property.schema_type, aliases);
6085+
}
6086+
if let ObjectAdditionalProperties::Typed { value_type } = additional_properties {
6087+
rewrite_schema_type_names(value_type, aliases);
6088+
}
6089+
}
6090+
SchemaType::DiscriminatedUnion { variants, .. } => {
6091+
for variant in variants {
6092+
variant.type_name = renamed_schema_name(&variant.type_name, aliases);
6093+
variant.schema_ref = renamed_schema_name(&variant.schema_ref, aliases);
6094+
}
6095+
}
6096+
SchemaType::Union { variants } | SchemaType::Composition { schemas: variants } => {
6097+
for variant in variants {
6098+
variant.target = renamed_schema_name(&variant.target, aliases);
6099+
}
6100+
}
6101+
SchemaType::Array { item_type } => rewrite_schema_type_names(item_type, aliases),
6102+
SchemaType::Reference { target } => {
6103+
*target = renamed_schema_name(target, aliases);
6104+
}
6105+
SchemaType::Primitive { .. }
6106+
| SchemaType::StringEnum { .. }
6107+
| SchemaType::ExtensibleEnum { .. } => {}
6108+
}
6109+
}
6110+
6111+
fn rewrite_request_body_schema_name(
6112+
request_body: &mut RequestBodyContent,
6113+
aliases: &BTreeMap<String, String>,
6114+
) {
6115+
match request_body {
6116+
RequestBodyContent::Json { schema_name, .. }
6117+
| RequestBodyContent::FormUrlEncoded { schema_name, .. }
6118+
| RequestBodyContent::Multipart { schema_name, .. } => {
6119+
*schema_name = renamed_schema_name(schema_name, aliases);
6120+
}
6121+
_ => {}
6122+
}
6123+
}
6124+
6125+
fn rewrite_query_serialization_schema_names(
6126+
serialization: &mut QuerySerialization,
6127+
aliases: &BTreeMap<String, String>,
6128+
) {
6129+
match serialization {
6130+
QuerySerialization::FormExplodedArray { item_type }
6131+
| QuerySerialization::FormArray { item_type }
6132+
| QuerySerialization::SimpleHeaderArray { item_type } => {
6133+
rewrite_array_item_type_schema_names(item_type, aliases);
6134+
}
6135+
QuerySerialization::FormExplodedNestedObject { properties } => {
6136+
for property in properties {
6137+
rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6138+
}
6139+
}
6140+
_ => {}
6141+
}
6142+
}
6143+
6144+
fn rewrite_array_item_type_schema_names(
6145+
item_type: &mut ArrayItemType,
6146+
aliases: &BTreeMap<String, String>,
6147+
) {
6148+
match item_type {
6149+
ArrayItemType::SchemaRef(name) => *name = renamed_schema_name(name, aliases),
6150+
ArrayItemType::FlatStructRef {
6151+
schema_name,
6152+
properties,
6153+
}
6154+
| ArrayItemType::NestedStructRef {
6155+
schema_name,
6156+
properties,
6157+
} => {
6158+
*schema_name = renamed_schema_name(schema_name, aliases);
6159+
for property in properties {
6160+
rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6161+
}
6162+
}
6163+
ArrayItemType::Scalar(_) => {}
6164+
}
6165+
}
6166+
6167+
fn rewrite_query_property_type_schema_names(
6168+
property_type: &mut QueryStructPropertyType,
6169+
aliases: &BTreeMap<String, String>,
6170+
) {
6171+
match property_type {
6172+
QueryStructPropertyType::Array { item_type } => {
6173+
rewrite_array_item_type_schema_names(item_type, aliases)
6174+
}
6175+
QueryStructPropertyType::Object { properties } => {
6176+
for property in properties {
6177+
rewrite_query_property_type_schema_names(&mut property.value_type, aliases);
6178+
}
6179+
}
6180+
QueryStructPropertyType::Scalar(_) => {}
6181+
}
6182+
}
6183+
6184+
fn rewrite_component_schema_references(value: &mut Value, aliases: &BTreeMap<String, String>) {
6185+
match value {
6186+
Value::Array(values) => {
6187+
for value in values {
6188+
rewrite_component_schema_references(value, aliases);
6189+
}
6190+
}
6191+
Value::Object(object) => {
6192+
if let Some(Value::String(reference)) = object.get_mut("$ref") {
6193+
rewrite_component_schema_reference(reference, aliases);
6194+
}
6195+
6196+
if let Some(Value::Object(mapping)) = object.get_mut("mapping") {
6197+
for target_value in mapping.values_mut() {
6198+
let Some(target) = target_value.as_str() else {
6199+
continue;
6200+
};
6201+
let replacement = aliases.get(target).cloned().or_else(|| {
6202+
let mut target = target.to_string();
6203+
rewrite_component_schema_reference(&mut target, aliases).then_some(target)
6204+
});
6205+
if let Some(replacement) = replacement {
6206+
*target_value = Value::String(replacement);
6207+
}
6208+
}
6209+
}
6210+
6211+
for value in object.values_mut() {
6212+
rewrite_component_schema_references(value, aliases);
6213+
}
6214+
}
6215+
_ => {}
6216+
}
6217+
}
6218+
6219+
fn rewrite_component_schema_reference(
6220+
reference: &mut String,
6221+
aliases: &BTreeMap<String, String>,
6222+
) -> bool {
6223+
const PREFIX: &str = "#/components/schemas/";
6224+
let Some(encoded_name) = reference.strip_prefix(PREFIX) else {
6225+
return false;
6226+
};
6227+
let encoded_name = encoded_name.split('/').next().unwrap_or(encoded_name);
6228+
6229+
for (source, replacement) in aliases {
6230+
let encoded_source = source.replace('~', "~0").replace('/', "~1");
6231+
if encoded_name == encoded_source {
6232+
reference.replace_range(
6233+
PREFIX.len()..PREFIX.len() + encoded_source.len(),
6234+
replacement,
6235+
);
6236+
return true;
6237+
}
6238+
}
6239+
6240+
false
6241+
}

0 commit comments

Comments
 (0)