From 7de15960636f308b3fc48d0f4157c1520024939f Mon Sep 17 00:00:00 2001 From: Tristan Druyen Date: Tue, 15 Apr 2025 16:12:00 +0200 Subject: [PATCH] Split into seperate files --- CHANGELOG.md | 2 +- src/lib.rs | 366 ++------------------------------------------------- 2 files changed, 12 insertions(+), 356 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c5e9a..77faec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- No changes yet +- Refactored huge lib.rs into 3 seperate files. ## [0.3.3] - 2025-04-15 diff --git a/src/lib.rs b/src/lib.rs index e4282b8..992ae7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,77 +127,28 @@ //! - **Compile-time Only**: The routing is determined at compile time, so dynamic route registration isn't supported. //! - **Expects seperate directory**: To make rust-analyzer & co work correctly the macro imports all route.rs files inside the given directory tree. //! It is highly recommended to keep the route directory seperate from the rest of your module-tree. -use std::{ - collections::BTreeMap, - fmt::Write, - fs, - path::{Path, PathBuf}, -}; +use std::path::Path; use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - Ident, - Item, - LitStr, - Result, - Token, - Visibility, - parse::{Parse, ParseStream}, - parse_file, - parse_macro_input, -}; +use quote::quote; +use syn::parse_macro_input; -#[derive(Debug)] -struct FolderRouterArgs { - path: String, - state_type: Ident, -} - -impl Parse for FolderRouterArgs { - fn parse(input: ParseStream) -> Result { - let path_lit = input.parse::()?; - input.parse::()?; - let state_type = input.parse::()?; - - Ok(FolderRouterArgs { - path: path_lit.value(), - state_type, - }) - } -} - -// A struct representing a directory in the module tree -#[derive(Debug)] -struct ModuleDir { - name: String, - has_route: bool, - children: BTreeMap, -} - -impl ModuleDir { - fn new(name: &str) -> Self { - ModuleDir { - name: name.to_string(), - has_route: false, - children: BTreeMap::new(), - } - } -} +mod generate; +mod parse; /// Creates an Axum router module tree & creation function /// by scanning a directory for `route.rs` files. /// /// # Parameters /// -/// * `path` - A string literal pointing to the API directory, relative to the +/// * `path` - A string literal pointing to the route directory, relative to the /// Cargo manifest directory /// * `state_type` - The type name of your application state that will be shared /// across all routes #[allow(clippy::missing_panics_doc)] #[proc_macro_attribute] pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as FolderRouterArgs); + let args = parse_macro_input!(attr as parse::FolderRouterArgs); let input_item = parse_macro_input!(item as syn::ItemStruct); let struct_name = &input_item.ident; @@ -221,12 +172,7 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { .collect::() ); - // Collect route files - let mut routes = Vec::new(); - collect_route_files(&base_dir, &base_dir, &mut routes); - - // ensures deterministic macro output - routes.sort(); + let routes = parse::collect_route_files(&base_dir, &base_dir); if routes.is_empty() { return TokenStream::from(quote! { @@ -237,8 +183,8 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { }); } - let module_tree = module_tree(&mod_namespace, &base_dir, &routes); - let route_registrations = route_registrations(&mod_namespace, &routes); + let module_tree = generate::module_tree(&mod_namespace, &base_dir, &routes); + let route_registrations = generate::route_registrations(&mod_namespace, &routes); quote! { #module_tree @@ -256,9 +202,7 @@ pub fn folder_router(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -// this is a workaround for macrotest behaviour -// which makes it impossible to get deterministic macro results -// without this weird stuff +// This is a workaround for macrotest behaviour #[cfg(debug_assertions)] fn get_manifest_dir() -> String { use regex::Regex; @@ -276,291 +220,3 @@ fn get_manifest_dir() -> String { fn get_manifest_dir() -> String { std::env::var("CARGO_MANIFEST_DIR").unwrap_or("./".to_string()) } - -/// parses the file at the specified location using syn -/// and returns a Vec<&'static str> of all used http verb fns -/// e.g. for the file -/// -/// ```rust -/// pub async fn get() {} // ✅ => "get" be added to vec -/// pub fn post() {} // not async -/// async fn delete() {} // not pub -/// fn patch() {} // not pub nor async -/// pub fn non_verb() {} // not a http verb -/// ``` -/// -/// it returns: `vec!["get"]` -fn methods_for_route(route_path: &PathBuf) -> Vec<&'static str> { - // Read the file content - let Ok(file_content) = fs::read_to_string(route_path) else { - return Vec::new(); - }; - - // Parse the file content into a syn syntax tree - let Ok(file) = parse_file(&file_content) else { - return Vec::new(); - }; - - // Define HTTP methods we're looking for - let allowed_methods = [ - "any", "get", "post", "put", "delete", "patch", "head", "options", "trace", "connect", - ]; - let mut found_methods = Vec::new(); - - // Collect all pub & async fn's - for item in &file.items { - if let Item::Fn(fn_item) = item { - let fn_name = fn_item.sig.ident.to_string(); - - // Check if the function name is one of our HTTP methods - // if let Some(&method) = methods.iter().find(|&&m| m == fn_name) { - // Check if the function is public - let is_public = matches!(fn_item.vis, Visibility::Public(_)); - - // Check if the function is async - let is_async = fn_item.sig.asyncness.is_some(); - - if is_public && is_async { - found_methods.push(fn_name); - } - // } - } - } - - // Iterate through methods to ensure consistent order - allowed_methods - .into_iter() - .filter(|elem| { - found_methods - .clone() - .into_iter() - .any(|method| method == *elem) - }) - .collect() -} - -fn route_registrations( - // root_namespace_str: String, - // base_dir: String, - root_namespace_str: &str, - routes: &Vec<(PathBuf, PathBuf)>, -) -> proc_macro2::TokenStream { - let root_namespace_ident = format_ident!("{}", root_namespace_str); - - let mut route_registrations = Vec::new(); - for (route_path, rel_path) in routes { - // Generate module path and axum path - let (axum_path, mod_path) = path_to_module_path(rel_path); - - let method_registrations = methods_for_route(route_path); - - if method_registrations.is_empty() { - return quote! { - compile_error!(concat!( - "No routes defined in your route.rs's !\n", - "ensure that at least one `pub async fn` named after an HTTP verb is defined. (e.g. get, post, put, delete)" - )); - }; - } - - let first_method = &method_registrations[0]; - let first_method_ident = format_ident!("{}", first_method); - - let mod_path_tokens = generate_mod_path_tokens(&mod_path); - - let mut builder = quote! { - axum::routing::#first_method_ident(#root_namespace_ident::#mod_path_tokens::#first_method_ident) - }; - - for method in &method_registrations[1..] { - let method_ident = format_ident!("{}", method); - - builder = quote! { - #builder.#method_ident(#root_namespace_ident::#mod_path_tokens::#method_ident) - }; - } - - let registration = quote! { - router = router.route(#axum_path, #builder); - }; - route_registrations.push(registration); - } - - proc_macro2::TokenStream::from_iter(route_registrations) -} - -fn module_tree( - root_namespace_str: &str, - base_dir: &Path, - routes: &Vec<(PathBuf, PathBuf)>, -) -> proc_macro2::TokenStream { - let root_namespace_ident = format_ident!("{}", root_namespace_str); - let base_path_lit = LitStr::new( - base_dir.to_str().unwrap_or("./"), - proc_macro2::Span::call_site(), - ); - - let mut root = ModuleDir::new(root_namespace_str); - for (route_path, rel_path) in routes { - add_to_module_tree(&mut root, rel_path, route_path); - } - - let mod_hierarchy = generate_module_hierarchy(&root); - quote! { - #[path = #base_path_lit] - mod #root_namespace_ident { - #mod_hierarchy - } - } -} - -// Add a route to the module tree -fn add_to_module_tree(root: &mut ModuleDir, rel_path: &Path, _route_path: &Path) { - let components: Vec<_> = rel_path - .components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect(); - - if components.is_empty() { - root.has_route = true; - return; - } - - let mut root = root; - - for (i, segment) in components.iter().enumerate() { - if i == components.len() - 1 && segment == "route.rs" { - root.has_route = true; - break; - } - - root = root - .children - .entry(segment.clone()) - .or_insert_with(|| ModuleDir::new(segment)); - } -} - -// Generate module hierarchy code -fn generate_module_hierarchy(dir: &ModuleDir) -> proc_macro2::TokenStream { - let mut result = proc_macro2::TokenStream::new(); - - // Add route.rs module if this directory has one - if dir.has_route { - let route_mod = quote! { - #[path = "route.rs"] - pub mod route; - }; - result.extend(route_mod); - } - - // Add subdirectories - for child in dir.children.values() { - let child_name = format_ident!("{}", normalize_module_name(&child.name)); - let child_path_lit = LitStr::new(&child.name, proc_macro2::Span::call_site()); - let child_content = generate_module_hierarchy(child); - - let child_mod = quote! { - #[path = #child_path_lit] - pub mod #child_name { - #child_content - } - }; - - result.extend(child_mod); - } - - result -} - -// Generate tokens for a module path -fn generate_mod_path_tokens(mod_path: &[String]) -> proc_macro2::TokenStream { - let mut result = proc_macro2::TokenStream::new(); - - for (i, segment) in mod_path.iter().enumerate() { - let segment_ident = format_ident!("{}", segment); - - if i == 0 { - result = quote! { #segment_ident }; - } else { - result = quote! { #result::#segment_ident }; - } - } - - result -} - -// Normalize a path segment for use as a module name -fn normalize_module_name(name: &str) -> String { - if name.starts_with('[') && name.ends_with(']') { - let inner = &name[1..name.len() - 1]; - if let Some(stripped) = inner.strip_prefix("...") { - format!("___{stripped}") - } else { - format!("__{inner}") - } - } else { - name.replace(['-', '.'], "_") - } -} - -// Convert a relative path to module path segments and axum route path -fn path_to_module_path(rel_path: &Path) -> (String, Vec) { - let mut axum_path = String::new(); - let mut mod_path = Vec::new(); - - let components: Vec<_> = rel_path - .components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect(); - - // Handle root route - if components.is_empty() { - return ("/".to_string(), vec!["route".to_string()]); - } - - for (i, segment) in components.iter().enumerate() { - if i == components.len() - 1 && segment == "route.rs" { - mod_path.push("route".to_string()); - } else { - // Process directory name - let normalized = normalize_module_name(segment); - mod_path.push(normalized); - - // Process URL path - if segment.starts_with('[') && segment.ends_with(']') { - let param = &segment[1..segment.len() - 1]; - if let Some(stripped) = param.strip_prefix("...") { - write!(&mut axum_path, "/{{*{stripped}}}").unwrap(); - } else { - write!(&mut axum_path, "/{{:{param}}}").unwrap(); - } - } else { - write!(&mut axum_path, "/{segment}").unwrap(); - } - } - } - - if axum_path.is_empty() { - axum_path = "/".to_string(); - } - - (axum_path, mod_path) -} - -// Collect route.rs files recursively -fn collect_route_files(base_dir: &Path, dir: &Path, routes: &mut Vec<(PathBuf, PathBuf)>) { - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.filter_map(std::result::Result::ok) { - let path = entry.path(); - - if path.is_dir() { - collect_route_files(base_dir, &path, routes); - } else if path.file_name().unwrap_or_default() == "route.rs" { - if let Ok(rel_dir) = path.strip_prefix(base_dir) { - routes.push((path.clone(), rel_dir.to_path_buf())); - } - } - } - } -}