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}