Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "utoipa-gen"
description = "Code generation implementation for utoipa"
version = "5.3.1"
version = "5.3.2"
edition = "2021"
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand All @@ -27,7 +27,7 @@ once_cell = { version = "1.19.0", optional = true }
proc-macro2 = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
regex = { version = "1.7", optional = true }
regex = "1.7"
uuid = { version = "1", features = ["serde"], optional = true }
ulid = { version = "1", optional = true, default-features = false }
url = { version = "2", optional = true }
Expand Down Expand Up @@ -57,17 +57,17 @@ insta = { version = "1.41", features = ["json"] }
[features]
# See README.md for list and explanations of features
debug = ["syn/extra-traits"]
actix_extras = ["regex", "syn/extra-traits"]
actix_extras = ["syn/extra-traits"]
chrono = []
yaml = []
decimal = []
decimal_float = []
rocket_extras = ["regex", "syn/extra-traits"]
rocket_extras = ["syn/extra-traits"]
non_strict_integers = []
uuid = ["dep:uuid"]
ulid = ["dep:ulid"]
url = ["dep:url"]
axum_extras = ["regex", "syn/extra-traits"]
axum_extras = ["syn/extra-traits"]
time = []
smallvec = []
repr = []
Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ impl ToTokens for ComponentDescription<'_> {
/// references. E.g. field: Vec<Foo> should have name: Foo::name(), tokens: Foo::schema() and
/// references: Foo::schemas()
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Default)]
#[derive(Default, Clone)]
pub struct SchemaReference {
pub name: TokenStream,
pub tokens: TokenStream,
Expand Down
195 changes: 185 additions & 10 deletions utoipa-gen/src/component/schema/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ impl<'p> MixedEnum<'p> {
.note("Read more about discriminators from the specs <https://spec.openapis.org/oas/latest.html#discriminator-object>"));
}

let mut items = variants
// First, process all variants normally
let all_content = variants
.into_iter()
.map(|(variant, variant_serde_rules, mut variant_features)| {
if features
Expand All @@ -342,16 +343,189 @@ impl<'p> MixedEnum<'p> {
{
variant_features.push(Feature::NoRecursion(NoRecursion));
}
MixedEnumContent::new(
variant,
root,
&container_rules,
rename_all.as_ref(),
variant_serde_rules,
variant_features,
)

(variant, variant_serde_rules, variant_features)
})
.collect::<Result<Vec<MixedEnumContent>, Diagnostics>>()?;
.collect::<Vec<_>>();

// Extract unit variants
let mut unit_variants = Vec::new();
let mut non_unit_variants = Vec::new();

for (variant, variant_serde_rules, variant_features) in all_content {
match &variant.fields {
Fields::Unit => {
// For unit variants, create MixedEnumContent immediately
let name = variant.ident.to_string();
let tokens = MixedEnumContent::get_unit_tokens(
name,
variant_features,
&container_rules,
variant_serde_rules,
rename_all.as_ref(),
);

unit_variants.push(MixedEnumContent {
tokens,
schema_references: Vec::new(),
});
},
_ => {
let content = MixedEnumContent::new(
variant,
root,
&container_rules,
rename_all.as_ref(),
variant_serde_rules,
variant_features,
)?;
non_unit_variants.push(content);
}
}
}

// Group simple unit variants (ones without title or description)
let mut items = Vec::new();

// Start with non-unit variants
items.extend(non_unit_variants);

if unit_variants.len() > 1 {
// Split unit variants into simple (can be grouped) and complex (needs to stay separate)
let (simple, complex): (Vec<_>, Vec<_>) = unit_variants.iter()
.partition(|content| {
let tokens_str = content.tokens.to_string();
// Simple variants don't have title or description attributes
!tokens_str.contains("title") && !tokens_str.contains("description")
});

// Extract the names of simple unit variants
let mut simple_names = Vec::new();
let mut first_simple_index = None;
let mut simple_values = std::collections::HashSet::new();

for (i, content) in unit_variants.iter().enumerate() {
let tokens_str = content.tokens.to_string();

// Skip complex variants
if tokens_str.contains("title") || tokens_str.contains("description") {
continue;
}

// Extract the enum value
if let Some(pos) = tokens_str.find("enum_values") {
let after_enum = &tokens_str[pos..];
if let Some(quote_pos) = after_enum.find('\"') {
if let Some(quote_end) = after_enum[quote_pos + 1..].find('\"') {
let value = &after_enum[quote_pos + 1..quote_pos + 1 + quote_end];
simple_names.push(quote! { #value });
simple_values.insert(value.to_string());

// Record the position of the first simple unit variant
if first_simple_index.is_none() {
first_simple_index = Some(i);
}
}
}
}
}

// Only combine if we have multiple simple unit variants
if simple_names.len() > 1 {
// Create the combined enum
let enum_tokens = match &container_rules.enum_repr {
SerdeEnumRepr::ExternallyTagged => {
let items = Array::Owned(simple_names);
let schema_type = PlainSchema::get_default_types().0;
let enum_type = PlainSchema::get_default_types().1;
EnumSchema::<PlainSchema>::with_types(
Roo::Owned(items),
schema_type,
enum_type,
)
.to_token_stream()
},
SerdeEnumRepr::InternallyTagged { tag } => {
let items = Array::Owned(simple_names);
let schema_type = PlainSchema::get_default_types().0;
let enum_type = PlainSchema::get_default_types().1;
EnumSchema::<PlainSchema>::with_types(
Roo::Owned(items),
schema_type,
enum_type,
)
.tagged(tag)
.to_token_stream()
},
SerdeEnumRepr::Untagged => {
let v: EnumSchema = EnumSchema::untagged();
v.to_token_stream()
},
SerdeEnumRepr::AdjacentlyTagged { tag, .. } => {
let items = Array::Owned(simple_names);
let schema_type = PlainSchema::get_default_types().0;
let enum_type = PlainSchema::get_default_types().1;
EnumSchema::<PlainSchema>::with_types(
Roo::Owned(items),
schema_type,
enum_type,
)
.tagged(tag)
.to_token_stream()
},
SerdeEnumRepr::UnfinishedAdjacentlyTagged { .. } => unreachable!(
"Invalid serde enum repr, serde should have panicked before reaching here"
),
};

// The combined variant to insert
let combined_variant = MixedEnumContent {
tokens: enum_tokens,
schema_references: Vec::new(),
};

// Add variants in order, replacing simple ones with the combined variant
let mut has_inserted_combined = false;

// For each original unit variant
for (i, content) in unit_variants.into_iter().enumerate() {
let tokens_str = content.tokens.to_string();
let is_simple = if let Some(pos) = tokens_str.find("enum_values") {
let after_enum = &tokens_str[pos..];
if let Some(quote_pos) = after_enum.find('\"') {
if let Some(quote_end) = after_enum[quote_pos + 1..].find('\"') {
let value = after_enum[quote_pos + 1..quote_pos + 1 + quote_end].to_string();
simple_values.contains(&value)
} else {
false
}
} else {
false
}
} else {
false
};

if is_simple {
// Insert the combined variant in place of the first simple variant
if !has_inserted_combined {
items.push(combined_variant.clone());
has_inserted_combined = true;
}
// Skip this variant as it's included in the combined one
} else {
// Keep complex variants
items.push(content);
}
}
} else {
// Just one or zero simple variants, no need to combine
items.extend(unit_variants);
}
} else {
// Just one unit variant total, no need for special handling
items.extend(unit_variants);
}

let schema_references = items
.iter_mut()
Expand Down Expand Up @@ -383,6 +557,7 @@ impl ToTokens for MixedEnum<'_> {
}

#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
struct MixedEnumContent {
tokens: TokenStream,
schema_references: Vec<SchemaReference>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: utoipa-gen/tests/schema_derive_test.rs
assertion_line: 2148
expression: doc
---
{
"oneOf": [
{
"properties": {
"A": {
"properties": {
"foo": {
"$ref": "#/components/schemas/Foo"
}
},
"required": [
"foo"
],
"type": "object"
}
},
"required": [
"A"
],
"type": "object"
},
{
"enum": [
"B",
"C"
],
"type": "string"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
source: utoipa-gen/tests/schema_derive_test.rs
assertion_line: 922
expression: value
---
{
"oneOf": [
{
"properties": {
"NamedFields": {
"properties": {
"id": {
"type": "string"
},
"names": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
}
},
"required": [
"id"
],
"type": "object"
}
},
"required": [
"NamedFields"
],
"type": "object"
},
{
"properties": {
"UnnamedFields": {
"$ref": "#/components/schemas/Foo"
}
},
"required": [
"UnnamedFields"
],
"type": "object"
},
{
"enum": [
"UnitValue"
],
"type": "string"
}
]
}
Loading