axum_controller/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ## Basic route macro usage
4//! See the docs of [`axum_typed_routing`] for details on the route macro.
5//! For convenience we re-export the route macro & TypedRouter for you
6//! so that all you need to use on your side is `use axum_controller::*`
7//!
8//! ## Controller macro usage
9//!
10//! This crate also offers a controller() attribute macro.
11//! use it like this:
12//!
13//! ```
14#![doc = include_str!("../examples/controller.rs")]
15//! ```
16
17#![forbid(unsafe_code)]
18#![feature(proc_macro_diagnostic)]
19use proc_macro::TokenStream;
20use proc_macro2::Ident;
21use syn::{
22    parse::{Parse, ParseStream},
23    punctuated::Punctuated,
24    ItemImpl, MetaNameValue,
25};
26#[macro_use]
27extern crate quote;
28
29#[macro_use]
30extern crate syn;
31
32#[derive(Clone, Default)]
33struct MyAttrs {
34    middlewares: Vec<syn::Expr>,
35    path: Option<syn::Expr>,
36    state: Option<syn::Expr>,
37}
38
39impl Parse for MyAttrs {
40    fn parse(input: ParseStream) -> syn::Result<Self> {
41        let mut path: Option<syn::Expr> = None;
42        let mut state: Option<syn::Expr> = None;
43        let mut middlewares: Vec<syn::Expr> = Vec::new();
44
45        // some = "values", seperated = "with", commas = true
46        for nv in Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?.into_iter() {
47            let segs = nv.path.segments.clone().into_pairs();
48            let seg = segs.into_iter().next().unwrap().into_value();
49            let ident = seg.ident;
50            match ident.to_string().as_str() {
51                "path" => {
52                    if path.is_some() {
53                        return Err(syn::Error::new_spanned(path, "duplicate `path` attribute"));
54                    }
55                    path = Some(nv.value);
56                }
57                "state" => {
58                    if state.is_some() {
59                        return Err(syn::Error::new_spanned(
60                            state,
61                            "duplicate `state` attribute",
62                        ));
63                    }
64                    state = Some(nv.value);
65                }
66                "middleware" => middlewares.push(nv.value),
67                _ => {
68                    panic!(
69                        "Unknown attribute given to controller macro, only path,state & middleware allowed"
70                    )
71                }
72            }
73        }
74        Ok(Self {
75            state,
76            path,
77            middlewares,
78        })
79    }
80}
81
82#[derive(Clone)]
83struct MyItem {
84    struct_name: syn::Type,
85    route_fns: Vec<syn::Ident>,
86}
87
88impl Parse for MyItem {
89    fn parse(input: ParseStream) -> syn::Result<Self> {
90        let ast: ItemImpl = input.parse()?;
91        let struct_name = *(ast.clone().self_ty.clone());
92        let mut route_fns: Vec<syn::Ident> = vec![];
93
94        for item in ast.items.iter() {
95            if let syn::ImplItem::Fn(impl_item_fn) = item {
96                // let fn_name = &impl_item_fn.sig.ident;
97                for attr in impl_item_fn.attrs.clone() {
98                    if attr.path().is_ident("route") {
99                        let fn_name: Ident = impl_item_fn.sig.ident.clone();
100                        route_fns.push(fn_name);
101                    }
102                }
103            }
104        }
105
106        Ok(Self {
107            route_fns,
108            struct_name,
109        })
110    }
111}
112
113// TODO add better docs
114/// A macro that generates a into_router(\_: State<_>) impl which automatically wires up all `route`'s and the given middlewares, path-prefix etc
115///
116/// ## Syntax:
117/// ```
118/// use axum_controller::controller;
119///
120/// struct ExampleController;
121/// #[controller(
122///   path = "/asd",
123/// )]
124/// impl ExampleController { /* ... */ }
125/// ```
126/// - path
127///   - optional, 0-1 allowed, defaults to `"/"`
128///   - A path to prefix `.nest` the `routes` in the controller Struct under
129/// - state
130///   - optional, 0-1 allowed, defaults to `"()"`)
131///   - The type signature of the state given to the routes
132/// - middleware
133///   - optional, 0-n allowed, default to [] (no middlewares)
134///   - Middlewares to `.layer` in the created router
135///
136#[proc_macro_attribute]
137pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
138    let args = parse_macro_input!(attr as MyAttrs);
139    let item2: proc_macro2::TokenStream = item.clone().into();
140    let myimpl = parse_macro_input!(item as MyItem);
141
142    let state = args.state.unwrap_or(parse_quote!(()));
143    let route_fns = myimpl.route_fns;
144    let struct_name = &myimpl.struct_name;
145    let route = args.path.unwrap_or(syn::parse_quote!("/"));
146
147    let route_calls = route_fns
148        .into_iter()
149        .map(move |route| {
150            quote! {
151            .typed_route(#struct_name :: #route)
152
153                  }
154        })
155        .collect::<Vec<_>>();
156
157    let nesting_call = quote! {
158        .nest(#route, __nested_router)
159    };
160
161    let nested_router_qoute = quote! {
162        axum::Router::new()
163        #nesting_call
164    };
165    let unnested_router_quote = quote! {
166        __nested_router
167    };
168    let maybe_nesting_call = if let syn::Expr::Lit(lit) = route {
169        if lit.eq(&syn::parse_quote!("/")) {
170            unnested_router_quote
171        } else {
172            nested_router_qoute
173        }
174    } else {
175        nested_router_qoute
176    };
177
178    let middleware_calls = args
179        .middlewares
180        .clone()
181        .into_iter()
182        .map(|middleware| quote! {.layer(#middleware)})
183        .collect::<Vec<_>>();
184
185    // TODO Checck if 2 possible to make 2 impls
186    // where state of parent router is ()
187    // one where it's #state
188    let from_controller_into_router_impl = quote! {
189        impl #struct_name {
190            pub fn into_router(state: #state) -> axum::Router<#state> {
191                let __nested_router = axum::Router::new()
192                    #(#route_calls)*
193                    #(#middleware_calls)*
194                    .with_state(state)
195                    ;
196
197                    #maybe_nesting_call
198            }
199        }
200    };
201
202    let res: TokenStream = quote! {
203        #item2
204        #from_controller_into_router_impl
205    }
206    .into();
207
208    res
209}