@@ -56,6 +56,7 @@ fn process_route_attribute(
5656}
5757
5858/// route attribute macro
59+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
5960#[ proc_macro_attribute]
6061pub fn route ( attr : TokenStream , item : TokenStream ) -> TokenStream {
6162 match process_route_attribute ( attr. into ( ) , item. into ( ) ) {
@@ -109,6 +110,7 @@ fn process_derive_schema(input: &syn::DeriveInput) -> (StructMetadata, proc_macr
109110/// Derive macro for Schema
110111///
111112/// Supports `#[schema(name = "CustomName")]` attribute to set custom OpenAPI schema name.
113+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
112114#[ proc_macro_derive( Schema , attributes( schema) ) ]
113115pub fn derive_schema ( input : TokenStream ) -> TokenStream {
114116 let input = syn:: parse_macro_input!( input as syn:: DeriveInput ) ;
@@ -162,6 +164,7 @@ pub fn derive_schema(input: TokenStream) -> TokenStream {
162164/// // For list endpoints, only return summary fields
163165/// let list_schema = schema!(User, pick = ["id", "name"]);
164166/// ```
167+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
165168#[ proc_macro]
166169pub fn schema ( input : TokenStream ) -> TokenStream {
167170 let input = syn:: parse_macro_input!( input as schema_macro:: SchemaInput ) ;
@@ -229,6 +232,7 @@ pub fn schema(input: TokenStream) -> TokenStream {
229232/// // ...
230233/// }
231234/// ```
235+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
232236#[ proc_macro]
233237pub fn schema_type ( input : TokenStream ) -> TokenStream {
234238 let input = syn:: parse_macro_input!( input as schema_macro:: SchemaTypeInput ) ;
@@ -669,46 +673,49 @@ fn generate_and_write_openapi(
669673 Ok ( ( docs_info, redoc_info) )
670674}
671675
672- # [ proc_macro ]
673- pub fn vespera ( input : TokenStream ) -> TokenStream {
674- let input = syn :: parse_macro_input! ( input as AutoRouterInput ) ;
675- let processed = process_vespera_input ( input ) ;
676-
676+ /// Process vespera macro - extracted for testability
677+ fn process_vespera_macro (
678+ processed : & ProcessedVesperaInput ,
679+ schema_storage : & [ StructMetadata ] ,
680+ ) -> syn :: Result < proc_macro2 :: TokenStream > {
677681 let folder_path = find_folder_path ( & processed. folder_name ) ;
678682 if !folder_path. exists ( ) {
679- return syn:: Error :: new (
683+ return Err ( syn:: Error :: new (
680684 Span :: call_site ( ) ,
681685 format ! ( "Folder not found: {}" , processed. folder_name) ,
682- )
683- . to_compile_error ( )
684- . into ( ) ;
686+ ) ) ;
685687 }
686688
687- let mut metadata = match collect_metadata ( & folder_path, & processed. folder_name ) {
688- Ok ( m) => m,
689- Err ( e) => {
690- return syn:: Error :: new (
691- Span :: call_site ( ) ,
692- format ! ( "Failed to collect metadata: {}" , e) ,
693- )
694- . to_compile_error ( )
695- . into ( ) ;
696- }
697- } ;
698- metadata
699- . structs
700- . extend ( SCHEMA_STORAGE . lock ( ) . unwrap ( ) . clone ( ) ) ;
701-
702- let ( docs_info, redoc_info) = match generate_and_write_openapi ( & processed, & metadata) {
703- Ok ( info) => info,
704- Err ( e) => {
705- return syn:: Error :: new ( Span :: call_site ( ) , e)
706- . to_compile_error ( )
707- . into ( ) ;
708- }
709- } ;
689+ let mut metadata = collect_metadata ( & folder_path, & processed. folder_name ) . map_err ( |e| {
690+ syn:: Error :: new (
691+ Span :: call_site ( ) ,
692+ format ! ( "Failed to collect metadata: {}" , e) ,
693+ )
694+ } ) ?;
695+ metadata. structs . extend ( schema_storage. iter ( ) . cloned ( ) ) ;
696+
697+ let ( docs_info, redoc_info) = generate_and_write_openapi ( processed, & metadata)
698+ . map_err ( |e| syn:: Error :: new ( Span :: call_site ( ) , e) ) ?;
699+
700+ Ok ( generate_router_code (
701+ & metadata,
702+ docs_info,
703+ redoc_info,
704+ & processed. merge ,
705+ ) )
706+ }
707+
708+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
709+ #[ proc_macro]
710+ pub fn vespera ( input : TokenStream ) -> TokenStream {
711+ let input = syn:: parse_macro_input!( input as AutoRouterInput ) ;
712+ let processed = process_vespera_input ( input) ;
713+ let schema_storage = SCHEMA_STORAGE . lock ( ) . unwrap ( ) ;
710714
711- generate_router_code ( & metadata, docs_info, redoc_info, & processed. merge ) . into ( )
715+ match process_vespera_macro ( & processed, & schema_storage) {
716+ Ok ( tokens) => tokens. into ( ) ,
717+ Err ( e) => e. to_compile_error ( ) . into ( ) ,
718+ }
712719}
713720
714721fn find_folder_path ( folder_name : & str ) -> std:: path:: PathBuf {
@@ -971,72 +978,62 @@ impl Parse for ExportAppInput {
971978/// // pub fn router() -> axum::Router { ... }
972979/// // }
973980/// ```
974- #[ proc_macro]
975- pub fn export_app ( input : TokenStream ) -> TokenStream {
976- let ExportAppInput { name, dir } = syn:: parse_macro_input!( input as ExportAppInput ) ;
977-
978- let folder_name = dir
979- . map ( |d| d. value ( ) )
980- . or_else ( || std:: env:: var ( "VESPERA_DIR" ) . ok ( ) )
981- . unwrap_or_else ( || "routes" . to_string ( ) ) ;
982-
983- let folder_path = find_folder_path ( & folder_name) ;
981+ ///
982+ /// Process export_app macro - extracted for testability
983+ fn process_export_app (
984+ name : & syn:: Ident ,
985+ folder_name : & str ,
986+ schema_storage : & [ StructMetadata ] ,
987+ manifest_dir : & str ,
988+ ) -> syn:: Result < proc_macro2:: TokenStream > {
989+ let folder_path = find_folder_path ( folder_name) ;
984990 if !folder_path. exists ( ) {
985- return syn:: Error :: new (
991+ return Err ( syn:: Error :: new (
986992 Span :: call_site ( ) ,
987993 format ! ( "Folder not found: {}" , folder_name) ,
988- )
989- . to_compile_error ( )
990- . into ( ) ;
994+ ) ) ;
991995 }
992996
993- let mut metadata = match collect_metadata ( & folder_path, & folder_name) {
994- Ok ( m) => m,
995- Err ( e) => {
996- return syn:: Error :: new (
997- Span :: call_site ( ) ,
998- format ! ( "Failed to collect metadata: {}" , e) ,
999- )
1000- . to_compile_error ( )
1001- . into ( ) ;
1002- }
1003- } ;
1004- metadata
1005- . structs
1006- . extend ( SCHEMA_STORAGE . lock ( ) . unwrap ( ) . clone ( ) ) ;
997+ let mut metadata = collect_metadata ( & folder_path, folder_name) . map_err ( |e| {
998+ syn:: Error :: new (
999+ Span :: call_site ( ) ,
1000+ format ! ( "Failed to collect metadata: {}" , e) ,
1001+ )
1002+ } ) ?;
1003+ metadata. structs . extend ( schema_storage. iter ( ) . cloned ( ) ) ;
10071004
10081005 // Generate OpenAPI spec JSON string
10091006 let openapi_doc = generate_openapi_doc_with_metadata ( None , None , None , & metadata) ;
1010- let spec_json = match serde_json:: to_string ( & openapi_doc) {
1011- Ok ( json) => json,
1012- Err ( e) => {
1013- return syn:: Error :: new (
1014- Span :: call_site ( ) ,
1015- format ! ( "Failed to serialize OpenAPI spec: {}" , e) ,
1016- )
1017- . to_compile_error ( )
1018- . into ( ) ;
1019- }
1020- } ;
1007+ let spec_json = serde_json:: to_string ( & openapi_doc) . map_err ( |e| {
1008+ syn:: Error :: new (
1009+ Span :: call_site ( ) ,
1010+ format ! ( "Failed to serialize OpenAPI spec: {}" , e) ,
1011+ )
1012+ } ) ?;
10211013
10221014 // Write spec to temp file for compile-time merging by parent apps
1023- // The file is written to target/vespera/{StructName}.openapi.json
10241015 let name_str = name. to_string ( ) ;
1025- let manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" ) . expect ( "CARGO_MANIFEST_DIR not set" ) ;
1026- // Find target directory (go up from manifest dir to workspace root if needed)
1027- let manifest_path = Path :: new ( & manifest_dir) ;
1016+ let manifest_path = Path :: new ( manifest_dir) ;
10281017 let target_dir = find_target_dir ( manifest_path) ;
10291018 let vespera_dir = target_dir. join ( "vespera" ) ;
1030- std:: fs:: create_dir_all ( & vespera_dir)
1031- . unwrap_or_else ( |e| panic ! ( "Failed to create vespera dir {:?}: {}" , vespera_dir, e) ) ;
1019+ std:: fs:: create_dir_all ( & vespera_dir) . map_err ( |e| {
1020+ syn:: Error :: new (
1021+ Span :: call_site ( ) ,
1022+ format ! ( "Failed to create vespera dir {:?}: {}" , vespera_dir, e) ,
1023+ )
1024+ } ) ?;
10321025 let spec_file = vespera_dir. join ( format ! ( "{}.openapi.json" , name_str) ) ;
1033- std:: fs:: write ( & spec_file, & spec_json)
1034- . unwrap_or_else ( |e| panic ! ( "Failed to write spec file {:?}: {}" , spec_file, e) ) ;
1026+ std:: fs:: write ( & spec_file, & spec_json) . map_err ( |e| {
1027+ syn:: Error :: new (
1028+ Span :: call_site ( ) ,
1029+ format ! ( "Failed to write spec file {:?}: {}" , spec_file, e) ,
1030+ )
1031+ } ) ?;
10351032
10361033 // Generate router code (without docs routes, no merge)
10371034 let router_code = generate_router_code ( & metadata, None , None , & [ ] ) ;
10381035
1039- quote ! {
1036+ Ok ( quote ! {
10401037 /// Auto-generated vespera app struct
10411038 pub struct #name;
10421039
@@ -1050,8 +1047,24 @@ pub fn export_app(input: TokenStream) -> TokenStream {
10501047 #router_code
10511048 }
10521049 }
1050+ } )
1051+ }
1052+
1053+ #[ cfg_attr( coverage_nightly, coverage( off) ) ]
1054+ #[ proc_macro]
1055+ pub fn export_app ( input : TokenStream ) -> TokenStream {
1056+ let ExportAppInput { name, dir } = syn:: parse_macro_input!( input as ExportAppInput ) ;
1057+ let folder_name = dir
1058+ . map ( |d| d. value ( ) )
1059+ . or_else ( || std:: env:: var ( "VESPERA_DIR" ) . ok ( ) )
1060+ . unwrap_or_else ( || "routes" . to_string ( ) ) ;
1061+ let schema_storage = SCHEMA_STORAGE . lock ( ) . unwrap ( ) ;
1062+ let manifest_dir = std:: env:: var ( "CARGO_MANIFEST_DIR" ) . expect ( "CARGO_MANIFEST_DIR not set" ) ;
1063+
1064+ match process_export_app ( & name, & folder_name, & schema_storage, & manifest_dir) {
1065+ Ok ( tokens) => tokens. into ( ) ,
1066+ Err ( e) => e. to_compile_error ( ) . into ( ) ,
10531067 }
1054- . into ( )
10551068}
10561069
10571070#[ cfg( test) ]
@@ -2537,4 +2550,78 @@ pub fn get_users() -> String {
25372550 assert ! ( result. is_ok( ) , "Method {} should be valid" , method) ;
25382551 }
25392552 }
2553+
2554+ // ========== Tests for process_vespera_macro ==========
2555+
2556+ #[ test]
2557+ fn test_process_vespera_macro_folder_not_found ( ) {
2558+ let processed = ProcessedVesperaInput {
2559+ folder_name : "nonexistent_folder_xyz_123" . to_string ( ) ,
2560+ openapi_file_names : vec ! [ ] ,
2561+ title : None ,
2562+ version : None ,
2563+ docs_url : None ,
2564+ redoc_url : None ,
2565+ servers : None ,
2566+ merge : vec ! [ ] ,
2567+ } ;
2568+ let result = process_vespera_macro ( & processed, & [ ] ) ;
2569+ assert ! ( result. is_err( ) ) ;
2570+ let err = result. unwrap_err ( ) . to_string ( ) ;
2571+ assert ! ( err. contains( "Folder not found" ) ) ;
2572+ }
2573+
2574+ // ========== Tests for process_export_app ==========
2575+
2576+ #[ test]
2577+ fn test_process_export_app_folder_not_found ( ) {
2578+ let name: syn:: Ident = syn:: parse_quote!( TestApp ) ;
2579+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
2580+ let result = process_export_app (
2581+ & name,
2582+ "nonexistent_folder_xyz" ,
2583+ & [ ] ,
2584+ & temp_dir. path ( ) . to_string_lossy ( ) ,
2585+ ) ;
2586+ assert ! ( result. is_err( ) ) ;
2587+ let err = result. unwrap_err ( ) . to_string ( ) ;
2588+ assert ! ( err. contains( "Folder not found" ) ) ;
2589+ }
2590+
2591+ // ========== Tests for generate_and_write_openapi with merge ==========
2592+
2593+ #[ test]
2594+ fn test_generate_and_write_openapi_with_merge_no_manifest_dir ( ) {
2595+ // When CARGO_MANIFEST_DIR is not set or merge is empty, it should work normally
2596+ let processed = ProcessedVesperaInput {
2597+ folder_name : "routes" . to_string ( ) ,
2598+ openapi_file_names : vec ! [ ] ,
2599+ title : Some ( "Test" . to_string ( ) ) ,
2600+ version : None ,
2601+ docs_url : Some ( "/docs" . to_string ( ) ) ,
2602+ redoc_url : None ,
2603+ servers : None ,
2604+ merge : vec ! [ syn:: parse_quote!( app:: TestApp ) ] , // Has merge but no valid manifest dir
2605+ } ;
2606+ let metadata = CollectedMetadata :: new ( ) ;
2607+ // This should still work - merge logic is skipped when CARGO_MANIFEST_DIR lookup fails
2608+ let result = generate_and_write_openapi ( & processed, & metadata) ;
2609+ assert ! ( result. is_ok( ) ) ;
2610+ }
2611+
2612+ // ========== Tests for find_folder_path ==========
2613+
2614+ #[ test]
2615+ fn test_find_folder_path_absolute_path ( ) {
2616+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
2617+ let absolute_path = temp_dir. path ( ) . to_string_lossy ( ) . to_string ( ) ;
2618+
2619+ // When given an absolute path that exists, it should return it
2620+ let result = find_folder_path ( & absolute_path) ;
2621+ // The function tries src/{folder_name} first, then falls back to the folder_name directly
2622+ assert ! (
2623+ result. to_string_lossy( ) . contains( & absolute_path)
2624+ || result == Path :: new( & absolute_path)
2625+ ) ;
2626+ }
25402627}
0 commit comments