Extract Struct Identifier in a Rust Procedural Macro

Mục tiêu: Parse the input TokenStream into an Abstract Syntax Tree (AST) using syn::parse and extract the struct’s identifier (syn::Ident) from the syn::DeriveInput struct. This name is fundamental for generating the builder struct and its methods.


Having successfully parsed the input TokenStream into the ast variable, you now possess a structured blueprint of the user’s code. This ast, a syn::DeriveInput struct, is our gateway to all the information we need to generate the builder. The first and most fundamental piece of information we must extract is the name of the struct itself.

Accessing the Struct’s Identifier

The syn::DeriveInput struct provides a direct and convenient way to access the item’s name through its ident field. This field contains a value of type syn::Ident, which is a specialized type from the syn crate designed specifically for representing Rust identifiers.

What is a syn::Ident?

While you can think of it as simply the struct’s name (e.g., Command from a struct Command { ... } definition), a syn::Ident is a more powerful and structured representation than a plain String. It’s a “syntax tree token” that retains important metadata from the original source code. The most crucial piece of this metadata is its span.

A span records the exact location (start and end position) of the identifier in the user’s source file. While we won’t use the span information directly in this task, it is the foundation for creating high-quality, targeted compile-time error messages later in the project. If we ever need to report an error related to the struct’s name, we can use the span from its Ident to make the compiler underline the correct part of the user’s code.

Why We Need the Name

Extracting the struct’s name is the cornerstone of our code generation logic. We will use this identifier in several key places:

  1. Naming the Builder: The conventional name for a builder struct is the original struct’s name with “Builder” appended. So, if the user’s struct is Command, we will generate a struct named CommandBuilder.
  2. The build() Method: The final build() method on our builder must return an instance of the original struct. We need the name to correctly define its return type: pub fn build(&self) -> Command.
  3. The builder() Method: We will add a static builder() method to the original struct as a convenient entry point. This method will need to know the name of the builder it’s creating: pub fn builder() -> CommandBuilder.

Let’s update our derive_builder function to extract this identifier and store it in a variable.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // NEW: Access the identifier of the struct from the parsed syntax tree.
    // `ast.ident` gives us a `syn::Ident` struct, which represents the name of the type
    // the derive macro is attached to. We take a reference to it.
    let name = &ast.ident;

    // This is a placeholder for the code generation logic that will follow.
    unimplemented!()
}

Code Breakdown

  • let ast = syn::parse(input).unwrap();: This is our existing code that provides the syn::DeriveInput struct.
  • let name = &ast.ident;: This is the new line you’ve added.
    • We access the ident field of our ast variable.
    • We store a reference to this identifier in a new variable called name. We use a reference (&) because we don’t need to take ownership of the identifier; we just need to read it to help generate our new code. This is an efficient and idiomatic Rust practice that avoids unnecessary data copying.

With the struct’s name now captured in our name variable, we have secured the first essential building block for our generated code. The next logical step is to dive deeper into the ast variable, specifically into its data field, to uncover the list of fields that make up the struct. This will be our focus in the upcoming tasks.

Further Reading

To better understand the data structures you are working with, you can explore their official documentation.

Accessing Struct Fields with syn::Data in Rust Macros

Mục tiêu: Learn how to navigate the syn::DeriveInput abstract syntax tree to access a struct’s fields. This involves understanding and matching on the syn::Data enum and its Data::Struct variant.


Excellent! You’ve successfully captured the name of the user’s struct in the name variable. This is a critical piece of the puzzle. Now, with the name secured, our next objective is to access the very heart of the struct: its fields. To do this, we must venture deeper into our ast variable.

Journeying into the data Field

While the ident field gave us the struct’s name, the data field of our syn::DeriveInput struct holds its body—the part between the curly braces {...}. This field is our key to understanding the structure and content of the user’s type.

The ast.data field is not a simple list of fields. Instead, its type is syn::Data, which is a powerful enum. This enum design is a core part of what makes syn so robust. It acknowledges that a #[derive] macro can be placed on different kinds of data structures.

The syn::Data enum has three variants:

  1. Data::Struct(DataStruct): This variant is present when the #[derive] is on a struct. It contains a syn::DataStruct value, which in turn holds all the information about the struct’s fields. This is the variant we care about for our Builder macro.
  2. Data::Enum(DataEnum): This variant is present for an enum. It contains a syn::DataEnum value, which provides access to the enum’s variants (e.g., Some(T) and None for Option<T>).
  3. Data::Union(DataUnion): This variant is present for a union. It contains a syn::DataUnion value, giving access to the union’s fields.

Why This Enum Matters to Us

Our Builder pattern is conceptually tied to structs, which are collections of named fields that must all be present to form a valid object. The pattern doesn’t make sense for enums (which represent a choice between different variants) or unions (which are for unsafe, low-level memory layout control).

Therefore, a well-behaved and robust macro must check what kind of data structure it is being applied to. By inspecting ast.data, we can:

  • Proceed confidently if it’s a Data::Struct.
  • Provide a clear, helpful compile-time error if it’s a Data::Enum or Data::Union, telling the user that #[derive(Builder)] can only be used on structs. (We will implement this advanced error handling later, but the structure we’re building now enables it).

A Glimpse Inside Data::Struct

When we confirm that ast.data is indeed a Data::Struct, the DataStruct value inside it will give us access to the fields of the struct. These fields themselves are represented by another enum, syn::Fields, which can be:

  • Fields::Named: For a struct with named fields, like struct Point { x: i32, y: i32 }. This is the case we’re interested in.
  • Fields::Unnamed: For a tuple struct, like struct Color(u8, u8, u8).
  • Fields::Unit: For a unit struct, like struct Marker;.

Here is a conceptual diagram of the path we will take to get to the field names and types:

ast: syn::DeriveInput {
  ...
  ident: "Command",
  ...
  // Our current focus: this `data` field
  data: syn::Data::Struct( // We need to ensure it's this variant
    syn::DataStruct {
      ...
      // Our next target: the fields within the struct data
      fields: syn::Fields::Named( // We also need to ensure it's this variant
        syn::FieldsNamed {
          named: [ // Finally, an iterable collection of `syn::Field`
            syn::Field { ident: "executable", ty: "String", ... },
            syn::Field { ident: "args", ty: "Vec<String>", ... }
          ]
        }
      )
      ...
    }
  )
}

As you can see, accessing the fields requires us to navigate through this nested structure. We must first check that data is a Struct, and then check that its fields are Named.

No code changes are needed for this specific task, as it’s about understanding the next part of our ast that we need to process. You’ve now learned what ast.data is and why it’s structured as an enum. In the very next task, you will put this knowledge into practice by writing a match statement to safely destructure ast.data and handle the Data::Struct case.

Further Reading

To prepare for the next steps, it’s highly recommended to review the documentation for these syn types.

Verify Struct Input with a match Statement in a Rust Macro

Mục tiêu: Implement a match statement to ensure the procedural macro is applied only to structs. Handle non-struct types like enums and unions by panicking with a descriptive error message to enforce correct usage.


Having successfully extracted the struct’s name and conceptually explored the ast.data field, it’s time to put that knowledge into action. Your current task is to programmatically verify that the user’s code is, in fact, a struct. Since our Builder pattern is designed exclusively for structs, attempting to apply it to an enum or union should result in a clear failure. The idiomatic and safest way to handle this in Rust is with a match statement.

The Power of match: Exhaustive Pattern Matching

In Rust, the match keyword is a control flow construct that allows you to compare a value against a series of patterns and execute code based on which pattern matches. Its true power shines when working with enums. The Rust compiler enforces exhaustiveness, meaning you must handle every possible variant of the enum. This is a powerful safety feature that eliminates an entire class of bugs common in other languages where you might forget to handle a specific case.

In our scenario, we need to handle the syn::Data enum. By using match, we can define specific logic for the Data::Struct variant (the “happy path”) and separate, explicit logic for the Data::Enum and Data::Union variants (the error paths).

Implementing the match Statement

We will now add a match statement that inspects ast.data.

  1. The Data::Struct Arm: If the ast.data is a syn::Data::Struct, we’ll destructure it to gain access to the syn::DataStruct it contains. This inner struct holds the field information we’ll need in the next tasks. For now, this arm will contain a placeholder for our future logic.
  2. The Catch-All _ Arm: For any other variant (Data::Enum or Data::Union), we’ll use the _ pattern, which acts as a catch-all. Inside this arm, we will panic!. This is consistent with our use of .unwrap() earlier—it provides a simple, immediate way to halt compilation with a clear error message. This ensures that a developer using our macro gets immediate feedback if they try to use it on an unsupported type.

Let’s update your src/lib.rs file with this new logic.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // Access the identifier of the struct.
    let name = &ast.ident;

    // NEW: Use a `match` statement to ensure we are deriving on a struct.
    // We match on `ast.data`, which is an enum `syn::Data`.
    let data = match ast.data {
        // This arm handles the `Data::Struct` variant.
        // We destructure it to get the `DataStruct` which contains the fields.
        syn::Data::Struct(data) => data,
        // The `_` arm is a catch-all for `Data::Enum` and `Data::Union`.
        // If we get anything other than a struct, we panic with a helpful message.
        _ => panic!("Builder can only be derived for structs"),
    };

    // This is a placeholder for the code generation logic that will follow.
    unimplemented!()
}

Code Breakdown

  • let data = match ast.data { ... };: We are matching on ast.data. Note that we are moving ast.data here, which is fine since we won’t need the rest of ast afterwards. The result of the entire match expression (the value from the executed arm) is assigned to a new variable, data.
  • syn::Data::Struct(data) => data,: This is our “happy path” pattern.

    • syn::Data::Struct(data): This pattern checks if the value is the Struct variant of the syn::Data enum.
    • If it matches, it destructures the enum, binding the syn::DataStruct value contained within the variant to a new variable, also named data.
    • => data: The code to execute for this arm is simply to return the extracted data variable. This means that if the input was a struct, our top-level data variable will now hold the syn::DataStruct instance, ready for further processing.
  • _ => panic!("Builder can only be derived for structs"),: This is our error-handling arm.

    • _: The underscore is a special pattern that matches any value but doesn’t bind it to a variable. It serves as our catch-all.
    • => panic!(...): If the input is an enum or union, this arm is executed. The panic! macro will halt the compilation and display our custom error message to the user.

You have now made your macro significantly more robust. It no longer blindly assumes its input is a struct; it actively verifies it and provides a clear error for invalid usage.

What’s Next?

With the syn::DataStruct now safely extracted into the data variable, our next step is to dive into it to access the fields. Just like ast.data was an enum, the fields within DataStruct are also an enum (syn::Fields). In the next task, we will perform another check to ensure these fields are named fields (e.g., struct Point { x: i32 }) and not unnamed fields from a tuple struct (e.g., struct Color(u8, u8)).

Further Reading

To solidify your understanding of these core Rust concepts, please refer to the official documentation:

Within the match arm, access the fields and ensure they are syn::Fields::Named.

Mục tiêu:


In the last task, you successfully guarded your macro against incorrect usage by ensuring it only operates on structs. You used a match statement on ast.data to extract a syn::DataStruct instance into a variable named data, and you wisely added a panic! for enums and unions. This was a crucial step in making your macro robust.

Our journey continues as we dive one level deeper. Just knowing we have a struct isn’t quite enough. Structs in Rust come in a few flavors, and our builder pattern is specifically designed for one of them. Your current task is to verify that the struct has named fields.

Understanding the Flavors of Structs

The syn::DataStruct you extracted contains a fields property of type syn::Fields. This, like syn::Data, is an enum that represents the different ways a struct’s body can be defined:

  1. Fields::Named(FieldsNamed): This is the most common type of struct, where each field has an explicit name and type. This is our target. rust // A struct with named fields struct Command { executable: String, args: Vec<String>, } The builder pattern maps perfectly to this structure, as we can generate setter methods named after each field (e.g., executable(...), args(...)).
  2. Fields::Unnamed(FieldsUnnamed): This represents a “tuple struct,” where fields are identified by their index, not by name. rust // A tuple struct with unnamed fields struct RgbColor(u8, u8, u8); While a builder could be created for this (e.g., with methods like .0(...), .1(...)), it’s a different and less common pattern that is outside the scope of our project.
  3. Fields::Unit: This represents a “unit struct,” which has no fields at all. rust // A unit struct struct Empty; A builder pattern is meaningless for a struct with no fields to build.

To create a correct and predictable builder, we must explicitly handle the Fields::Named case and reject the other two. We will use the same robust match technique you’ve just learned.

Ensuring We Have Named Fields

We will now add another match statement that inspects the data.fields enum. If it’s the Fields::Named variant, we’ll destructure it to extract the syn::FieldsNamed struct it contains. This inner struct holds the iterable collection of the actual fields. For any other variant, we will panic! with a clear error message.

Let’s modify the derive_builder function. The changes are highlighted below. We are replacing the unimplemented!() placeholder with this new logic.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // Access the identifier of the struct.
    let name = &ast.ident;

    // Ensure we are deriving on a struct.
    let data = match ast.data {
        syn::Data::Struct(data) => data,
        _ => panic!("Builder can only be derived for structs"),
    };

    // NEW: Access the fields of the struct and ensure they are named fields.
    // The `data.fields` property is of type `syn::Fields`. We match on it.
    let fields = match data.fields {
        // This is the case we want: a struct with named fields.
        // `syn::Fields::Named` contains a `syn::FieldsNamed` struct, which we
        // destructure and bind to the `fields` variable.
        syn::Fields::Named(fields) => fields,
        // For tuple structs (`Fields::Unnamed`) or unit structs (`Fields::Unit`),
        // we panic with a helpful error message.
        _ => panic!("Builder can only be derived for structs with named fields"),
    };

    // This is a placeholder for the logic that will iterate over the fields.
    unimplemented!()
}

Code Breakdown

  • let fields = match data.fields { ... };: We are inspecting the fields property of the syn::DataStruct we extracted previously. The result of this match expression—the value from the successful arm—will be assigned to a new fields variable.
  • syn::Fields::Named(fields) => fields,: This is our new “happy path”.
    • syn::Fields::Named(fields): This pattern checks if the value is the Named variant of the syn::Fields enum.
    • If it matches, it destructures the enum, binding the syn::FieldsNamed value contained within to a new variable, also named fields.
    • => fields: The successfully extracted syn::FieldsNamed struct is then returned from the match expression.
  • _ => panic!("Builder can only be derived for structs with named fields"),: This catch-all arm handles both Fields::Unnamed and Fields::Unit. As before, it halts compilation with a specific and helpful error message, preventing misuse of our macro.

You have now successfully navigated two levels deep into the Abstract Syntax Tree! You have programmatically guaranteed that you are working with a struct that has named fields, and you’ve safely extracted the syn::FieldsNamed struct which contains the list of those fields.

What’s Next?

The fields variable you just created holds the final prize. It contains an iterable collection called named which is a Punctuated<Field, Token![,]>. In the very next task, we will iterate over this collection to finally access each individual syn::Field, from which we can extract its name (ident) and type (ty).

Further Reading

To understand the structures you’ve just worked with, explore their official documentation.

Iterate over the fields.named collection.

Mục tiêu:


You’ve done an excellent job of systematically drilling down into the Abstract Syntax Tree (AST). Through a series of robust checks, you have successfully navigated from the top-level syn::DeriveInput all the way down to a syn::FieldsNamed struct, which you’ve stored in the fields variable. You’ve programmatically confirmed you’re working with a struct that has named fields. The final prize—the list of individual fields—is now within your grasp.

Unlocking the Collection: The fields.named Property

The syn::FieldsNamed struct you’ve isolated acts as a container. Its most important property is a field named named. This property holds the collection of all the fields defined in the user’s struct.

The type of fields.named is Punctuated<Field, Token![,]>. This might look intimidating, but you can think of it as a specialized Vec provided by the syn crate.

  • Punctuated<T, P>: This is a generic type representing a sequence of items of type T that are separated by punctuation of type P.
  • Our specific type: Punctuated<Field, Token![,]> means it’s a collection of syn::Field items, separated by commas (,).

The most important thing to know about Punctuated is that it’s designed to be easily iterable. You can loop over it with a standard for loop, just as you would with a Vec or a slice.

The syn::Field Struct: A Blueprint of a Single Field

Each item you get during the iteration will be of type syn::Field. This struct is the AST’s representation of a single line within a struct definition, like executable: String. It’s the lowest-level piece of information we need, and it contains everything we care about for a single field:

  • ident: This field is an Option<syn::Ident>. It holds the name of the field (e.g., executable). Since we’ve already guaranteed we are in a Fields::Named context, this will always be Some(Ident) for us.
  • ty: This field is a syn::Type. It holds the AST representation of the field’s type (e.g., String or Vec<String>).
  • vis: The field’s visibility (e.g., pub).
  • attrs: A Vec of any attributes on the field, like #[serde(rename = "...")].

Implementing the Field Iteration

Now, let’s put this knowledge into practice. We will replace the final unimplemented!() placeholder with a for loop that iterates over the fields.named collection. For now, the body of the loop will be empty, as our only goal in this task is to set up the iteration itself.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // Access the identifier of the struct.
    let name = &ast.ident;

    // Ensure we are deriving on a struct.
    let data = match ast.data {
        syn::Data::Struct(data) => data,
        _ => panic!("Builder can only be derived for structs"),
    };

    // Access the fields of the struct and ensure they are named fields.
    let fields = match data.fields {
        syn::Fields::Named(fields) => fields,
        _ => panic!("Builder can only be derived for structs with named fields"),
    };

    // NEW: Iterate over the named fields of the struct.
    for field in &fields.named {
        // In the next tasks, we will extract the field name and type here.
    }

    // This is a placeholder for the final code generation step.
    unimplemented!();
}

Code Breakdown

  • for field in &fields.named { ... }: This is the new block of code you’ve added.
    • &fields.named: We are borrowing the named collection to iterate over it. This is standard Rust practice. The Punctuated type implements IntoIterator, which allows the for loop to work seamlessly.
    • field: On each pass through the loop, the field variable will contain a reference to one of the syn::Field structs from the collection (so its type is &syn::Field).

You have now successfully set up the loop that will be the engine of our code generation. This loop will allow us to act on each field of the user’s struct one by one.

What’s Next?

With the iteration structure in place, the next logical task is to do something useful inside that loop. In the upcoming tasks, you will reach into the field variable on each iteration to extract two crucial pieces of information: the field’s name (field.ident) and its type (field.ty). Once you’ve collected this information for all fields, you’ll have everything you need to start generating the builder struct.

Further Reading

To learn more about the types and concepts you’ve just used, these resources are highly recommended.

Extract Field Name and Type in a Rust Proc-Macro

Mục tiêu: Iterate over the named fields of a parsed struct’s abstract syntax tree (AST) using the syn crate and extract the name (ident) and type (ty) of each field.


You’ve masterfully navigated the abstract syntax tree, using match statements to guarantee you’re working with a struct that has named fields. The for field in &fields.named loop you set up in the previous task is the perfect foundation for our next step. That loop is our data processing engine; now it’s time to put it to work.

Your current task is to look inside each syn::Field as you iterate and extract the two most vital pieces of information we need to generate our builder: the field’s name and its type.

Deconstructing syn::Field

On each iteration of your loop, the field variable (of type &syn::Field) is a treasure trove of information about a single field from the user’s struct. Let’s inspect the two properties we’re interested in: ident and ty.

Extracting the Field Name: field.ident

The ident property of a syn::Field holds its name. If you look at the syn documentation, you’ll see its type is not syn::Ident, but Option<syn::Ident>.

  • Why an Option? The syn::Field struct is a general representation that must also be able to describe fields in tuple structs (e.g., struct Color(u8, u8, u8);). Since the fields in a tuple struct don’t have names (identifiers), syn models this by making the ident property optional.
  • Why is it safe for us to unwrap? This is a perfect example of how our previous validation steps pay off. Because you have already used a match statement to guarantee you are in a syn::Fields::Named context, you can be 100% certain that every field in the loop will have an identifier. The Option will always be the Some(Ident) variant. This makes it safe for us to call .unwrap() to extract the syn::Ident.

We will use field.ident.as_ref().unwrap(). Let’s break this down:

  1. field.ident: Accesses the Option<syn::Ident>.
  2. .as_ref(): This is a crucial method on Option. It converts an &Option<T> into an Option<&T>. This allows us to get a reference to the Ident inside the Some variant without moving it, which is exactly what we need.
  3. .unwrap(): Since we know the Option<&syn::Ident> is Some, we can safely unwrap it to get the &syn::Ident.

Extracting the Field Type: field.ty

Accessing the field’s type is more straightforward. The ty property of a syn::Field is of type syn::Type. This is another complex syn enum that can represent any valid Rust type, from a simple String to a complex generic type like HashMap<K, Vec<Option<T>>>.

For our purposes, we just need a reference to this syn::Type object. The quote! macro will know how to convert this AST node back into the tokens representing the type when we generate our code. So, all we need to do is let field_type = &field.ty;.

Implementing the Extraction

Let’s update your src/lib.rs by adding the extraction logic inside the for loop you created.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // Access the identifier of the struct.
    let name = &ast.ident;

    // Ensure we are deriving on a struct.
    let data = match ast.data {
        syn::Data::Struct(data) => data,
        _ => panic!("Builder can only be derived for structs"),
    };

    // Access the fields of the struct and ensure they are named fields.
    let fields = match data.fields {
        syn::Fields::Named(fields) => fields,
        _ => panic!("Builder can only be derived for structs with named fields"),
    };

    // Iterate over the named fields of the struct.
    for field in &fields.named {
        // NEW: Extract the field name (identifier).
        // `field.ident` is an `Option<syn::Ident>` because fields in tuple structs
        // don't have names. Since we've already confirmed we're in a struct with
        // named fields, we can safely `unwrap()` it. We use `as_ref()` to get a
        // reference to the `Ident` without taking ownership.
        let field_name = field.ident.as_ref().unwrap();

        // NEW: Extract the field type.
        // `field.ty` is a `syn::Type`, which is an AST node representing the type.
        let field_type = &field.ty;

        // NOTE: In this task, we are only extracting the variables. In the next task,
        // we will store `field_name` and `field_type` in a collection to use later.
    }

    // This is a placeholder for the final code generation step.
    unimplemented!();
}

You have now successfully extracted the name and type for each field of the user’s struct. Inside the loop, you have field_name (a &syn::Ident) and field_type (a &syn::Type) available for each field. These are the fundamental ingredients we need to generate the fields of our builder struct and the signatures of its setter methods.

What’s Next?

The variables field_name and field_type are currently local to the for loop, so they disappear after each iteration. This isn’t very useful! In the final task of this step, you will create a Vec before the loop and, inside the loop, push the extracted name and type into it. This will give you a persistent collection of all field information that you can use later in the code generation phase.

Further Reading

To learn more about the types and concepts you’ve just worked with, these resources are highly recommended.

Store the collected field names and types in a Vec of a temporary struct for easy access later.

Mục tiêu:


Excellent work! You have successfully created a loop that iterates through each field of the user’s struct and, within that loop, you’ve extracted the field’s name and type. This is a major milestone. However, as you’ve likely noticed, the field_name and field_type variables are created anew on each iteration and are lost as soon as the next iteration begins.

Our current task is to fix this by creating a persistent collection before the loop, and then storing the information for each field in that collection. This will give us a complete dataset of the struct’s anatomy, which we can then use in the subsequent code-generation steps.

The Need for a Data Container

To generate our builder, we will need to iterate over the struct’s fields multiple times:

  1. Once to define the fields of the Builder struct itself.
  2. Again to generate the setter method for each field.
  3. A third time to implement the logic inside the final build() method.

Parsing the AST is a relatively expensive operation. It’s far more efficient and cleaner to perform the parsing and data extraction just once, store the results in a convenient format, and then work with that collected data. A Vec (a growable list) is the perfect tool for this job.

Designing a Better Container: A Temporary Struct

We could store our data in a Vec of tuples, like Vec<(&syn::Ident, &syn::Type)>. This would work, but it can make the code harder to read. When you access an element later, you’d have to use field.0 and field.1, which isn’t very descriptive.

A much better practice is to define a small, temporary helper struct whose sole purpose is to give meaningful names to our collected data. This makes our code more self-documenting and maintainable. We will create a struct named FieldInfo to hold the name and type of a field.

A Quick Dive into Lifetimes

Since we are storing references (&syn::Ident and &syn::Type) in our new struct, we must tell the Rust compiler how long those references are valid for. We do this using a lifetime parameter, denoted with an apostrophe (e.g., 'a).

We will define our struct as struct FieldInfo<'a>. This syntax tells the compiler: “I’m defining a struct called FieldInfo. It is generic over a single lifetime, 'a. This means that any references inside this struct must be valid for at least as long as the duration represented by 'a.”

By doing this, we are making a promise to the compiler that the FieldInfo instances will not outlive the ast data from which we are borrowing the ident and ty. Since our Vec and the ast both exist only within the derive_builder function, this promise is easy to keep, and the compiler will be satisfied.

Implementing the Collection Logic

Let’s modify src/lib.rs to include our new FieldInfo struct, initialize the Vec, and populate it inside the loop.

use proc_macro::TokenStream;
use quote::quote;
use syn;

// NEW: Define a helper struct to store the field information we need.
// The lifetime parameter `'a` indicates that this struct contains references
// that are valid for the lifetime 'a. This is necessary because we are borrowing
// the field name (`&'a syn::Ident`) and type (`&'a syn::Type`) from the input AST.
struct FieldInfo<'a> {
    name: &'a syn::Ident,
    ty: &'a syn::Type,
}

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree.
    let ast = syn::parse(input).unwrap();

    // Access the identifier of the struct.
    let name = &ast.ident;

    // Ensure we are deriving on a struct.
    let data = match ast.data {
        syn::Data::Struct(data) => data,
        _ => panic!("Builder can only be derived for structs"),
    };

    // Access the fields of the struct and ensure they are named fields.
    let fields = match data.fields {
        syn::Fields::Named(fields) => fields,
        _ => panic!("Builder can only be derived for structs with named fields"),
    };

    // NEW: Initialize a vector to store our collected field information.
    // We make it mutable (`mut`) so we can push items into it inside the loop.
    let mut collected_fields = Vec::new();

    // Iterate over the named fields of the struct.
    for field in &fields.named {
        let field_name = field.ident.as_ref().unwrap();
        let field_type = &field.ty;

        // NEW: Create an instance of our helper struct and push it into the vector.
        collected_fields.push(FieldInfo {
            name: field_name,
            ty: field_type,
        });
    }

    // This is a placeholder for the final code generation step.
    unimplemented!();
}

Note: It’s perfectly idiomatic in Rust to define helper structs like FieldInfo outside the main function but within the same module if they are specific to that module’s logic.

What You’ve Accomplished

Congratulations! You have now completed the entire “Extracting Structural Information” step. You have successfully:

  1. Parsed the raw code into a structured AST.
  2. Validated that the code represents a struct with named fields.
  3. Iterated over every field.
  4. Extracted the essential name and type information for each field.
  5. Stored this information in a clean, readable, and persistent Vec called collected_fields.

You have now transformed the user’s source code into a structured dataset. All the analysis is done. The rest of the project is about using this dataset to generate new code.

What’s Next?

You are now perfectly positioned to start the next major step: “Generating the Builder Struct Definition”. With your collected_fields vector ready, you will use the powerful quote! macro to begin constructing the token stream for the new CommandBuilder struct. You’ll iterate through your vector and generate a field inside the builder for each field from the original struct.

Further Reading

This task introduced the important concept of lifetimes. Understanding them is key to mastering Rust. * The Rust Programming Language: Validating References with Lifetimes: The official book chapter explaining what lifetimes are and why they are necessary. https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html * The Vec<T> Collection: A detailed guide on using vectors in Rust. https://doc.rust-lang.org/book/ch08-01-vectors.html * The Rust Programming Language: Defining Structs: A refresher on the syntax and usage of structs. https://doc.rust-lang.org/book/ch05-01-defining-structs.html