Parsing turns macro input into a structure the macro can inspect, validate, and later transform. In Rust procedural macros, the input arrives as a TokenStream: a sequence of TokenTrees rather than an already meaningful application model. A minimal macro can iterate through tokens, extract an identifier, generate Rust as a string, and parse that string back into a TokenStream, but this becomes tedious as the accepted input grows. section 3.6
For richer inputs, parse into custom structs first and generate output in a separate phase. This separation keeps syntax handling distinct from the work performed with the result; an infrastructure DSL, for example, can represent bucket and lambda declarations before any cloud resources are created. It also makes the parser a potential boundary between a high-level DSL and different backends, though provider details can still leak into the design. section 9.3
Build a practical parser incrementally:
- Model the accepted input with structs such as a top-level input containing optional Bucket and Lambda values. section 9.3.2
- Implement syn’s Parse trait at each level, letting the top-level parser delegate resource-specific syntax to the relevant struct. section 9.3.2
- Use peek to choose a parser from the next token, and stop only when the ParseStream is empty. section 9.3.2
- Define DSL-only words with custom_keyword!, then report unexpected tokens or missing values with span-aware custom errors. section 9.3.2
- Validate relationships after parsing, such as requiring a lambda when a bucket declares an event. section 9.3.2
A small infrastructure DSL
An input such as `bucket unique => lambda my_name mem 1024 time 15` can be parsed into an IacInput containing a Bucket named `unique` with events enabled and a Lambda with optional memory and timeout settings. Parsing a malformed declaration instead produces focused errors, such as `lambda needs a name` or `expected integer literal`, at the relevant token. section 9.3.2
Listing 9.5 IacInput definition and Parse implementation
#[derive(Debug)]
struct IacInput {
bucket: Option<Bucket>,
lambda: Option<Lambda>,
}
impl Parse for IacInput {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut bucket: Option<Bucket> = None;
let mut lambda = None;
loop {
if input.peek(kw::bucket) { #1
bucket = Some(input.parse()?); #2
} else if input.peek(kw::lambda) {
lambda = Some(input.parse()?);
} else if !input.is_empty() {
return Err(syn::Error::new( #3
input.lookahead1().error().span(),
"only 'bucket' and 'lambda' resources are supported")
);
} else {
break; #4
}
}
if bucket.as_ref().map(|v| v.has_event).unwrap_or(false)
&& lambda.is_none() {
return Err(syn::Error::new(
input.span(),
"a lambda is required for an event ('=>')"
))
}
Ok(
IacInput {
bucket,
lambda,
}
)
}
}
Follow the top-level Parse implementation: it dispatches on `bucket` and `lambda`, rejects leftovers, ends at empty input, and then validates the completed structure. section 9.3.2
Choose the parsing tool to fit the grammar and build constraints. Direct TokenTree handling can be sufficient when only a name is needed and can avoid heavier dependencies. `syn` provides a widely used, powerful parsing model for more complete macros, while lightweight parsers such as `venial` can expose declarations through a simpler enum for matching. section 3.6 section 5.7












