From d6fa5e1c6632eb2c607b466dc8582af2a6470a80 Mon Sep 17 00:00:00 2001 From: Domenic Quirl Date: Wed, 5 Feb 2025 15:50:18 +0100 Subject: [PATCH 1/3] replace String-based interpolation with quoting `TokenStream`s --- utoipauto-core/src/attribute_utils.rs | 328 ++++++++++++++------------ utoipauto-core/src/discover.rs | 69 +++--- utoipauto-core/src/file_utils.rs | 83 +++++-- utoipauto-core/src/string_utils.rs | 30 +-- utoipauto-macro/src/lib.rs | 2 +- 5 files changed, 292 insertions(+), 220 deletions(-) diff --git a/utoipauto-core/src/attribute_utils.rs b/utoipauto-core/src/attribute_utils.rs index 776dad4..5c1edd7 100644 --- a/utoipauto-core/src/attribute_utils.rs +++ b/utoipauto-core/src/attribute_utils.rs @@ -1,11 +1,11 @@ -use quote::ToTokens; -use syn::Attribute; +use proc_macro2::TokenStream; +use syn::{punctuated::Punctuated, Attribute, Meta, Token}; pub fn update_openapi_macro_attributes( macro_attibutes: &mut Vec, - uto_paths: &String, - uto_models: &String, - uto_responses: &String, + uto_paths: &TokenStream, + uto_models: &TokenStream, + uto_responses: &TokenStream, ) { let mut is_ok = false; for attr in macro_attibutes { @@ -13,12 +13,21 @@ pub fn update_openapi_macro_attributes( continue; } is_ok = true; - let mut src_uto_macro = attr.to_token_stream().to_string(); - - src_uto_macro = src_uto_macro.replace("#[openapi()]", ""); - src_uto_macro = src_uto_macro.replace("#[openapi(", ""); - src_uto_macro = src_uto_macro.replace(")]", ""); - *attr = build_new_openapi_attributes(src_uto_macro, uto_paths, uto_models, uto_responses); + match &attr.meta { + // #[openapi] + Meta::Path(_path) => { + *attr = build_new_openapi_attributes(Punctuated::new(), uto_paths, uto_models, uto_responses); + } + // #[openapi()] or #[openapi(attribute(...))] + Meta::List(meta_list) => { + let nested = meta_list + .parse_args_with(Punctuated::::parse_terminated) + .expect("Expected a list of attributes inside #[openapi(...)]!"); + *attr = build_new_openapi_attributes(nested, uto_paths, uto_models, uto_responses); + } + // This would be #[openapi = "foo"], which is not valid + Meta::NameValue(_) => panic!("Expected #[openapi(...)], but found #[openapi = value]!"), + } } if !is_ok { panic!("No utoipa::openapi Macro found !"); @@ -27,145 +36,134 @@ pub fn update_openapi_macro_attributes( /// Build the new openapi macro attribute with the newly discovered paths pub fn build_new_openapi_attributes( - src_uto_macro: String, - uto_paths: &String, - uto_models: &String, - uto_responses: &String, + nested_attributes: Punctuated, + uto_paths: &TokenStream, + uto_models: &TokenStream, + uto_responses: &TokenStream, ) -> Attribute { - let paths = extract_paths(src_uto_macro.clone()); - let schemas = extract_schemas(src_uto_macro.clone()); - let responses = extract_responses(src_uto_macro.clone()); - let src_uto_macro = remove_paths(src_uto_macro); - let src_uto_macro = remove_schemas(src_uto_macro); - let src_uto_macro = remove_responses(src_uto_macro); - let src_uto_macro = remove_components(src_uto_macro); - - let paths = format!("{}{}", uto_paths, paths); - let schemas = format!("{}{}", uto_models, schemas); - let responses = format!("{}{}", uto_responses, responses); - let src_uto_macro = format!( - "paths({}),components(schemas({}),responses({})),{}", - paths, schemas, responses, src_uto_macro - ) - .replace(",,", ","); + let paths = extract_paths(&nested_attributes); + let schemas = extract_schemas(&nested_attributes); + let responses = extract_responses(&nested_attributes); + let remaining_nested_attributes = remove_paths_and_components(nested_attributes); - let stream: proc_macro2::TokenStream = src_uto_macro.parse().unwrap(); - - syn::parse_quote! { #[openapi( #stream )] } -} + let uto_paths = match uto_paths.is_empty() { + true => TokenStream::new(), + false => quote::quote!(#uto_paths,), + }; + let uto_models = match uto_models.is_empty() { + true => TokenStream::new(), + false => quote::quote!(#uto_models,), + }; + let uto_responses = match uto_responses.is_empty() { + true => TokenStream::new(), + false => quote::quote!(#uto_responses,), + }; + let uto_macro = quote::quote!( + paths(#uto_paths #paths),components(schemas(#uto_models #schemas),responses(#uto_responses #responses)), + #remaining_nested_attributes + ); -fn remove_paths(src_uto_macro: String) -> String { - if src_uto_macro.contains("paths(") { - let paths = src_uto_macro.split("paths(").collect::>()[1]; - let paths = paths.split(')').collect::>()[0]; - src_uto_macro - .replace(format!("paths({})", paths).as_str(), "") - .replace(",,", ",") - } else { - src_uto_macro - } -} - -fn remove_schemas(src_uto_macro: String) -> String { - if src_uto_macro.contains("schemas(") { - let schemas = src_uto_macro.split("schemas(").collect::>()[1]; - let schemas = schemas.split(')').collect::>()[0]; - src_uto_macro - .replace(format!("schemas({})", schemas).as_str(), "") - .replace(",,", ",") - } else { - src_uto_macro - } + syn::parse_quote! { #[openapi( #uto_macro )] } } -fn remove_components(src_uto_macro: String) -> String { - if src_uto_macro.contains("components(") { - let components = src_uto_macro.split("components(").collect::>()[1]; - let components = components.split(')').collect::>()[0]; - src_uto_macro - .replace(format!("components({})", components).as_str(), "") - .replace(",,", ",") - } else { - src_uto_macro +fn remove_paths_and_components(nested_attributes: Punctuated) -> TokenStream { + let mut remaining = Vec::with_capacity(nested_attributes.len()); + for meta in nested_attributes { + match meta { + Meta::List(list) if list.path.is_ident("paths") => (), + Meta::List(list) if list.path.is_ident("components") => (), + // These should be handled by removing `components`, this is just in case they occur outside of `components` for some reason. + Meta::List(list) if list.path.is_ident("schemas") => (), + Meta::List(list) if list.path.is_ident("responses") => (), + _ => remaining.push(meta), + } } + quote::quote!( #(#remaining),* ) } -fn remove_responses(src_uto_macro: String) -> String { - if src_uto_macro.contains("responses(") { - let responses = src_uto_macro.split("responses(").collect::>()[1]; - let responses = responses.split(')').collect::>()[0]; - src_uto_macro - .replace(format!("responses({})", responses).as_str(), "") - .replace(",,", ",") - } else { - src_uto_macro +fn extract_paths(nested_attributes: &Punctuated) -> TokenStream { + for meta in nested_attributes { + if let Meta::List(list) = meta { + if list.path.is_ident("paths") { + return list.tokens.clone(); + } + } } -} -fn extract_paths(src_uto_macro: String) -> String { - if src_uto_macro.contains("paths(") { - let paths = src_uto_macro.split("paths(").collect::>()[1]; - let paths = paths.split(')').collect::>()[0]; - paths.to_string() - } else { - "".to_string() - } + TokenStream::new() } -fn extract_schemas(src_uto_macro: String) -> String { - if src_uto_macro.contains("schemas(") { - let schemas = src_uto_macro.split("schemas(").collect::>()[1]; - let schemas = schemas.split(')').collect::>()[0]; - schemas.to_string() - } else { - "".to_string() +fn extract_schemas(nested_attributes: &Punctuated) -> TokenStream { + for meta in nested_attributes { + if let Meta::List(list) = meta { + if list.path.is_ident("components") { + let nested = list + .parse_args_with(Punctuated::::parse_terminated) + .expect("Expected a list of attributes inside components(...)!"); + for meta in nested { + if let Meta::List(list) = meta { + if list.path.is_ident("schemas") { + return list.tokens; + } + } + } + } + } } + TokenStream::new() } -fn extract_responses(src_uto_macro: String) -> String { - if src_uto_macro.contains("responses(") { - let responses = src_uto_macro.split("responses(").collect::>()[1]; - let responses = responses.split(')').collect::>()[0]; - responses.to_string() - } else { - "".to_string() +fn extract_responses(nested_attributes: &Punctuated) -> TokenStream { + for meta in nested_attributes { + if let Meta::List(list) = meta { + if list.path.is_ident("components") { + let nested = list + .parse_args_with(Punctuated::::parse_terminated) + .expect("Expected a list of attributes inside components(...)!"); + for meta in nested { + if let Meta::List(list) = meta { + if list.path.is_ident("responses") { + return list.tokens; + } + } + } + } + } } + TokenStream::new() } #[cfg(test)] mod test { + use proc_macro2::TokenStream; use quote::ToTokens; + use syn::punctuated::Punctuated; #[test] - fn test_remove_paths() { + fn test_extract_paths() { assert_eq!( - super::remove_paths("description(test),paths(p1),info(test)".to_string()), - "description(test),info(test)".to_string() + super::extract_paths(&syn::parse_quote!(paths(p1))).to_string(), + "p1".to_string() ); } - #[test] - fn test_extract_paths() { - assert_eq!(super::extract_paths("paths(p1)".to_string()), "p1".to_string()); - } - #[test] fn test_extract_paths_empty() { - assert_eq!(super::extract_paths("".to_string()), "".to_string()); + assert_eq!(super::extract_paths(&Punctuated::new()).to_string(), "".to_string()); } #[test] fn test_build_new_openapi_attributes() { assert_eq!( super::build_new_openapi_attributes( - "".to_string(), - &"./src".to_string(), - &"".to_string(), - &"".to_string(), + Punctuated::new(), + "e::quote!(crate::api::test), + &TokenStream::new(), + &TokenStream::new(), ) - .to_token_stream() - .to_string() - .replace(' ', ""), - "#[openapi(paths(./src),components(schemas(),responses()),)]".to_string() + .to_token_stream() + .to_string() + .replace(' ', ""), + "#[openapi(paths(crate::api::test,),components(schemas(),responses()),)]".to_string() ); } @@ -173,15 +171,15 @@ mod test { fn test_build_new_openapi_attributes_path_replace() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1)".to_string(), - &"./src,".to_string(), - &"".to_string(), - &"".to_string(), + syn::parse_quote!(paths(p1)), + "e::quote!(crate::api::test), + &TokenStream::new(), + &TokenStream::new(), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(),responses()),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(),responses()),)]".to_string() ); } @@ -189,15 +187,15 @@ mod test { fn test_build_new_openapi_attributes_components() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1)".to_string(), - &"./src,".to_string(), - &"model".to_string(), - &"".to_string() + syn::parse_quote!(paths(p1)), + "e::quote!(crate::api::test), + "e::quote!(model), + &TokenStream::new(), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(model),responses()),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(model,),responses()),)]".to_string() ); } @@ -205,15 +203,15 @@ mod test { fn test_build_new_openapi_attributes_components_schemas_replace() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1), components(schemas(m1))".to_string(), - &"./src,".to_string(), - &"model,".to_string(), - &"".to_string(), + syn::parse_quote!(paths(p1), components(schemas(m1))), + "e::quote!(crate::api::test), + "e::quote!(model), + &TokenStream::new(), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(model,m1),responses()),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses()),)]".to_string() ); } @@ -221,15 +219,15 @@ mod test { fn test_build_new_openapi_attributes_components_responses_replace() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1), components(responses(r1))".to_string(), - &"./src,".to_string(), - &"".to_string(), - &"response,".to_string(), + syn::parse_quote!(paths(p1), components(responses(r1))), + "e::quote!(crate::api::test), + &TokenStream::new(), + "e::quote!(response), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(),responses(response,r1)),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(),responses(response,r1)),)]".to_string() ); } @@ -237,15 +235,15 @@ mod test { fn test_build_new_openapi_attributes_components_responses_schemas_replace() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1), components(responses(r1), schemas(m1))".to_string(), - &"./src,".to_string(), - &"model,".to_string(), - &"response,".to_string(), + syn::parse_quote!(paths(p1), components(responses(r1), schemas(m1))), + "e::quote!(crate::api::test), + "e::quote!(model), + "e::quote!(response), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(model,m1),responses(response,r1)),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(response,r1)),)]".to_string() ); } @@ -253,15 +251,15 @@ mod test { fn test_build_new_openapi_attributes_components_responses_schemas() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1), components(responses(r1), schemas(m1))".to_string(), - &"./src,".to_string(), - &"".to_string(), - &"response,".to_string(), + syn::parse_quote!(paths(p1), components(responses(r1), schemas(m1))), + "e::quote!(crate::api::test), + &TokenStream::new(), + "e::quote!(response), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(m1),responses(response,r1)),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(m1),responses(response,r1)),)]".to_string() ); } @@ -269,15 +267,45 @@ mod test { fn test_build_new_openapi_attributes_components_schemas_reponses() { assert_eq!( super::build_new_openapi_attributes( - "paths(p1), components(schemas(m1), responses(r1))".to_string(), - &"./src,".to_string(), - &"model,".to_string(), - &"".to_string(), + syn::parse_quote!(paths(p1), components(schemas(m1), responses(r1))), + "e::quote!(crate::api::test), + "e::quote!(model), + &TokenStream::new(), ) .to_token_stream() .to_string() .replace(' ', ""), - "#[openapi(paths(./src,p1),components(schemas(model,m1),responses(r1)),)]".to_string() + "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(r1)),)]".to_string() + ); + } + + #[test] + fn test_update_openapi_attributes_empty() { + let mut attrs = vec![syn::parse_quote!(#[openapi])]; + super::update_openapi_macro_attributes( + &mut attrs, + "e::quote!(crate::api::test), + "e::quote!(model), + &TokenStream::new(), + ); + assert_eq!( + attrs[0].to_token_stream().to_string().replace(' ', ""), + "#[openapi(paths(crate::api::test,),components(schemas(model,),responses()),)]".to_string() + ); + } + + #[test] + fn test_update_openapi_attributes_components_schemas_reponses() { + let mut attrs = vec![syn::parse_quote!(#[openapi(paths(p1), components(schemas(m1), responses(r1)))])]; + super::update_openapi_macro_attributes( + &mut attrs, + "e::quote!(crate::api::test), + "e::quote!(model), + &TokenStream::new(), + ); + assert_eq!( + attrs[0].to_token_stream().to_string().replace(' ', ""), + "#[openapi(paths(crate::api::test,p1),components(schemas(model,m1),responses(r1)),)]".to_string() ); } } diff --git a/utoipauto-core/src/discover.rs b/utoipauto-core/src/discover.rs index 3fcbdf3..37a4a92 100644 --- a/utoipauto-core/src/discover.rs +++ b/utoipauto-core/src/discover.rs @@ -4,6 +4,7 @@ use crate::file_utils::{extract_module_name_from_path, parse_files}; use crate::token_utils::Parameters; use quote::ToTokens; use syn::token::Comma; +use syn::Ident; use syn::{punctuated::Punctuated, Attribute, GenericParam, Item, ItemFn, ItemImpl, Meta, Token}; /// Discover everything from a file, will explore folder recursively @@ -11,19 +12,23 @@ pub fn discover_from_file( src_path: String, crate_name: String, params: &Parameters, -) -> (Vec, Vec, Vec) { +) -> (Vec, Vec, Vec) { let files = parse_files(&src_path).unwrap_or_else(|_| panic!("Failed to parse file {}", src_path)); files .into_iter() - .map(|e| parse_module_items(&extract_module_name_from_path(&e.0, &crate_name), e.1.items, params)) + .map(|e| parse_module_items(extract_module_name_from_path(&e.0, &crate_name), e.1.items, params)) .fold(Vec::::new(), |mut acc, mut v| { acc.append(&mut v); acc }) .into_iter() .fold( - (Vec::::new(), Vec::::new(), Vec::::new()), + ( + Vec::::new(), + Vec::::new(), + Vec::::new(), + ), |mut acc, v| { match v { DiscoverType::Fn(n) => acc.0.push(n), @@ -40,14 +45,14 @@ pub fn discover_from_file( #[allow(unused)] enum DiscoverType { - Fn(String), - Model(String), - Response(String), - CustomModelImpl(String), - CustomResponseImpl(String), + Fn(syn::Path), + Model(syn::Path), + Response(syn::Path), + CustomModelImpl(syn::Path), + CustomResponseImpl(syn::Path), } -fn parse_module_items(module_path: &str, items: Vec, params: &Parameters) -> Vec { +fn parse_module_items(module_path: syn::Path, items: Vec, params: &Parameters) -> Vec { items .into_iter() .filter(|e| { @@ -58,25 +63,15 @@ fn parse_module_items(module_path: &str, items: Vec, params: &Parameters) }) .map(|v| match v { Item::Mod(m) => m.content.map_or(Vec::::new(), |cs| { - parse_module_items(&build_path(module_path, &m.ident.to_string()), cs.1, params) + parse_module_items(build_path(&module_path, &m.ident), cs.1, params) }), Item::Fn(f) => parse_function(&f, ¶ms.fn_attribute_name) .into_iter() - .map(|item| DiscoverType::Fn(build_path(module_path, &item))) + .map(|item| DiscoverType::Fn(build_path(&module_path, &item))) .collect(), - Item::Struct(s) => parse_from_attr( - &s.attrs, - &build_path(module_path, &s.ident.to_string()), - s.generics.params, - params, - ), - Item::Enum(e) => parse_from_attr( - &e.attrs, - &build_path(module_path, &e.ident.to_string()), - e.generics.params, - params, - ), - Item::Impl(im) => parse_from_impl(&im, module_path, params), + Item::Struct(s) => parse_from_attr(&s.attrs, build_path(&module_path, &s.ident), s.generics.params, params), + Item::Enum(e) => parse_from_attr(&e.attrs, build_path(&module_path, &e.ident), e.generics.params, params), + Item::Impl(im) => parse_from_impl(&im, &module_path, params), _ => vec![], }) .fold(Vec::::new(), |mut acc, mut v| { @@ -88,7 +83,7 @@ fn parse_module_items(module_path: &str, items: Vec, params: &Parameters) /// Search for ToSchema and ToResponse implementations in attr fn parse_from_attr( a: &Vec, - name: &str, + name: syn::Path, generic_params: Punctuated, params: &Parameters, ) -> Vec { @@ -109,16 +104,16 @@ fn parse_from_attr( for nested_meta in nested { if nested_meta.path().segments.len() == 2 && nested_meta.path().segments[0].ident == "utoipa" { match nested_meta.path().segments[1].ident.to_string().as_str() { - "ToSchema" => out.push(DiscoverType::Model(name.to_string())), - "ToResponse" => out.push(DiscoverType::Response(name.to_string())), + "ToSchema" => out.push(DiscoverType::Model(name.clone())), + "ToResponse" => out.push(DiscoverType::Response(name.clone())), _ => {} } } else { if nested_meta.path().is_ident(¶ms.schema_attribute_name) { - out.push(DiscoverType::Model(name.to_string())); + out.push(DiscoverType::Model(name.clone())); } if nested_meta.path().is_ident(¶ms.response_attribute_name) { - out.push(DiscoverType::Response(name.to_string())); + out.push(DiscoverType::Response(name.clone())); } } } @@ -128,7 +123,7 @@ fn parse_from_attr( out } -fn parse_from_impl(im: &ItemImpl, module_base_path: &str, params: &Parameters) -> Vec { +fn parse_from_impl(im: &ItemImpl, module_base_path: &syn::Path, params: &Parameters) -> Vec { im.trait_ .as_ref() .and_then(|trt| trt.1.segments.last().map(|p| p.ident.to_string())) @@ -136,12 +131,12 @@ fn parse_from_impl(im: &ItemImpl, module_base_path: &str, params: &Parameters) - if impl_name.eq(params.schema_attribute_name.as_str()) { Some(vec![DiscoverType::CustomModelImpl(build_path( module_base_path, - &im.self_ty.to_token_stream().to_string(), + &im.self_ty, ))]) } else if impl_name.eq(params.response_attribute_name.as_str()) { Some(vec![DiscoverType::CustomResponseImpl(build_path( module_base_path, - &im.self_ty.to_token_stream().to_string(), + &im.self_ty, ))]) } else { None @@ -150,8 +145,8 @@ fn parse_from_impl(im: &ItemImpl, module_base_path: &str, params: &Parameters) - .unwrap_or_default() } -fn parse_function(f: &ItemFn, fn_attributes_name: &str) -> Vec { - let mut fns_name: Vec = vec![]; +fn parse_function(f: &ItemFn, fn_attributes_name: &str) -> Vec { + let mut fns_name: Vec = vec![]; if should_parse_fn(f) { for i in 0..f.attrs.len() { if f.attrs[i] @@ -161,7 +156,7 @@ fn parse_function(f: &ItemFn, fn_attributes_name: &str) -> Vec { .iter() .any(|item| item.ident.eq(fn_attributes_name)) { - fns_name.push(f.sig.ident.to_string()); + fns_name.push(f.sig.ident.clone()); } } } @@ -182,8 +177,8 @@ fn is_ignored(f: &ItemFn) -> bool { }) } -fn build_path(file_name: &str, fn_name: &str) -> String { - format!("{}::{}", file_name, fn_name) +fn build_path(file_path: &syn::Path, fn_name: impl ToTokens) -> syn::Path { + syn::parse_quote!(#file_path::#fn_name) } #[cfg(test)] diff --git a/utoipauto-core/src/file_utils.rs b/utoipauto-core/src/file_utils.rs index 8a5c43e..7b8741c 100644 --- a/utoipauto-core/src/file_utils.rs +++ b/utoipauto-core/src/file_utils.rs @@ -5,6 +5,8 @@ use std::{ path::{Path, PathBuf}, }; +use proc_macro2::Span; + pub fn parse_file>(filepath: T) -> Result { let pb: PathBuf = filepath.into(); @@ -67,7 +69,7 @@ fn is_rust_file(path: &Path) -> bool { /// "crate::controllers::controller1".to_string() /// ); /// ``` -pub fn extract_module_name_from_path(path: &str, crate_name: &str) -> String { +pub fn extract_module_name_from_path(path: &str, crate_name: &str) -> syn::Path { let path = path.replace('\\', "/"); let path = path .trim_end_matches(".rs") @@ -95,10 +97,13 @@ pub fn extract_module_name_from_path(path: &str, crate_name: &str) -> String { None => segments_inside_crate, }; - let full_crate_path: Vec<_> = iter::once(first_crate_fragment) + let full_crate_path = iter::once(first_crate_fragment) .chain(segments_inside_crate.iter().copied()) - .collect(); - full_crate_path.join("::") + .map(|segment| syn::PathSegment::from(syn::Ident::new(&segment.replace('-', "_"), Span::mixed_site()))); + syn::Path { + leading_colon: None, + segments: full_crate_path.collect(), + } } fn find_segment_and_skip<'a>(segments: &'a [&str], to_find: &[&str], to_skip: usize) -> &'a [&'a str] { @@ -110,12 +115,17 @@ fn find_segment_and_skip<'a>(segments: &'a [&str], to_find: &[&str], to_skip: us #[cfg(test)] mod tests { + use quote::ToTokens; + use super::*; #[test] fn test_extract_module_name_from_path() { assert_eq!( - extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/controller1.rs", "crate"), + extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/controller1.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::controllers::controller1" ); } @@ -123,7 +133,10 @@ mod tests { #[test] fn test_extract_module_name_from_path_windows() { assert_eq!( - extract_module_name_from_path(".\\utoipa-auto-macro\\tests\\controllers\\controller1.rs", "crate"), + extract_module_name_from_path(".\\utoipa-auto-macro\\tests\\controllers\\controller1.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::controllers::controller1" ); } @@ -131,25 +144,43 @@ mod tests { #[test] fn test_extract_module_name_from_mod() { assert_eq!( - extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/mod.rs", "crate"), + extract_module_name_from_path("./utoipa-auto-macro/tests/controllers/mod.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::controllers" ); } #[test] fn test_extract_module_name_from_lib() { - assert_eq!(extract_module_name_from_path("./src/lib.rs", "crate"), "crate"); + assert_eq!( + extract_module_name_from_path("./src/lib.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), + "crate" + ); } #[test] fn test_extract_module_name_from_main() { - assert_eq!(extract_module_name_from_path("./src/main.rs", "crate"), "crate"); + assert_eq!( + extract_module_name_from_path("./src/main.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), + "crate" + ); } #[test] fn test_extract_module_name_from_workspace() { assert_eq!( - extract_module_name_from_path("./server/src/routes/asset.rs", "crate"), + extract_module_name_from_path("./server/src/routes/asset.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::routes::asset" ); } @@ -157,7 +188,10 @@ mod tests { #[test] fn test_extract_module_name_from_workspace_nested() { assert_eq!( - extract_module_name_from_path("./crates/server/src/routes/asset.rs", "crate"), + extract_module_name_from_path("./crates/server/src/routes/asset.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::routes::asset" ); } @@ -165,7 +199,10 @@ mod tests { #[test] fn test_extract_module_name_from_folders() { assert_eq!( - extract_module_name_from_path("./src/routing/api/audio.rs", "crate"), + extract_module_name_from_path("./src/routing/api/audio.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::routing::api::audio" ); } @@ -173,7 +210,10 @@ mod tests { #[test] fn test_extract_module_name_from_folders_nested() { assert_eq!( - extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "crate"), + extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::retail_api::controllers" ); } @@ -181,7 +221,10 @@ mod tests { #[test] fn test_extract_module_name_from_folders_nested_external_crate() { assert_eq!( - extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "other_crate"), + extract_module_name_from_path("./src/applications/src/retail_api/controllers/mod.rs", "other_crate") + .to_token_stream() + .to_string() + .replace(" ", ""), "other_crate::retail_api::controllers" ); } @@ -189,7 +232,10 @@ mod tests { #[test] fn test_extract_module_name_from_workspace_with_prefix_path() { assert_eq!( - extract_module_name_from_path("./crates/server/src/routes_lib/routes/asset.rs", "crate::routes"), + extract_module_name_from_path("./crates/server/src/routes_lib/routes/asset.rs", "crate::routes") + .to_token_stream() + .to_string() + .replace(" ", ""), "crate::routes::asset" ); } @@ -197,8 +243,11 @@ mod tests { #[test] fn test_extract_module_name_from_workspace_with_external_crate_and_underscore() { assert_eq!( - extract_module_name_from_path("./src/applications/src/retail-api/controllers/mod.rs", "other-crate"), - "other_crate::retail-api::controllers" + extract_module_name_from_path("./src/applications/src/retail-api/controllers/mod.rs", "other-crate") + .to_token_stream() + .to_string() + .replace(" ", ""), + "other_crate::retail_api::controllers" ); } } diff --git a/utoipauto-core/src/string_utils.rs b/utoipauto-core/src/string_utils.rs index d545ea9..ecc55e9 100644 --- a/utoipauto-core/src/string_utils.rs +++ b/utoipauto-core/src/string_utils.rs @@ -1,3 +1,5 @@ +use proc_macro2::TokenStream; + use crate::{discover::discover_from_file, token_utils::Parameters}; pub fn rem_first_and_last(value: &str) -> &str { @@ -89,25 +91,23 @@ fn extract_paths_coma(attributes: String) -> Vec { /// Return the list of all the functions with the #[utoipa] attribute /// and the list of all the structs with the #[derive(ToSchema)] attribute /// and the list of all the structs with the #[derive(ToResponse)] attribute -pub fn discover(paths: Vec, params: &Parameters) -> (String, String, String) { - let mut uto_paths: String = String::new(); - let mut uto_models: String = String::new(); - let mut uto_responses: String = String::new(); +pub fn discover(paths: Vec, params: &Parameters) -> (TokenStream, TokenStream, TokenStream) { + let mut uto_paths = Vec::new(); + let mut uto_models = Vec::new(); + let mut uto_responses = Vec::new(); for p in paths { let path = extract_crate_name(p); let (list_fn, list_model, list_reponse) = discover_from_file(path.paths, path.crate_name, params); - // We need to add a coma after each path - for i in list_fn { - uto_paths.push_str(format!("{},", i).as_str()); - } - for i in list_model { - uto_models.push_str(format!("{},", i).as_str()); - } - for i in list_reponse { - uto_responses.push_str(format!("{},", i).as_str()); - } + uto_paths.extend(list_fn); + uto_models.extend(list_model); + uto_responses.extend(list_reponse); } - (uto_paths, uto_models, uto_responses) + // We need to add a coma after each path + ( + quote::quote!(#(#uto_paths),*), + quote::quote!(#(#uto_models),*), + quote::quote!(#(#uto_responses),*), + ) } #[derive(Debug, PartialEq)] diff --git a/utoipauto-macro/src/lib.rs b/utoipauto-macro/src/lib.rs index 9a9411d..771deb1 100644 --- a/utoipauto-macro/src/lib.rs +++ b/utoipauto-macro/src/lib.rs @@ -23,7 +23,7 @@ pub fn utoipauto( let mut openapi_macro = parse_macro_input!(item as syn::ItemStruct); // Discover all the functions with the #[utoipa] attribute - let (uto_paths, uto_models, uto_responses): (String, String, String) = discover(paths, ¶ms); + let (uto_paths, uto_models, uto_responses) = discover(paths, ¶ms); // extract the openapi macro attributes : #[openapi(openapi_macro_attibutes)] let openapi_macro_attibutes = &mut openapi_macro.attrs; From 80492dbfc9498b573dfce3cedff61edfa38272fe Mon Sep 17 00:00:00 2001 From: Domenic Quirl Date: Wed, 5 Feb 2025 16:27:23 +0100 Subject: [PATCH 2/3] fix doc test on `extract_module_name_from_path` --- utoipauto-core/src/file_utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utoipauto-core/src/file_utils.rs b/utoipauto-core/src/file_utils.rs index 7b8741c..6aab1df 100644 --- a/utoipauto-core/src/file_utils.rs +++ b/utoipauto-core/src/file_utils.rs @@ -59,13 +59,14 @@ fn is_rust_file(path: &Path) -> bool { /// Extract the module name from the file path /// # Example /// ``` +/// # use quote::ToTokens as _; /// use utoipauto_core::file_utils::extract_module_name_from_path; /// let module_name = extract_module_name_from_path( /// &"./utoipa-auto-macro/tests/controllers/controller1.rs".to_string(), /// "crate" /// ); /// assert_eq!( -/// module_name, +/// module_name.to_token_stream().to_string().replace(' ', ""), /// "crate::controllers::controller1".to_string() /// ); /// ``` From cfdd1ef0ee5b44fc7072e5c8dc68a81720c544f1 Mon Sep 17 00:00:00 2001 From: Domenic Quirl Date: Mon, 10 Feb 2025 08:27:33 +0100 Subject: [PATCH 3/3] convert `extract_` methods from for loops to iterators --- utoipauto-core/src/attribute_utils.rs | 76 ++++++++++----------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/utoipauto-core/src/attribute_utils.rs b/utoipauto-core/src/attribute_utils.rs index 5c1edd7..8f02c42 100644 --- a/utoipauto-core/src/attribute_utils.rs +++ b/utoipauto-core/src/attribute_utils.rs @@ -42,8 +42,8 @@ pub fn build_new_openapi_attributes( uto_responses: &TokenStream, ) -> Attribute { let paths = extract_paths(&nested_attributes); - let schemas = extract_schemas(&nested_attributes); - let responses = extract_responses(&nested_attributes); + let schemas = extract_components(&nested_attributes, "schemas"); + let responses = extract_components(&nested_attributes, "responses"); let remaining_nested_attributes = remove_paths_and_components(nested_attributes); let uto_paths = match uto_paths.is_empty() { @@ -67,7 +67,7 @@ pub fn build_new_openapi_attributes( } fn remove_paths_and_components(nested_attributes: Punctuated) -> TokenStream { - let mut remaining = Vec::with_capacity(nested_attributes.len()); + let mut remaining = Vec::new(); for meta in nested_attributes { match meta { Meta::List(list) if list.path.is_ident("paths") => (), @@ -82,56 +82,36 @@ fn remove_paths_and_components(nested_attributes: Punctuated) - } fn extract_paths(nested_attributes: &Punctuated) -> TokenStream { - for meta in nested_attributes { - if let Meta::List(list) = meta { - if list.path.is_ident("paths") { - return list.tokens.clone(); - } - } - } - - TokenStream::new() + nested_attributes + .iter() + .find_map(|meta| { + let Meta::List(list) = meta else { return None }; + list.path.is_ident("paths").then(|| list.tokens.clone()) + }) + .unwrap_or_else(TokenStream::new) } -fn extract_schemas(nested_attributes: &Punctuated) -> TokenStream { - for meta in nested_attributes { - if let Meta::List(list) = meta { - if list.path.is_ident("components") { - let nested = list - .parse_args_with(Punctuated::::parse_terminated) - .expect("Expected a list of attributes inside components(...)!"); - for meta in nested { - if let Meta::List(list) = meta { - if list.path.is_ident("schemas") { - return list.tokens; - } - } - } +fn extract_components(nested_attributes: &Punctuated, component_kind: &str) -> TokenStream { + nested_attributes + .iter() + .find_map(|meta| { + let Meta::List(list) = meta else { return None }; + if !list.path.is_ident("components") { + return None; } - } - } - TokenStream::new() -} -fn extract_responses(nested_attributes: &Punctuated) -> TokenStream { - for meta in nested_attributes { - if let Meta::List(list) = meta { - if list.path.is_ident("components") { - let nested = list - .parse_args_with(Punctuated::::parse_terminated) - .expect("Expected a list of attributes inside components(...)!"); - for meta in nested { - if let Meta::List(list) = meta { - if list.path.is_ident("responses") { - return list.tokens; - } - } - } - } - } - } - TokenStream::new() + let nested = list + .parse_args_with(Punctuated::::parse_terminated) + .expect("Expected a list of attributes inside components(...)!"); + + nested.iter().find_map(|meta| { + let Meta::List(list) = meta else { return None }; + list.path.is_ident(component_kind).then(|| list.tokens.clone()) + }) + }) + .unwrap_or_else(TokenStream::new) } + #[cfg(test)] mod test { use proc_macro2::TokenStream;