Skip to content
Draft
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
1 change: 1 addition & 0 deletions utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ include = [
proc-macro = true

[dependencies]
desynt = { git = "https://github.com/jayvdb/desynt" }
utoipa-config = { version = "0.1", path = "../utoipa-config", optional = true }
once_cell = { version = "1.19.0", optional = true }
proc-macro2 = "1.0"
Expand Down
95 changes: 54 additions & 41 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,48 +145,55 @@ impl<'p> SynPathExt for &'p Path {
.expect("syn::Path must have at least one segment");

let mut segment = last_segment.clone();
if let PathArguments::AngleBracketed(anglebracketed_args) = &last_segment.arguments {
let args = anglebracketed_args.args.iter().try_fold(
Punctuated::<GenericArgument, Comma>::new(),
|mut args, generic_arg| {
match generic_arg {
GenericArgument::Type(ty) => {
let type_tree = TypeTree::from_type(ty)?;
let alias_type = type_tree.get_alias_type()?;
let alias_type_tree =
alias_type.as_ref().map(TypeTree::from_type).transpose()?;
let type_tree = alias_type_tree.unwrap_or(type_tree);

let path = type_tree
.path
.as_ref()
.expect("TypeTree must have a path")
.as_ref();

if let Some(default_type) = PrimitiveType::new(path) {
args.push(GenericArgument::Type(default_type.ty.clone()));
} else {
let inner = path.rewrite_path()?;
args.push(GenericArgument::Type(syn::Type::Path(
syn::parse_quote!(#inner),
)))
let args =
if let PathArguments::AngleBracketed(anglebracketed_args) = &last_segment.arguments {
anglebracketed_args.args.iter().try_fold(
Punctuated::<GenericArgument, Comma>::new(),
|mut args, generic_arg| {
match generic_arg {
GenericArgument::Type(ty) => {
let type_tree = TypeTree::from_type(ty)?;
let alias_type = type_tree.get_alias_type()?;
let alias_type_tree =
alias_type.as_ref().map(TypeTree::from_type).transpose()?;
let type_tree = alias_type_tree.unwrap_or(type_tree);

let path = type_tree
.path
.as_ref()
.expect("TypeTree must have a path")
.as_ref();

if let Some(default_type) = PrimitiveType::new(path) {
args.push(GenericArgument::Type(default_type.ty.clone()));
} else {
let inner = path.rewrite_path()?;
args.push(GenericArgument::Type(syn::Type::Path(
syn::parse_quote!(#inner),
)))
}
}
other => args.push(other.clone()),
}
other => args.push(other.clone()),
}

Result::<_, Diagnostics>::Ok(args)
},
)?;

let angle_bracket_args = AngleBracketedGenericArguments {
args,
lt_token: anglebracketed_args.lt_token,
gt_token: anglebracketed_args.gt_token,
colon2_token: anglebracketed_args.colon2_token,
Result::<_, Diagnostics>::Ok(args)
},
)?
} else {
Punctuated::new()
};

segment.arguments = PathArguments::AngleBracketed(angle_bracket_args);
if !args.is_empty() {
if let PathArguments::AngleBracketed(anglebracketed_args) = &last_segment.arguments {
let angle_bracket_args = AngleBracketedGenericArguments {
args,
lt_token: anglebracketed_args.lt_token,
gt_token: anglebracketed_args.gt_token,
colon2_token: anglebracketed_args.colon2_token,
};

segment.arguments = PathArguments::AngleBracketed(angle_bracket_args);
}
}

let segment_ident = &segment.ident;
Expand Down Expand Up @@ -412,7 +419,7 @@ impl TypeTree<'_> {
}

fn convert<'t>(path: &'t Path, last_segment: &'t PathSegment) -> TypeTree<'t> {
let generic_type = Self::get_generic_type(last_segment);
let generic_type = Self::get_generic_type(path, last_segment);
let schema_type = SchemaType {
path: Cow::Borrowed(path),
nullable: matches!(generic_type, Some(GenericType::Option)),
Expand All @@ -434,12 +441,16 @@ impl TypeTree<'_> {
}

// TODO should we recognize unknown generic types with `GenericType::Unknown` instead of `None`?
fn get_generic_type(segment: &PathSegment) -> Option<GenericType> {
fn get_generic_type(path: &Path, segment: &PathSegment) -> Option<GenericType> {
if segment.arguments.is_empty() {
return None;
}

match &*segment.ident.to_string() {
// Try to resolve the full path to a canonical type name using desynt::PathResolver
// Only recognize standard library types - if resolution fails, it's not a stdlib type
let canonical_name = crate::resolve_path_to_canonical(path)?;
Comment on lines -442 to +451
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one of the key parts of using the resolver.


match canonical_name {
"HashMap" | "Map" | "BTreeMap" => Some(GenericType::Map),
#[cfg(feature = "indexmap")]
"IndexMap" => Some(GenericType::Map),
Expand Down Expand Up @@ -568,7 +579,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::<syn::Type>(alias.as_ref()))
.map_err(|error| Diagnostics::new(error.to_string()))
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/component/features/attributes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::mem;

use desynt::StripRaw;
use proc_macro2::{Ident, TokenStream};
use quote::ToTokens;
use syn::parse::ParseStream;
Expand Down Expand Up @@ -497,7 +498,7 @@ impl_feature! {

impl ValueType {
/// Create [`TypeTree`] from current [`syn::Type`].
pub fn as_type_tree(&self) -> Result<TypeTree, Diagnostics> {
pub fn as_type_tree(&self) -> Result<TypeTree<'_>, Diagnostics> {
TypeTree::from_type(&self.0)
}
}
Expand Down Expand Up @@ -682,7 +683,7 @@ impl As {
.path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.map(|segment| segment.ident.strip_raw().to_string())
.collect::<Vec<_>>()
.join(".")
}
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/src/component/features/validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl Validator for IsString<'_> {
}
}

#[allow(dead_code)]
pub struct IsInteger<'a>(&'a SchemaType<'a>);

impl Validator for IsInteger<'_> {
Expand Down
9 changes: 3 additions & 6 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use desynt::StripRaw;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
Expand Down Expand Up @@ -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)?;
Expand Down
14 changes: 6 additions & 8 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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::<str>::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 {
Expand Down Expand Up @@ -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()))
);
}
}
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/component/schema/enums.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -103,7 +104,7 @@ impl<'e> PlainEnum<'e> {
.collect::<Result<Vec<_>, 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,
Expand Down Expand Up @@ -398,7 +399,7 @@ impl MixedEnumContent {
mut variant_features: Vec<Feature>,
) -> Result<Self, Diagnostics> {
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<Description>);
Expand Down
5 changes: 3 additions & 2 deletions utoipa-gen/src/ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use desynt::StripRaw;
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -288,7 +289,7 @@ pub trait ArgumentResolver {
_: &'_ Punctuated<syn::FnArg, Comma>,
_: Option<Vec<MacroArg>>,
_: String,
) -> Result<Arguments, Diagnostics> {
) -> Result<Arguments<'_>, Diagnostics> {
Ok((None, None, None))
}
}
Expand Down
14 changes: 14 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ use self::{
static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
once_cell::sync::Lazy::new(utoipa_config::Config::read_from_file);

// Global PathResolver using desynt's type groups for normalizing type paths
static PATH_RESOLVER: std::sync::OnceLock<
desynt::PathResolver<std::collections::HashMap<String, String>>,
> = std::sync::OnceLock::new();

fn get_path_resolver() -> &'static desynt::PathResolver<std::collections::HashMap<String, String>> {
PATH_RESOLVER.get_or_init(|| desynt::PathResolver::with_all_groups())
}

/// Resolve a type path to its canonical name using desynt::PathResolver
pub(crate) fn resolve_path_to_canonical(path: &syn::Path) -> Option<&'static str> {
get_path_resolver().resolve(path)
}

#[proc_macro_derive(ToSchema, attributes(schema))]
/// Generate reusable OpenAPI schema to be used
/// together with [`OpenApi`][openapi_derive].
Expand Down
11 changes: 6 additions & 5 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;

use desynt::StripRaw;
use proc_macro2::Ident;
use syn::{
bracketed, parenthesized,
Expand Down Expand Up @@ -70,7 +71,7 @@ impl<'o> OpenApiAttr<'o> {
}
}

pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Result<Option<OpenApiAttr>, Error> {
pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Result<Option<OpenApiAttr<'_>>, Error> {
attrs
.iter()
.filter(|attribute| attribute.path().is_ident("openapi"))
Expand Down Expand Up @@ -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::<Vec<_>>()
.join("::");
let tags = &item.tags.iter().collect::<Array<_>>();
Expand Down Expand Up @@ -696,7 +697,7 @@ fn impl_paths(handler_paths: Option<&Punctuated<ExprPath, Comma>>) -> Paths {
let segments = handler.path.segments.iter().collect::<Vec<_>>();
let handler_config_name = segments
.iter()
.map(|segment| segment.ident.to_string())
.map(|segment| segment.ident.strip_raw().to_string())
.collect::<Vec<_>>()
.join("_");
let handler_fn = &segments.last().unwrap().ident;
Expand All @@ -706,7 +707,7 @@ fn impl_paths(handler_paths: Option<&Punctuated<ExprPath, Comma>>) -> Paths {
let tag = segments
.iter()
.take(segments.len() - 1)
.map(|part| part.ident.to_string())
.map(|part| part.ident.strip_raw().to_string())
.collect::<Vec<_>>()
.join("::");

Expand Down Expand Up @@ -759,7 +760,7 @@ fn impl_paths(handler_paths: Option<&Punctuated<ExprPath, Comma>>) -> Paths {
let segments = handler.path.segments.iter().collect::<Vec<_>>();
let handler_config_name = segments
.iter()
.map(|segment| segment.ident.to_string())
.map(|segment| segment.ident.strip_raw().to_string())
.collect::<Vec<_>>()
.join("_");
let handler_ident_config = format_ident!("{}_config", handler_config_name);
Expand Down
4 changes: 2 additions & 2 deletions utoipa-gen/src/path/media_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ impl Default for Schema<'_> {
}

impl Schema<'_> {
pub fn get_type_tree(&self) -> Result<Option<Cow<TypeTree<'_>>>, Diagnostics> {
pub fn get_type_tree(&self) -> Result<Option<Cow<'_, TypeTree<'_>>>, Diagnostics> {
match self {
Self::Default(def) => def.get_type_tree(),
Self::Ext(ext) => ext.get_type_tree(),
Expand Down Expand Up @@ -417,7 +417,7 @@ pub struct ParsedType<'i> {

impl ParsedType<'_> {
/// Get's the underlying [`syn::Type`] as [`TypeTree`].
fn to_type_tree(&self) -> Result<TypeTree, Diagnostics> {
fn to_type_tree(&self) -> Result<TypeTree<'_>, Diagnostics> {
TypeTree::from_type(&self.ty)
}
}
Expand Down
6 changes: 3 additions & 3 deletions utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,12 +673,12 @@ struct ResponseStatus(TokenStream2);

impl Parse for ResponseStatus {
fn parse(input: ParseStream) -> syn::Result<Self> {
fn parse_lit_int(input: ParseStream) -> syn::Result<Cow<'_, str>> {
fn parse_lit_int(input: ParseStream<'_>) -> syn::Result<Cow<'_, str>> {
input.parse::<LitInt>()?.base10_parse().map(Cow::Owned)
}

fn parse_lit_str_status_range(input: ParseStream) -> syn::Result<Cow<'_, str>> {
const VALID_STATUS_RANGES: [&str; 6] = ["default", "1XX", "2XX", "3XX", "4XX", "5XX"];
fn parse_lit_str_status_range(input: ParseStream<'_>) -> syn::Result<Cow<'_, str>> {
const VALID_STATUS_RANGES: [&str; 5] = ["1XX", "2XX", "3XX", "4XX", "5XX"];

input
.parse::<LitStr>()
Expand Down
Loading
Loading