ruled_router_derive/
lib.rs

1//! Procedural macros for ruled-router
2//!
3//! This crate provides derive macros for automatically implementing
4//! the RouterData and Query traits.
5
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use syn::{parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, Lit, LitStr, Meta};
9
10mod query;
11mod querystring;
12mod route;
13mod router_match;
14
15use query::expand_query_derive;
16use querystring::expand_querystring_derive;
17use route::expand_route_derive;
18use router_match::expand_router_match_derive;
19
20/// sub_router attribute macro
21#[proc_macro_attribute]
22pub fn sub_router(_args: TokenStream, input: TokenStream) -> TokenStream {
23  input
24}
25
26/// Derive macro for implementing the RouterData trait
27///
28/// **Note**: `RouterData` is used for individual route definitions and cannot be used
29/// as a top-level router. For top-level routing, use `RouterMatch` with an enum structure
30/// that contains multiple `RouterData` implementations.
31///
32/// # Example
33///
34/// ```rust
35/// use ruled_router_derive::RouterData;
36/// use ruled_router::traits::{RouterData, RouteMatcher};
37///
38/// // Individual route - cannot be used as top-level router
39/// #[derive(RouterData)]
40/// #[router(pattern = "/users/:id")]
41/// struct UserRoute {
42///     id: u32,
43/// }
44///
45/// // For top-level routing, use RouterMatch with an enum:
46/// // #[derive(RouterMatch)]
47/// // enum AppRouter {
48/// //     User(UserRoute),
49/// //     // ... other routes
50/// // }
51/// ```
52#[proc_macro_derive(RouterData, attributes(router, query, sub_router))]
53pub fn derive_router(input: TokenStream) -> TokenStream {
54  let input = parse_macro_input!(input as DeriveInput);
55  expand_route_derive(input).unwrap_or_else(syn::Error::into_compile_error).into()
56}
57
58/// Derive macro for implementing the Query trait
59///
60/// # Example
61///
62/// ```rust
63/// use ruled_router_derive::QueryDerive;
64/// use ruled_router::traits::Query;
65///
66/// #[derive(QueryDerive)]
67/// struct SearchQuery {
68///     q: Option<String>,
69///     page: Option<u32>,
70/// }
71/// ```
72#[proc_macro_derive(QueryDerive, attributes(query))]
73pub fn derive_query(input: TokenStream) -> TokenStream {
74  let input = parse_macro_input!(input as DeriveInput);
75  expand_query_derive(input).unwrap_or_else(syn::Error::into_compile_error).into()
76}
77
78/// Derive macro for implementing querystring parsing and formatting
79///
80/// This macro automatically implements parsing from and formatting to
81/// query string format for structs.
82///
83/// # Example
84///
85/// ```rust
86/// use ruled_router_derive::QueryString;
87///
88/// #[derive(QueryString)]
89/// struct UserQuery {
90///     tab: Option<String>,
91///     active: Option<bool>,
92/// }
93/// ```
94#[proc_macro_derive(QueryString)]
95pub fn derive_querystring(input: TokenStream) -> TokenStream {
96  let input = parse_macro_input!(input as DeriveInput);
97  expand_querystring_derive(input)
98    .unwrap_or_else(syn::Error::into_compile_error)
99    .into()
100}
101
102/// Derive macro for implementing the RouterMatch trait
103///
104/// **This is the recommended approach for top-level routing.** RouterMatch is designed
105/// to handle multiple route types in an enum structure, with automatic prefix extraction
106/// from each route's pattern.
107///
108/// This macro generates implementations for parsing and formatting
109/// nested router structures with automatic prefix extraction.
110///
111/// # Example
112///
113/// ```rust,ignore
114/// use ruled_router_derive::{RouterMatch, RouterData};
115/// use ruled_router::traits::{RouterMatch, RouterData};
116///
117/// // Individual routes
118/// #[derive(RouterData)]
119/// #[router(pattern = "/users/:id")]
120/// struct UserRoute { id: u32 }
121///
122/// #[derive(RouterData)]  
123/// #[router(pattern = "/blog/:slug")]
124/// struct BlogRoute { slug: String }
125///
126/// // Top-level router using RouterMatch
127/// #[derive(RouterMatch)]
128/// enum AppRouter {
129///     User(UserRoute),  // Automatically extracts "/users" prefix
130///     Blog(BlogRoute),  // Automatically extracts "/blog" prefix  
131///     Api(ApiRoute),
132/// }
133/// ```
134#[proc_macro_derive(RouterMatch)]
135pub fn derive_router_match(input: TokenStream) -> TokenStream {
136  let input = parse_macro_input!(input as DeriveInput);
137  expand_router_match_derive(input)
138    .unwrap_or_else(syn::Error::into_compile_error)
139    .into()
140}
141
142/// Extract route configuration from router attribute
143fn extract_route_config(input: &DeriveInput) -> syn::Result<(String, Option<String>)> {
144  for attr in &input.attrs {
145    if attr.path().is_ident("router") {
146      if let Meta::List(meta_list) = &attr.meta {
147        let mut pattern = None;
148        let mut query_type = None;
149
150        // Parse multiple name-value pairs
151        let parser = meta_list.parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)?;
152
153        for meta in parser {
154          if let Meta::NameValue(name_value) = meta {
155            if name_value.path.is_ident("pattern") {
156              if let syn::Expr::Lit(expr_lit) = &name_value.value {
157                if let Lit::Str(lit_str) = &expr_lit.lit {
158                  pattern = Some(lit_str.value());
159                }
160              }
161            } else if name_value.path.is_ident("query") {
162              if let syn::Expr::Lit(expr_lit) = &name_value.value {
163                if let Lit::Str(lit_str) = &expr_lit.lit {
164                  query_type = Some(lit_str.value());
165                }
166              }
167            }
168          }
169        }
170
171        if let Some(pattern) = pattern {
172          return Ok((pattern, query_type));
173        }
174      }
175    }
176  }
177  Err(syn::Error::new_spanned(input, "Missing #[router(pattern = \"...\")]"))
178}
179
180pub(crate) fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
181  let mut docs = Vec::new();
182
183  for attr in attrs {
184    if attr.path().is_ident("doc") {
185      match &attr.meta {
186        Meta::NameValue(name_value) => {
187          if let Expr::Lit(expr_lit) = &name_value.value {
188            if let Lit::Str(lit_str) = &expr_lit.lit {
189              docs.push(lit_str.value());
190            }
191          }
192        }
193        _ => {
194          if let Ok(lit_str) = attr.parse_args::<LitStr>() {
195            docs.push(lit_str.value());
196          }
197        }
198      }
199    }
200  }
201
202  if docs.is_empty() {
203    return None;
204  }
205
206  let combined = docs.join("\n");
207  let trimmed = combined.trim();
208  if trimmed.is_empty() {
209    None
210  } else {
211    Some(trimmed.to_string())
212  }
213}
214
215pub(crate) fn doc_comment_tokens(doc: Option<String>) -> proc_macro2::TokenStream {
216  match doc {
217    Some(text) => {
218      let lit = LitStr::new(&text, Span::call_site());
219      quote::quote! { Some(#lit) }
220    }
221    None => quote::quote! { None },
222  }
223}
224
225/// Extract field information from struct
226fn extract_struct_fields(data: &Data) -> syn::Result<Vec<(syn::Ident, syn::Type)>> {
227  match data {
228    Data::Struct(data_struct) => match &data_struct.fields {
229      Fields::Named(fields_named) => {
230        let mut field_info = Vec::new();
231        for field in &fields_named.named {
232          if let Some(ident) = &field.ident {
233            field_info.push((ident.clone(), field.ty.clone()));
234          }
235        }
236        Ok(field_info)
237      }
238      _ => Err(syn::Error::new_spanned(&data_struct.fields, "Only named fields are supported")),
239    },
240    _ => Err(syn::Error::new(proc_macro2::Span::call_site(), "Only structs are supported")),
241  }
242}