If you have created a web server in Rust using frameworks such as Rocket, you must have seen attribute macros that allow you to specify functions for different endpoints such as
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
In this article, we will create an attribute macro that will help you to understand the underlying implementation.
We will build upon the setup of the previous article:
Route
First, let’s create a trait `Route` that has an endpoint and a handler:
pub trait Route {
fn endpoint() -> String;
fn handle() -> Result<(), ()>;
}
Now, we will try to implement our `get` macro to generate a Unit Struct that implements this trait.
Macro Expansion
As done previously, we start with a proc_macro_attribute on a function named get:
#[proc_macro_attribute]
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
}
Function Name
Next, we try to parse the provided `item` as an `ItemFunc` because our macro is supposed to work on functions:
#[proc_macro_attribute]
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
//Avoid unwrap in production code
let func: ItemFn = syn::parse(item).unwrap();
}
Once we have our `func`, we can get its name and convert to a respective Struct name:
let ident = func.sig.ident;
let name = Ident::new(&ident.to_string().to_uppercase(), ident.span());
Endpoint
Finally, we need to figure out the endpoint. To do this, we can first define a parsed struct:
#[derive(Parse)]
struct EndpointParam {
name: LitStr,
}
And try to obtain it from the attributes:
let endpoint_param = syn::parse_macro_input!(attr as EndpointParam);
let endpoint = endpoint_param.name;
We are now ready to generate our implementation like so:
quote! {
struct #name;
impl Route for #name {
fn endpoint() -> String {
#endpoint.to_string()
}
fn handle() -> Result<(), ()> {
Ok(())
}
}
}.into()
The code above declares a unit struct with same name (but uppercased) as the function and implements `Route` for this struct, returning the endpoint and an `Ok(())` response.
Final Thoughts
We can now use our macro to define different routes like so:
#[get("/")]
fn index() {
}
#[get("/health")]
fn health() {
}
The code above generates two unit structs. If you run `cargo expand`, you should see the following output:
struct INDEX;
impl Route for INDEX {
fn endpoint() -> String {
"/".to_string()
}
fn handle() -> Result<(), ()> {
Ok(())
}
}
struct HEALTH;
impl Route for HEALTH {
fn endpoint() -> String {
"/health".to_string()
}
fn handle() -> Result<(), ()> {
Ok(())
}
}
Thus, our macro successfully generated different routes for respective endpoints.
In order to understand this further, you can try to change our expansion to call the provided function in `handle` instead of simply returning an `Ok(())` response. In case of any doubts, please feel free to ask in the comments section.