gemfra_codegen/
lib.rs

1//! Macros for gemfra
2//!
3//! ## [route](macro@route) macro
4//!
5//! A macro that allows you to write routes for a [RoutedApp](gemfra::routed::RoutedApp).
6//!
7//! ```
8//! use gemfra::{
9//!     response::Response,
10//!     request::Request,
11//!     error::AnyError,
12//! };
13//! use gemfra_codegen::route;
14//!
15//! #[route("/foo/:bar")]
16//! async fn my_route(request: Request, bar: &str) -> Result<Response, AnyError> {
17//!     todo!()
18//! }
19//! ```
20
21use std::collections::HashSet;
22
23use proc_macro::TokenStream;
24use proc_macro_error::{abort, proc_macro_error};
25use quote::{quote, quote_spanned};
26use syn::{parse_macro_input, spanned::Spanned, FnArg, Item, LitStr, Type};
27
28/// Convert the provided route into a struct that implements [Route](gemfra::routed::Route).
29///
30/// The macro should get an endpoint that the route will handle. This can have
31/// variables which can be passed to the route function.
32///
33/// The route function will need a parameter named `request` and can optionally
34/// have parameters specified by the endpoint. Internally, the route function is
35/// async, so it does not matter whether the route function is marked async.
36///
37/// The endpoint can contain four kinds of segments:
38///
39/// * __segments__: these are of the format `/a/b`.
40/// * __params__: these are of the format `/a/:b`.
41/// * __named wildcards__: these are of the format `/a/*b`.
42/// * __unnamed wildcards__: these are of the format `/a/*`.
43///
44/// Only params and named wildcards can be passed to the route function. By default,
45/// a parameter is of type `&str`. You can however specify any type that impls
46/// [FromStr](std::str::FromStr). The param will be parsed, and if it fails, a
47/// `51 File not found` will be sent.
48///
49/// > Note that currently, it is not possible to have mutliple routes with the
50/// > same endpoint, but different parameter types.
51///
52/// ### Examples
53///
54/// ```
55/// use gemfra::{
56///     response::Response,
57///     request::Request,
58///     error::AnyError,
59/// };
60/// use gemfra_codegen::route;
61///
62/// #[route("/foo/bar")]
63/// async fn no_params(_request: Request) -> Result<Response, AnyError> {
64///     Ok(Response::success("text/gemini", "# Hello World!"))
65/// }
66///
67/// #[route("/foo/:my_var")]
68/// async fn default_param(_request: Request, my_var: &str) -> Result<Response, AnyError> {
69///     Ok(Response::success("text/gemini", format!("# Hello {my_var}")))
70/// }
71///
72/// #[route("/foo/:year")]
73/// async fn typed_param(_request: Request, year: i32) -> Result<Response, AnyError> {
74///     // Any non i32 value for year will result in a `51 File not found`
75///     Ok(Response::success("text/gemini", format!("# The year is {year}")))
76/// }
77/// ```
78#[proc_macro_error]
79#[proc_macro_attribute]
80pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
81    let endpoint = parse_macro_input!(args as LitStr);
82
83    let endpoint_val = endpoint.value();
84    let mut param_names = HashSet::new();
85    for segment in endpoint_val.split("/") {
86        if segment.starts_with(":") || segment.starts_with("*") {
87            if segment == "*" {
88                // We don't want unnamed
89                continue;
90            }
91            if !(param_names.insert(segment[1..].to_owned())) {
92                abort!(
93                    endpoint.span(),
94                    "Cannot have multiple named parameters with the same name";
95                    help = "Rename or remove one of the parameters named `{}`", &segment[1..]
96                );
97            }
98        }
99    }
100
101    let input = parse_macro_input!(input as Item);
102
103    // Extract the function from the input
104    let func = match &input {
105        Item::Fn(f) => f,
106        _ => {
107            abort!(input.span(), "You can only use route on functions");
108        }
109    };
110    let name = &func.sig.ident;
111    let return_ty = &func.sig.output;
112    let block = &func.block;
113
114    // Extract all the parameters
115    let mut request_arg = None;
116    let mut params = Vec::new();
117    for arg in &func.sig.inputs {
118        if let FnArg::Typed(arg) = arg {
119            if let syn::Pat::Ident(ident) = arg.pat.as_ref() {
120                let mut arg_name = ident.ident.to_string();
121                if arg_name.starts_with("_") {
122                    arg_name.remove(0);
123                }
124                if arg_name == "request" {
125                    request_arg = Some(arg);
126                } else {
127                    if !param_names.contains(&arg_name) {
128                        abort!(
129                            arg.span(), "Parameter `{}` not in endpoint", arg_name;
130                            note = endpoint.span() => "Add `{}` to the endpoint", arg_name
131                        );
132                    }
133
134                    let ty = &arg.ty;
135                    let param_lit = LitStr::new(&arg_name, ident.ident.span());
136
137                    let get_param = quote! {
138                        gemfra::error::ToGemError::into_gem(params.find(#param_lit))?
139                    };
140
141                    // If the type is `&str`, we don't need to parse the value
142                    if let Type::Reference(r) = ty.as_ref() {
143                        if let Type::Path(path) = r.elem.as_ref() {
144                            if let Some(segment) = path.path.segments.first() {
145                                if segment.ident.to_string() == "str" {
146                                    params.push(quote_spanned! {arg.span()=>
147                                        let #ident: #ty = #get_param;
148                                    });
149                                    continue;
150                                }
151                            }
152                        }
153                    }
154
155                    // Parse the type into the requested type
156                    params.push(quote_spanned! {arg.span()=>
157                        let #ident: #ty = gemfra::error::ToGemError::into_gem_type(
158                            #get_param.parse(),
159                            gemfra::error::GemErrorType::NotFound
160                        )?;
161                    });
162                }
163            }
164        }
165    }
166    let request_arg = match request_arg {
167        Some(v) => v,
168        None => {
169            abort!(func.sig.span(), "input `request` is a required parameter");
170        }
171    };
172
173    TokenStream::from(quote! {
174        #[allow(non_camel_case_types)]
175        struct #name;
176
177        #[async_trait::async_trait]
178        impl gemfra::routed::Route for #name {
179            fn endpoint(&self) -> &str {
180                #endpoint
181            }
182
183            async fn handle(&self, params: &gemfra::routed::Params, #request_arg) #return_ty {
184                #(#params)*
185                #block
186            }
187        }
188    })
189}