DEV Community

Mark Nefedov
Mark Nefedov

Posted on

How to find all structs that Derive known macro in Rust

I've recently embarked on an endeavor to add migration generation to my favorite Rust ORM Welds.

Rust's powerful macro system allows us to extend the language in ways that can greatly enhance productivity and maintainability. In this article, I'll focus on a specific use case: finding all the structs in a codebase that derive a particular macro, in this case the WeldsModel macro.

The solution provided in the question uses the syn crate, a parsing library for Rust syntax, and the walkdir crate, which provides an iterator over the files in a directory and its subdirectories. Here, we'll walk through this solution and explain how it works.

The Main Function

First, let's look at the main function:

fn main() {
    for entry in walkdir::WalkDir::new("welds/examples")
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if path.is_file() && path.extension().unwrap() == "rs" {
            let mut file = File::open(path).expect("Unable to open file");
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .expect("Unable to read file");
            let syntax_tree = syn::parse_file(&contents).unwrap();
            let weldsmodel_structs = find_weldsmodel_structs(syntax_tree);
            for weldsmodel_struct in weldsmodel_structs {
                println!("Found struct: {}", weldsmodel_struct.ident);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The main function here uses the WalkDir iterator from the walkdir crate to iterate over all the files in the welds/examples directory and its subdirectories. It filters out directories and non-Rust files (path.extension().unwrap() == "rs"), then reads the contents of each Rust file into a String.

The syn::parse_file function is then used to parse this string into a syntax tree, which represents the structure of the Rust code in the file. This syntax tree is passed to the find_weldsmodel_structs function, which returns a Vec<ItemStruct> containing all structs in the file that derive the WeldsModel macro. ItemStruct is a syn data model that describes struct in a Rust AST, they will be used later in the project as a data source to build schema from.

Finally, the main function prints out the name of each struct found.

The find_weldsmodel_structs Function

Next, let's look at the find_weldsmodel_structs function:

fn find_weldsmodel_structs(syntax_tree: syn::File) -> Vec<ItemStruct> {
    let mut weldsmodel_structs = Vec::new();
    for item in syntax_tree.items {
        if let syn::Item::Struct(item_struct) = item {
            if has_weldsmodel_derive(&item_struct) {
                weldsmodel_structs.push(item_struct);
            }
        }
    }
    weldsmodel_structs
}
Enter fullscreen mode Exit fullscreen mode

This function iterates over all items in the syntax tree, checks if each item is a struct, and if so, checks if the struct derives the WeldsModel macro using the has_weldsmodel_derive function. If both conditions are met, the struct is added to the weldsmodel_structs vector.

The has_weldsmodel_derive Function

Finally, the has_weldsmodel_derive function:

fn has_weldsmodel_derive(item_struct: &ItemStruct) -> bool {
    let attrs = &item_struct.attrs;
    for attr in attrs {
        let meta = &attr.meta;
        if meta.path().is_ident("derive") {
            let nested = attr
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
                .unwrap();
            for nested_meta in nested {
                if nested_meta.path().is_ident("WeldsModel") {
                    return true;
                }
            }
        }
    }
    false
}
Enter fullscreen mode Exit fullscreen mode

This function checks if a given struct item has the WeldsModel derive attribute. It does this by looking at each attribute of the struct, and if the attribute's path is "derive", it parses the arguments of the attribute with parse_args_with. This gives a Punctuated list of Meta items, each representing an argument of the derive attribute. If any of these arguments' path is "WeldsModel", the function returns true, otherwise false.

TLDR

That's the overall solution: use WalkDir to iterate over all Rust files in a directory, use syn::parse_file to parse the contents of each file into a syntax tree, and then check each ATS node attributes and meta to find structures you need.

Top comments (0)