diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index c4323534..89c060f5 100644 --- a/utoipa-gen/CHANGELOG.md +++ b/utoipa-gen/CHANGELOG.md @@ -495,7 +495,7 @@ * Add derive info support for derive OpenApi (https://github.com/juhaku/utoipa/pull/400) * Add `merge` functionality for `OpenApi` (https://github.com/juhaku/utoipa/pull/397) * Add derive servers attribute for OpenApi (https://github.com/juhaku/utoipa/pull/395) -* Add support for unit sructs (https://github.com/juhaku/utoipa/pull/392) +* Add support for unit structs (https://github.com/juhaku/utoipa/pull/392) * Add support for `schema_with` custom fn reference (https://github.com/juhaku/utoipa/pull/390) * Add support for multiple serde definitions (https://github.com/juhaku/utoipa/pull/389) * Add support for tuple Path parameters for axum (https://github.com/juhaku/utoipa/pull/388) @@ -534,7 +534,7 @@ * Update `ToSchema` documentation * Chore make `serde_json` mandatory dependency (https://github.com/juhaku/utoipa/pull/378) * Feature http status codes (https://github.com/juhaku/utoipa/pull/376) -* Refactor some path derive with `IntoParmas` tests +* Refactor some path derive with `IntoParams` tests * Chore refine description attribute (https://github.com/juhaku/utoipa/pull/373) * cargo format * Update to axum 0.6.0 (https://github.com/juhaku/utoipa/pull/369) diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 96328ac8..d36967d9 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -22,6 +22,7 @@ include = [ proc-macro = true [dependencies] +desynt = "0.1" utoipa-config = { version = "0.1", path = "../utoipa-config", optional = true } once_cell = { version = "1.19.0", optional = true } proc-macro2 = "1.0" diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 90bce51d..9cf3398e 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use desynt::StripRaw; use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use syn::punctuated::Punctuated; @@ -439,7 +440,7 @@ impl TypeTree<'_> { return None; } - match &*segment.ident.to_string() { + match &*segment.ident.strip_raw().to_string() { "HashMap" | "Map" | "BTreeMap" => Some(GenericType::Map), #[cfg(feature = "indexmap")] "IndexMap" => Some(GenericType::Map), @@ -568,7 +569,9 @@ impl TypeTree<'_> { .as_ref() .and_then(|path| path.segments.iter().last()) .and_then(|last_segment| { - crate::CONFIG.aliases.get(&*last_segment.ident.to_string()) + crate::CONFIG + .aliases + .get(&*last_segment.ident.strip_raw().to_string()) }) .map_try(|alias| syn::parse_str::(alias.as_ref())) .map_err(|error| Diagnostics::new(error.to_string())) diff --git a/utoipa-gen/src/component/features/attributes.rs b/utoipa-gen/src/component/features/attributes.rs index cb343b2f..0e1e3e7f 100644 --- a/utoipa-gen/src/component/features/attributes.rs +++ b/utoipa-gen/src/component/features/attributes.rs @@ -1,5 +1,6 @@ use std::mem; +use desynt::StripRaw; use proc_macro2::{Ident, TokenStream}; use quote::ToTokens; use syn::parse::ParseStream; @@ -682,7 +683,7 @@ impl As { .path .segments .iter() - .map(|segment| segment.ident.to_string()) + .map(|segment| segment.ident.strip_raw().to_string()) .collect::>() .join(".") } diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index 3871cd80..fff6a9c4 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use desynt::StripRaw; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; use syn::{ @@ -323,19 +324,15 @@ impl Param { let mut tokens = TokenStream::new(); let field_serde_params = &field_serde_params; let ident = &field.ident; - let mut name = &*ident + let name = &*ident .as_ref() - .map(|ident| ident.to_string()) + .map(|ident| ident.strip_raw().to_string()) .or_else(|| container_attributes.name.cloned()) .ok_or_else(|| Diagnostics::with_span(field.span(), "No name specified for unnamed field.") .help("Try adding #[into_params(names(...))] container attribute to specify the name for this field") )?; - if name.starts_with("r#") { - name = &name[2..]; - } - let (schema_features, mut param_features) = Param::resolve_field_features(field_features, &container_attributes) .map_err(Diagnostics::from)?; diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 9f09bc2b..c8766484 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -1,5 +1,6 @@ use std::borrow::{Borrow, Cow}; +use desynt::StripRaw; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use syn::{ @@ -347,11 +348,8 @@ impl NamedStructSchema { let mut fields_vec = fields .iter() .filter_map(|field| { - let mut field_name = Cow::Owned(field.ident.as_ref().unwrap().to_string()); - - if Borrow::::borrow(&field_name).starts_with("r#") { - field_name = Cow::Owned(field_name[2..].to_string()); - } + let field_ident = field.ident.as_ref().unwrap(); + let field_name: Cow<'_, str> = Cow::Owned(field_ident.strip_raw().to_string()); let field_rules = serde::parse_value(&field.attrs); let field_rules = match field_rules { @@ -500,11 +498,11 @@ impl NamedStructSchema { Some(flattened_map_field) => { return Err(Diagnostics::with_span( fields.span(), - format!("The structure `{}` contains multiple flattened map fields.", root.ident)) + format!("The structure `{}` contains multiple flattened map fields.", root.ident.strip_raw())) .note( format!("first flattened map field was declared here as `{}`", - flattened_map_field.ident.as_ref().unwrap())) - .note(format!("second flattened map field was declared here as `{}`", field.ident.as_ref().unwrap())) + flattened_map_field.ident.as_ref().unwrap().strip_raw())) + .note(format!("second flattened map field was declared here as `{}`", field.ident.as_ref().unwrap().strip_raw())) ); } } diff --git a/utoipa-gen/src/component/schema/enums.rs b/utoipa-gen/src/component/schema/enums.rs index 224925ce..5f56bf62 100644 --- a/utoipa-gen/src/component/schema/enums.rs +++ b/utoipa-gen/src/component/schema/enums.rs @@ -1,5 +1,6 @@ use std::{borrow::Cow, ops::Deref}; +use desynt::StripRaw; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{punctuated::Punctuated, spanned::Spanned, token::Comma, Fields, TypePath, Variant}; @@ -103,7 +104,7 @@ impl<'e> PlainEnum<'e> { .collect::, Diagnostics>>()? .into_iter() .map(|(variant, variant_rules, mut variant_features)| { - let name = &*variant.ident.to_string(); + let name = &*variant.ident.strip_raw().to_string(); let renamed = super::rename_enum_variant( name, &mut variant_features, @@ -398,7 +399,7 @@ impl MixedEnumContent { mut variant_features: Vec, ) -> Result { let mut tokens = TokenStream::new(); - let name = variant.ident.to_string(); + let name = variant.ident.strip_raw().to_string(); // TODO support `description = ...` attribute via Feature::Description // let description = // pop_feature!(variant_features => Feature::Description(_) as Option); diff --git a/utoipa-gen/src/ext.rs b/utoipa-gen/src/ext.rs index ce8400ad..0ead1060 100644 --- a/utoipa-gen/src/ext.rs +++ b/utoipa-gen/src/ext.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use desynt::StripRaw; use proc_macro2::TokenStream; use quote::ToTokens; use syn::spanned::Spanned; @@ -123,7 +124,7 @@ impl ExtSchema<'_> { } else { Cow::Borrowed(actual_body) } - }).expect("ExtSchema must have actual request body resoved from TypeTree of handler fn argument") + }).expect("ExtSchema must have actual request body resolved from TypeTree of handler fn argument") } pub fn get_type_tree(&self) -> Result>>, Diagnostics> { @@ -182,7 +183,7 @@ fn get_actual_body_type<'t>(ty: &'t TypeTree<'t>) -> Option<&'t TypeTree<'t>> { .expect("RequestBody TypeTree must have syn::Path") .segments .iter() - .find_map(|segment| match &*segment.ident.to_string() { + .find_map(|segment| match &*segment.ident.strip_raw().to_string() { "Json" => Some( ty.children .as_deref() diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 1aabca7b..d1c799d0 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -135,7 +135,7 @@ static CONFIG: once_cell::sync::Lazy = /// * `min_properties = ...` Can be used to define minimum number of properties this struct can /// contain. Value must be a number. ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On /// struct level the _`no_recursion`_ rule will be applied to all of its fields. /// @@ -202,7 +202,7 @@ static CONFIG: once_cell::sync::Lazy = ///* `ignore` or `ignore = ...` Can be used to skip the field from being serialized to OpenAPI schema. It accepts either a literal `bool` value /// or a path to a function that returns `bool` (`Fn() -> bool`). ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. /// /// #### Field nullability and required rules @@ -267,7 +267,7 @@ static CONFIG: once_cell::sync::Lazy = /// * `content_media_type = ...` Can be used to define MIME type of a string for underlying schema object. /// See [`Object::content_media_type`][schema_object_media_type] ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. /// /// # Enum Optional Configuration Options for `#[schema(...)]` @@ -335,7 +335,7 @@ static CONFIG: once_cell::sync::Lazy = /// field for enums with single unnamed _`ToSchema`_ reference field. See the [discriminator /// syntax][derive@ToSchema#schemadiscriminator-syntax]. ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On /// enum level the _`no_recursion`_ rule will be applied to all of its variants. /// @@ -391,7 +391,7 @@ static CONFIG: once_cell::sync::Lazy = /// * `min_properties = ...` Can be used to define minimum number of properties this struct can /// contain. Value must be a number. ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On /// named field variant level the _`no_recursion`_ rule will be applied to all of its fields. /// @@ -422,7 +422,7 @@ static CONFIG: once_cell::sync::Lazy = /// not in the code. If you'd like to mark the field as deprecated in the code as well use /// Rust's own `#[deprecated]` attribute instead. ///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` -> -/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow +/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Owner` type not to allow /// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. /// /// #### Mixed Enum Unnamed Field Variant's Field Configuration Options @@ -3771,7 +3771,7 @@ mod parse_utils { Punctuated::parse_terminated(&group) } - pub fn parse_comma_separated_within_parethesis_with( + pub fn parse_comma_separated_within_parenthesis_with( input: ParseStream, with: fn(ParseStream) -> syn::Result, ) -> syn::Result> diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs index 8ba84abf..9ca7bc67 100644 --- a/utoipa-gen/src/openapi.rs +++ b/utoipa-gen/src/openapi.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use desynt::StripRaw; use proc_macro2::Ident; use syn::{ bracketed, parenthesized, @@ -440,7 +441,7 @@ impl OpenApi<'_> { .segments .iter() .take(nest_api.path.segments.len() - 1) - .map(|segment| segment.ident.to_string()) + .map(|segment| segment.ident.strip_raw().to_string()) .collect::>() .join("::"); let tags = &item.tags.iter().collect::>(); @@ -696,7 +697,7 @@ fn impl_paths(handler_paths: Option<&Punctuated>) -> Paths { let segments = handler.path.segments.iter().collect::>(); let handler_config_name = segments .iter() - .map(|segment| segment.ident.to_string()) + .map(|segment| segment.ident.strip_raw().to_string()) .collect::>() .join("_"); let handler_fn = &segments.last().unwrap().ident; @@ -706,7 +707,7 @@ fn impl_paths(handler_paths: Option<&Punctuated>) -> Paths { let tag = segments .iter() .take(segments.len() - 1) - .map(|part| part.ident.to_string()) + .map(|part| part.ident.strip_raw().to_string()) .collect::>() .join("::"); @@ -759,7 +760,7 @@ fn impl_paths(handler_paths: Option<&Punctuated>) -> Paths { let segments = handler.path.segments.iter().collect::>(); let handler_config_name = segments .iter() - .map(|segment| segment.ident.to_string()) + .map(|segment| segment.ident.strip_raw().to_string()) .collect::>() .join("_"); let handler_ident_config = format_ident!("{}_config", handler_config_name); diff --git a/utoipa-gen/src/openapi/info.rs b/utoipa-gen/src/openapi/info.rs index aa908751..7922b7d1 100644 --- a/utoipa-gen/src/openapi/info.rs +++ b/utoipa-gen/src/openapi/info.rs @@ -412,19 +412,19 @@ mod tests { #[test] fn contact_from_only_name() { let author = "Suzy Lin"; - let contanct = Contact::try_from(author.to_string()).unwrap(); + let contact = Contact::try_from(author.to_string()).unwrap(); - assert!(contanct.name.is_some(), "Suzy should have name"); - assert!(contanct.email.is_none(), "Suzy should not have email"); + assert!(contact.name.is_some(), "Suzy should have name"); + assert!(contact.email.is_none(), "Suzy should not have email"); } #[test] fn contact_from_name_and_email() { let author = "Suzy Lin "; - let contanct = Contact::try_from(author.to_string()).unwrap(); + let contact = Contact::try_from(author.to_string()).unwrap(); - assert!(contanct.name.is_some(), "Suzy should have name"); - assert!(contanct.email.is_some(), "Suzy should have email"); + assert!(contact.name.is_some(), "Suzy should have name"); + assert!(contact.email.is_some(), "Suzy should have email"); } #[test] diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index b6646845..2b72a525 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -245,7 +245,7 @@ impl HttpMethod { .map_err(|error| { let mut diagnostics = Diagnostics::with_span(ident.span(), error.to_string()); if name == "connect" { - diagnostics = diagnostics.note("HTTP method `CONNET` is not supported by OpenAPI spec "); + diagnostics = diagnostics.note("HTTP method `CONNECT` is not supported by OpenAPI spec "); } diagnostics diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 51c9a3d9..76332dc1 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -145,7 +145,7 @@ impl Parse for RequestBodyAttr<'_> { } let media_type = - parse_utils::parse_comma_separated_within_parethesis_with( + parse_utils::parse_comma_separated_within_parenthesis_with( &group, group_parser, )? diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index 64bb1ff4..06f9a089 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -313,10 +313,12 @@ impl<'r> ResponseValue<'r> { buf.call(MediaTypeAttr::parse) } - let content = - parse_utils::parse_comma_separated_within_parethesis_with(input, group_parser)? - .into_iter() - .collect::>(); + let content = parse_utils::parse_comma_separated_within_parenthesis_with( + input, + group_parser, + )? + .into_iter() + .collect::>(); self.content = content; } diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index e5d0b19c..5a118667 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -1,3 +1,4 @@ +use desynt::StripRaw; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::spanned::Spanned; @@ -68,7 +69,7 @@ impl SchemaType<'_> { Some(segment) => segment, None => return false, }; - let name = &*last_segment.ident.to_string(); + let name = &*last_segment.ident.strip_raw().to_string(); #[cfg(not(any( feature = "chrono", @@ -233,7 +234,7 @@ impl ToTokensDiagnostics for SchemaType<'_> { "schema type should have at least one segment in the path", ) })?; - let name = &*last_segment.ident.to_string(); + let name = &*last_segment.ident.strip_raw().to_string(); fn schema_type_tokens( tokens: &mut TokenStream, @@ -373,7 +374,7 @@ impl KnownFormat { "type should have at least one segment in the path", ) })?; - let name = &*last_segment.ident.to_string(); + let name = &*last_segment.ident.strip_raw().to_string(); let variant = match name { #[cfg(feature = "non_strict_integers")] @@ -688,7 +689,7 @@ impl PrimitiveType { ) }); - let name = &*last_segment.ident.to_string(); + let name = &*last_segment.ident.strip_raw().to_string(); let ty: syn::Type = match name { "String" | "str" | "char" => syn::parse_quote!(#path), diff --git a/utoipa-gen/tests/path_derive_axum_test.rs b/utoipa-gen/tests/path_derive_axum_test.rs index a2a3d3dd..a09ca1c6 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -211,13 +211,13 @@ fn derive_path_params_with_unnamed_struct_destructed() { fn derive_path_query_params_with_named_struct_destructed() { #[derive(IntoParams)] #[allow(unused)] - struct QueryParmas<'q> { + struct QueryParams<'q> { name: &'q str, } - #[utoipa::path(get, path = "/item", params(QueryParmas))] + #[utoipa::path(get, path = "/item", params(QueryParams))] #[allow(unused)] - async fn get_item(Query(QueryParmas { name }): Query>) {} + async fn get_item(Query(QueryParams { name }): Query>) {} #[derive(OpenApi)] #[openapi(paths(get_item))] diff --git a/utoipa-gen/tests/raw_identifier.rs b/utoipa-gen/tests/raw_identifier.rs new file mode 100644 index 00000000..40516631 --- /dev/null +++ b/utoipa-gen/tests/raw_identifier.rs @@ -0,0 +1,217 @@ +#![expect( + dead_code, + reason = "Test structs with raw identifier fields are used only for schema generation" +)] + +use utoipa::{IntoParams, PartialSchema, ToSchema}; + +mod common; + +/// Test that demonstrates raw identifiers in struct fields are properly handled +#[test] +fn struct_with_raw_identifier_fields() { + let schema = { + #[derive(ToSchema)] + struct TestStruct { + r#type: String, + r#match: i32, + normal_field: bool, + } + + serde_json::to_value(::schema()).unwrap() + }; + + // The schema should contain clean field names without r# prefix + assert_value! { schema=> + "properties.type.type" = r#""string""#, "type field should be string type" + "properties.match.type" = r#""integer""#, "match field should be integer type" + "properties.normal_field.type" = r#""boolean""#, "normal_field should be boolean type" + }; + + // Verify that the property keys are clean (no r# prefix) + let properties = schema + .pointer("/properties") + .expect("Should have properties"); + let property_names: Vec = properties + .as_object() + .expect("Properties should be object") + .keys() + .cloned() + .collect(); + + println!("SUCCESS: Field names are clean: {:?}", property_names); + + assert!( + property_names.contains(&"type".to_string()), + "Should contain 'type' property" + ); + assert!( + property_names.contains(&"match".to_string()), + "Should contain 'match' property" + ); + assert!( + property_names.contains(&"normal_field".to_string()), + "Should contain 'normal_field' property" + ); + + // Assert that raw identifiers are NOT present + assert!( + !property_names.contains(&"r#type".to_string()), + "Should NOT contain 'r#type' property" + ); + assert!( + !property_names.contains(&"r#match".to_string()), + "Should NOT contain 'r#match' property" + ); +} + +/// Test that demonstrates raw identifiers in IntoParams are properly handled +#[test] +fn into_params_with_raw_identifiers() { + #[derive(IntoParams)] + struct QueryParams { + r#type: Option, + r#match: Option, + limit: Option, + } + + let params = QueryParams::into_params(|| None); + + // Check that parameter names are clean (without r# prefix) + let param_names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect(); + + println!("Parameter names: {:?}", param_names); + + assert!( + param_names.contains(&"type"), + "Should contain 'type' parameter, got: {:?}", + param_names + ); + assert!( + param_names.contains(&"match"), + "Should contain 'match' parameter, got: {:?}", + param_names + ); + assert!( + param_names.contains(&"limit"), + "Should contain 'limit' parameter, got: {:?}", + param_names + ); + + // Assert that raw identifiers are NOT present + assert!( + !param_names.contains(&"r#type"), + "Should NOT contain 'r#type' parameter" + ); + assert!( + !param_names.contains(&"r#match"), + "Should NOT contain 'r#match' parameter" + ); + + println!("SUCCESS: Parameter names are clean: {:?}", param_names); +} + +/// Test error messages and display formatting +#[test] +fn clean_names_in_debug_output() { + let schema = { + #[derive(ToSchema)] + struct TestStruct { + r#type: String, + r#async: bool, + } + + serde_json::to_value(::schema()).unwrap() + }; + + let debug_output = format!("{:?}", schema); + + // The debug output should contain clean field names + assert!( + debug_output.contains("\"type\""), + "Debug output should contain clean 'type' field name" + ); + assert!( + debug_output.contains("\"async\""), + "Debug output should contain clean 'async' field name" + ); + + println!("SUCCESS: Debug output contains clean field names"); + println!( + "Sample debug output: {}", + debug_output.chars().take(200).collect::() + ); +} + +/// Test that demonstrates raw identifiers work with primitive type aliases +#[test] +fn primitive_type_aliases_with_raw_identifiers() { + // This test demonstrates what happens when people use raw identifiers + // for field names with primitive types - the more common real-world scenario + let schema = { + #[derive(ToSchema)] + struct PrimitiveTestStruct { + // These field names use raw identifiers but the types are normal primitives + r#i8: i8, + r#u8: u8, + r#i16: i16, + r#u16: u16, + r#i32: i32, + r#u32: u32, + r#i64: i64, + r#f32: f32, + r#f64: f64, + r#bool: bool, + r#String: String, + } + + serde_json::to_value(::schema()).unwrap() + }; + + // Verify that all field names are clean (no r# prefix) + let properties = schema + .pointer("/properties") + .expect("Should have properties"); + let property_names: Vec = properties + .as_object() + .expect("Properties should be object") + .keys() + .cloned() + .collect(); + + println!("Primitive type field names: {:?}", property_names); + + // Check that all expected field names are present and clean + let expected_fields = [ + "i8", "u8", "i16", "u16", "i32", "u32", "i64", "f32", "f64", "bool", "String", + ]; + + for field in expected_fields { + assert!( + property_names.contains(&field.to_string()), + "Should contain '{}' field", + field + ); + } + + // Ensure raw identifier prefixes are NOT present + for field in expected_fields { + let raw_field = format!("r#{}", field); + assert!( + !property_names.contains(&raw_field), + "Should NOT contain '{}' field", + raw_field + ); + } + + // Verify basic types are correctly mapped in the schema (without worrying about specific formats) + assert_value! { schema=> + "properties.i8.type" = r#""integer""#, "i8 field should be integer type" + "properties.u8.type" = r#""integer""#, "u8 field should be integer type" + "properties.String.type" = r#""string""#, "String field should be string type" + "properties.bool.type" = r#""boolean""#, "bool field should be boolean type" + "properties.f32.type" = r#""number""#, "f32 field should be number type" + }; + + println!("SUCCESS: All primitive type fields with raw identifier names work correctly - r#i8, r#bool, etc. are all cleaned!"); +} diff --git a/utoipa/CHANGELOG.md b/utoipa/CHANGELOG.md index 46b567e5..4b98140e 100644 --- a/utoipa/CHANGELOG.md +++ b/utoipa/CHANGELOG.md @@ -493,7 +493,7 @@ Migration guide: https://github.com/juhaku/utoipa/discussions/456 * Add derive info support for derive OpenApi (https://github.com/juhaku/utoipa/pull/400) * Add `merge` functionality for `OpenApi` (https://github.com/juhaku/utoipa/pull/397) * Add derive servers attribute for OpenApi (https://github.com/juhaku/utoipa/pull/395) -* Add support for unit sructs (https://github.com/juhaku/utoipa/pull/392) +* Add support for unit structs (https://github.com/juhaku/utoipa/pull/392) * Add support for `schema_with` custom fn reference (https://github.com/juhaku/utoipa/pull/390) * Add support for multiple serde definitions (https://github.com/juhaku/utoipa/pull/389) * Add support for tuple Path parameters for axum (https://github.com/juhaku/utoipa/pull/388) @@ -542,7 +542,7 @@ Migration guide: https://github.com/juhaku/utoipa/discussions/456 * Use BTreeMap for responses of components to make it fixed order (https://github.com/juhaku/utoipa/pull/380) * Chore make `serde_json` mandatory dependency (https://github.com/juhaku/utoipa/pull/378) * Feature http status codes (https://github.com/juhaku/utoipa/pull/376) -* Refactor some path derive with `IntoParmas` tests +* Refactor some path derive with `IntoParams` tests * Update utoipa-swagger-ui install example * Chore refine description attribute (https://github.com/juhaku/utoipa/pull/373) * Update swagger-ui dependencies versions