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:
- 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 namedCommandBuilder. - The
build()Method: The finalbuild()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. - The
builder()Method: We will add a staticbuilder()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 thesyn::DeriveInputstruct.let name = &ast.ident;: This is the new line you’ve added.- We access the
identfield of ourastvariable. - 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.
- We access the
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.
syn::DeriveInputDocumentation: The top-level struct representing astruct,enum, orunion. https://docs.rs/syn/1.0/syn/struct.DeriveInput.htmlsyn::IdentDocumentation: A detailed look at the identifier type, including its methods for accessing span information. https://docs.rs/syn/1.0/syn/struct.Ident.html- The Rust Programming Language: References and Borrowing: A foundational chapter on why we use
&to borrow data instead of taking ownership. https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
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:
Data::Struct(DataStruct): This variant is present when the#[derive]is on astruct. It contains asyn::DataStructvalue, which in turn holds all the information about the struct’s fields. This is the variant we care about for ourBuildermacro.Data::Enum(DataEnum): This variant is present for anenum. It contains asyn::DataEnumvalue, which provides access to the enum’s variants (e.g.,Some(T)andNoneforOption<T>).Data::Union(DataUnion): This variant is present for aunion. It contains asyn::DataUnionvalue, 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::EnumorData::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, likestruct Point { x: i32, y: i32 }. This is the case we’re interested in.Fields::Unnamed: For a tuple struct, likestruct Color(u8, u8, u8).Fields::Unit: For a unit struct, likestruct 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.
syn::DataEnum Documentation: Understand the three variants (Struct,Enum,Union). https://docs.rs/syn/1.0/syn/enum.Data.htmlsyn::DataStructStruct Documentation: See the structure that contains the fields of a struct. https://docs.rs/syn/1.0/syn/struct.DataStruct.htmlsyn::FieldsEnum Documentation: Learn about the different ways fields can be structured (Named,Unnamed,Unit). https://docs.rs/syn/1.0/syn/enum.Fields.html
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.
- The
Data::StructArm: If theast.datais asyn::Data::Struct, we’ll destructure it to gain access to thesyn::DataStructit 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. - The Catch-All
_Arm: For any other variant (Data::EnumorData::Union), we’ll use the_pattern, which acts as a catch-all. Inside this arm, we willpanic!. 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 onast.data. Note that we are movingast.datahere, which is fine since we won’t need the rest ofastafterwards. The result of the entirematchexpression (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 theStructvariant of thesyn::Dataenum.- If it matches, it destructures the enum, binding the
syn::DataStructvalue contained within the variant to a new variable, also nameddata. => data: The code to execute for this arm is simply to return the extracteddatavariable. This means that if the input was a struct, our top-leveldatavariable will now hold thesyn::DataStructinstance, 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 anenumorunion, this arm is executed. Thepanic!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:
- The Rust Book:
matchControl Flow: A comprehensive guide to usingmatch. https://doc.rust-lang.org/book/ch06-02-match.html - The Rust Book: Patterns and Matching: A deeper dive into the patterns you can use within a
matchstatement. https://doc.rust-lang.org/book/ch18-00-patterns.html syn::DataEnum Documentation: Revisit the documentation for the enum you just handled. https://docs.rs/syn/1.0/syn/enum.Data.html
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:
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(...)).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.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 thefieldsproperty of thesyn::DataStructwe extracted previously. The result of thismatchexpression—the value from the successful arm—will be assigned to a newfieldsvariable.syn::Fields::Named(fields) => fields,: This is our new “happy path”.syn::Fields::Named(fields): This pattern checks if the value is theNamedvariant of thesyn::Fieldsenum.- If it matches, it destructures the enum, binding the
syn::FieldsNamedvalue contained within to a new variable, also namedfields. => fields: The successfully extractedsyn::FieldsNamedstruct is then returned from thematchexpression.
_ => panic!("Builder can only be derived for structs with named fields"),: This catch-all arm handles bothFields::UnnamedandFields::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.
syn::DataStructDocumentation: Revisit the documentation for the struct containing thefieldsproperty. https://docs.rs/syn/latest/syn/struct.DataStruct.htmlsyn::FieldsEnum Documentation: The documentation for the enum you just matched on, detailing theNamed,Unnamed, andUnitvariants. https://docs.rs/syn/latest/syn/enum.Fields.htmlsyn::FieldsNamedStruct Documentation: The documentation for the struct you extracted, which contains thenamedcollection of fields. https://docs.rs/syn/latest/syn/struct.FieldsNamed.html
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 typeTthat are separated by punctuation of typeP.- Our specific type:
Punctuated<Field, Token![,]>means it’s a collection ofsyn::Fielditems, 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 anOption<syn::Ident>. It holds the name of the field (e.g.,executable). Since we’ve already guaranteed we are in aFields::Namedcontext, this will always beSome(Ident)for us.ty: This field is asyn::Type. It holds the AST representation of the field’s type (e.g.,StringorVec<String>).vis: The field’s visibility (e.g.,pub).attrs: AVecof 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 thenamedcollection to iterate over it. This is standard Rust practice. ThePunctuatedtype implementsIntoIterator, which allows theforloop to work seamlessly.field: On each pass through the loop, thefieldvariable will contain a reference to one of thesyn::Fieldstructs 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.
syn::FieldsNamedDocumentation: The struct containing thenamedproperty you iterated over. https://docs.rs/syn/latest/syn/struct.FieldsNamed.htmlsyn::FieldDocumentation: The detailed documentation for the struct that represents a single field. https://docs.rs/syn/latest/syn/struct.Field.html- The Rust Book:
forLoops: A refresher on howforloops and iterators work in Rust. https://doc.rust-lang.org/book/ch03-05-control-flow.html#looping-through-a-collection-with-for
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? Thesyn::Fieldstruct 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),synmodels this by making theidentproperty 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 amatchstatement to guarantee you are in asyn::Fields::Namedcontext, you can be 100% certain that everyfieldin the loop will have an identifier. TheOptionwill always be theSome(Ident)variant. This makes it safe for us to call.unwrap()to extract thesyn::Ident.
We will use field.ident.as_ref().unwrap(). Let’s break this down:
field.ident: Accesses theOption<syn::Ident>..as_ref(): This is a crucial method onOption. It converts an&Option<T>into anOption<&T>. This allows us to get a reference to theIdentinside theSomevariant without moving it, which is exactly what we need..unwrap(): Since we know theOption<&syn::Ident>isSome, 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.
syn::FieldDocumentation: The definitive guide to theFieldstruct and its properties likeidentandty. https://docs.rs/syn/latest/syn/struct.Field.html- The
OptionEnum: Official documentation forOption<T>, including theas_refmethod which is crucial for working with references. https://doc.rust-lang.org/std/option/enum.Option.html#method.as_ref syn::TypeDocumentation: An overview of theTypeenum, showing the many kinds of Rust typessyncan represent. https://docs.rs/syn/latest/syn/enum.Type.html
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:
- Once to define the fields of the
Builderstruct itself. - Again to generate the setter method for each field.
- 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:
- Parsed the raw code into a structured AST.
- Validated that the code represents a struct with named fields.
- Iterated over every field.
- Extracted the essential name and type information for each field.
- Stored this information in a clean, readable, and persistent
Veccalledcollected_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