@@ -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