From 980efcc44db3b5292d90368ab4a49c0dd95d7158 Mon Sep 17 00:00:00 2001 From: z23cc Date: Wed, 13 May 2026 11:59:13 +0800 Subject: [PATCH] fix: avoid duplicate nullable type emission --- typify-impl/src/convert.rs | 2 +- typify-impl/src/lib.rs | 12 +++-- typify-impl/tests/test_generation.rs | 71 +++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index f97b31b9..3d0a107b 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -96,7 +96,7 @@ impl TypeSpace { // new name for the inner type; otherwise, the inner type // can just have this name. let inner_type_name = match &type_name { - Name::Required(name) => Name::Suggested(format!("{}Inner", name)), + Name::Required(name) => Name::Required(format!("{}Inner", name)), _ => type_name, }; self.convert_option(inner_type_name, metadata, &ss) diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index a9cf8c24..f0e07b42 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -915,9 +915,15 @@ impl TypeSpace { ); // Add all types. - self.id_to_entry - .values() - .for_each(|type_entry| type_entry.output(self, &mut output)); + let mut output_names = BTreeSet::new(); + self.id_to_entry.values().for_each(|type_entry| { + if type_entry + .name() + .is_none_or(|name| output_names.insert(name.clone())) + { + type_entry.output(self, &mut output); + } + }); // Add all shared default functions. self.defaults diff --git a/typify-impl/tests/test_generation.rs b/typify-impl/tests/test_generation.rs index 64394a63..9c60211b 100644 --- a/typify-impl/tests/test_generation.rs +++ b/typify-impl/tests/test_generation.rs @@ -1,7 +1,7 @@ // Copyright 2022 Oxide Computer Company use quote::quote; -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +use schemars::{gen::SchemaGenerator, schema::RootSchema, schema::Schema, JsonSchema}; use serde::Serialize; use typify_impl::{TypeSpace, TypeSpacePatch, TypeSpaceSettings}; @@ -112,3 +112,72 @@ fn test_generation() { expectorate::assert_contents("tests/generator.out", fmt.as_str()); } + +#[test] +fn test_required_nullable_object_with_title_uses_distinct_inner_name() { + let schema: RootSchema = serde_json::from_value(serde_json::json!({ + "definitions": { + "aaa-wrapper": { + "type": "object", + "properties": { + "author_association": { + "title": "author_association", + "type": "string", + "enum": ["OWNER", "MEMBER"] + } + } + }, + "author-association": { + "title": "author_association", + "type": "string", + "enum": ["OWNER", "MEMBER"] + }, + "simple-user": { + "title": "Simple User", + "type": "object", + "properties": { + "login": { + "type": "string" + } + }, + "required": ["login"] + }, + "auto-merge": { + "title": "Auto merge", + "type": ["object", "null"], + "properties": { + "enabled_by": { + "$ref": "#/definitions/simple-user" + }, + "merge_method": { + "type": "string", + "enum": ["merge", "squash", "rebase"] + }, + "commit_title": { + "type": "string" + }, + "commit_message": { + "type": "string" + } + }, + "required": [ + "enabled_by", + "merge_method", + "commit_title", + "commit_message" + ] + } + } + })) + .unwrap(); + + let mut type_space = TypeSpace::default(); + type_space.add_root_schema(schema).unwrap(); + + let fmt = rustfmt_wrapper::rustfmt(type_space.to_stream().to_string()).unwrap(); + + assert!(fmt.contains("pub struct AutoMerge(pub ::std::option::Option);")); + assert!(fmt.contains("pub struct AutoMergeInner {")); + assert!(!fmt.contains("pub struct AutoMerge(pub ::std::option::Option);")); + assert_eq!(fmt.matches("pub enum AuthorAssociation {").count(), 1); +}