Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proc macro attribute to iterate through struct's field names and types #516

Closed
chinedufn opened this issue Oct 18, 2018 · 5 comments
Closed

Comments

@chinedufn
Copy link

chinedufn commented Oct 18, 2018

Hey!

So I'm diving around the examples trying to figure out how to implement a macro. The examples and docs are very informative so thank you! But I figured I'd post an issue in case you immediately knew what things I should be looking at.

#[route(path = "/users/:id/cars/:car_name"]
struct Foo {
  id: u32,
  car_name: String
}

I want the above code to generate the following impl block:

impl Foo {
  fn route () {
    println!("{}", path); // This would be /users/:id/cars/:car_name

    let id: u32 = 5; // 5 is just a hard coded number to illustrate
    let car_name: String = "Hello".to_string() // Hello is hard coded for illustration  
  }
}

So basically I want to get access to the path string as well as every field in my struct and the type
of that field.. Then I can use quote to generate some code.

I'm going to dig around to see how I can accomplish this since I see some examples that do different bits of this but nothing that encompasses all of this (I don't think?).. but if ya see this in the meantime could ya point me in the right direction?

@chinedufn chinedufn changed the title Proc macro attribute to iterate through names and types of struct Proc macro attribute to iterate through struct's field names and types Oct 18, 2018
@dtolnay
Copy link
Owner

dtolnay commented Oct 21, 2018

I would write this as:

// [package]
// name = "chinedu-example"
// version = "0.0.0"
// edition = "2018"
//
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "0.15"
// quote = "0.6"

extern crate proc_macro;
use self::proc_macro::TokenStream;

use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields, LitStr, Token};

struct RouteArgs {
    path: String,
}

mod keyword {
    syn::custom_keyword!(path);
}

impl Parse for RouteArgs {
    fn parse(input: ParseStream) -> Result<Self> {
        input.parse::<keyword::path>()?;
        input.parse::<Token![=]>()?;
        let path: LitStr = input.parse()?;

        Ok(RouteArgs {
            path: path.value(),
        })
    }
}

#[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as RouteArgs);
    let input = parse_macro_input!(input as DeriveInput);

    let fields = match &input.data {
        Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => &fields.named,
        _ => panic!("expected a struct with named fields"),
    };
    let field_name = fields.iter().map(|field| &field.ident);
    let field_type = fields.iter().map(|field| &field.ty);

    let path = args.path;
    let struct_name = &input.ident;

    TokenStream::from(quote! {
        // Preserve the input struct unchanged in the output.
        #input

        impl #struct_name {
            fn route() {
                println!("{}", #path);

                // The following repetition expands to, for example:
                //
                //    let id: u32 = Default::default();
                //    let car_name: String = Default::default();
                #(
                    let #field_name: #field_type = Default::default();
                )*
            }
        }
    })
}

Here is the short test program I used:

use chinedu_example::route;

#[route(path = "/users/:id/cars/:car_name")]
struct Foo {
    id: u32,
    car_name: String,
}

fn main() {
    Foo::route();
}

The implementation above uses a short Parse impl to parse the path = "..." attribute argument. A different possible way would be to use the existing parser provided by syn::AttributeArgs followed by destructuring to extract the literal value:

use syn::{Lit, Meta, NestedMeta};

#[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as AttributeArgs);

    assert_eq!(args.len(), 1);
    let argument_name_and_value = match args.get(0) {
        Some(NestedMeta::Meta(Meta::NameValue(meta))) => meta,
        _ => panic!("expected argument `path = \"...\"`"),
    };
    assert_eq!(argument_name_and_value.ident, "path");
    let path = match &argument_name_and_value.lit {
        Lit::Str(lit) => lit.value(),
        _ => panic!("path argument must be a string"),
    };

    /* ... */
}

@chinedufn
Copy link
Author

I AM SO EXCITED THANK YOU

You are the most helpful human to exist ever (maybe an overstatement but that's how I feel right now)

Thanks a lot. This plus the help that you gave me in real life is PLENTY to get my router attribute working.. CHEERS!

@chinedufn
Copy link
Author

chinedufn commented Mar 3, 2019

Alright so I've circled back to this. I didn't go with my original plan - but these tips still informed the final implementation.


I'm stuck on the last bit that I have left.

I'm trying to dynamically generate a path based on some identifier called route.

        let route_handler = format!("self::__{}_mod__::{}::new()", route, route);
        let route_handler = Ident::new(route_handler.as_str(), route.span());

        let route_handler = quote! {
            Box::new(#route_handler::new())
        };
        tokens.push(route_handler);

So say route is called root_route.

I want this to create Box::new(self::__route_route_mod__::root_route::new())

Of course, this code above doesn't work.

thread 'rustc' panicked at '`"self::__root_route_mod__::root_route::new()"` is not a valid identifier', src/libsyntax_ext/proc_macro_server.rs:342:13

That makes sense.

So, my question is, how can I create some tokens for my path, given that the path is dynamic so I can't just hard code it?

Sorry if I'm missing something obvious! As always thanks so much for any pointers!!

@dtolnay
Copy link
Owner

dtolnay commented Mar 3, 2019

As long as you stick to creating only Idents that are valid identifiers, it should work. Something like:

let route_mod_name = format!("__{}_mod__", route);
let route_mod = Ident::new(&route_mod_name, route.span());

quote! {
    Box::new(self :: #route_mod :: #route :: new())
}

@chinedufn
Copy link
Author

Worked perfectly, thanks!!!!!

Repository owner locked and limited conversation to collaborators Oct 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants