Rust

Macros Procedurais — Criando Derive Macros e Atributos Customizados Já leu

17 min de leitura

Macros Procedurais — Criando Derive Macros e Atributos Customizados
No Artigo #21 exploramos macros declarativas com macro_rules! — padrões que transformam código em código. Macros procedurais são o próximo nível: programas Rust que recebem tokens como entrada e produzem tokens como saíd

 

No Artigo #21 exploramos macros declarativas com macro_rules! — padrões que transformam código em código. Macros procedurais são o próximo nível: programas Rust que recebem tokens como entrada e produzem tokens como saída, executando em tempo de compilação.

Quando você escreve #[derive(Serialize)] ou #[tokio::main], está usando macros procedurais. Elas permitem eliminar boilerplate que seria impossível com macro_rules!, gerar código com base na estrutura de tipos, e criar DSLs (Domain Specific Languages) que parecem parte natural da linguagem.


Os três tipos de macros procedurais

// 1. Derive macros — adicionam implementações a structs/enums
#[derive(MinhaDerive)]
struct MeuTipo { ... }

// 2. Attribute macros — transformam o item anotado
#[meu_atributo(opcao = "valor")]
fn minha_funcao() { ... }

// 3. Function-like macros — chamadas como funções mas com poder de macro
minha_macro!(conteudo arbitrario aqui)

Estrutura de um crate de macros procedurais

Macros procedurais precisam ficar em um crate separado com proc-macro = true:

meu_projeto/
├── Cargo.toml
├── src/
│   └── main.rs
└── meu_derive/           ← crate de macros procedurais
    ├── Cargo.toml
    └── src/
        └── lib.rs

meu_derive/Cargo.toml:

[package]
name = "meu_derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true  # obrigatório

[dependencies]
proc-macro2 = "1"  # versão melhorada de proc_macro
quote = "1"        # geração de código TokenStream
syn = { version = "2", features = ["full"] }  # parsing de AST Rust

Cargo.toml do projeto principal:

[dependencies]
meu_derive = { path = "./meu_derive" }

As ferramentas essenciais

Três crates formam o ecossistema de macros procedurais:

syn — parseia código Rust em uma AST (Abstract Syntax Tree). Transforma TokenStream em estruturas como DeriveInput, ItemFn, Type, que você pode inspecionar e transformar.

quote — gera código Rust a partir de templates. O macro quote! { ... } interpola variáveis Rust em código Rust, retornando um TokenStream.

proc-macro2 — versão melhorada do proc_macro da stdlib, com suporte a testes unitários e melhor ergonomia.


Derive macro: Descrever

Vamos criar uma derive macro que implementa automaticamente um método descrever() para qualquer struct:

meu_derive/src/lib.rs:

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, FieldsNamed};

#[proc_macro_derive(Descrever)]
pub fn derive_descrever(input: TokenStream) -> TokenStream {
    // Parseia o input como DeriveInput (struct/enum/union)
    let input = parse_macro_input!(input as DeriveInput);

    // Gera o código ou retorna erro
    match impl_descrever(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_descrever(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;

    // Obtém os campos da struct
    let campos = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(campos) => campos,
            _ => return Err(syn::Error::new_spanned(
                nome,
                "Descrever suporta apenas structs com campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome,
            "Descrever suporta apenas structs"
        )),
    };

    // Gera linhas de descrição para cada campo
    let linhas_campos = campos.named.iter().map(|campo| {
        let nome_campo = campo.ident.as_ref().unwrap();
        let nome_str = nome_campo.to_string();
        quote! {
            // Para cada campo, chama format! com seu valor
            partes.push(format!("  {}: {:?}", #nome_str, self.#nome_campo));
        }
    });

    let nome_str = nome.to_string();

    // Gera a implementação completa
    let codigo_gerado = quote! {
        impl #nome {
            pub fn descrever(&self) -> String {
                let mut partes = Vec::new();
                partes.push(format!("{}:", #nome_str));
                #(#linhas_campos)*
                partes.join("
")
            }
        }
    };

    Ok(codigo_gerado)
}

Uso:

use meu_derive::Descrever;

#[derive(Descrever, Debug)]
struct Usuario {
    nome: String,
    email: String,
    idade: u32,
    ativo: bool,
}

#[derive(Descrever, Debug)]
struct Produto {
    id: u64,
    nome: String,
    preco: f64,
    estoque: i32,
}

fn main() {
    let usuario = Usuario {
        nome: "Ana Silva".to_string(),
        email: "ana@exemplo.com".to_string(),
        idade: 30,
        ativo: true,
    };

    println!("{}", usuario.descrever());
    // Usuario:
    //   nome: "Ana Silva"
    //   email: "ana@exemplo.com"
    //   idade: 30
    //   ativo: true

    let produto = Produto {
        id: 1,
        nome: "Teclado Mecânico".to_string(),
        preco: 350.99,
        estoque: 15,
    };

    println!("{}", produto.descrever());
}

Derive macro com atributos de campo

Vamos criar uma derive macro Builder que gera um builder pattern automaticamente, com suporte a atributos de configuração:

meu_derive/src/lib.rs (adição):

use syn::{Attribute, Lit, Meta, NameValue};

// Estrutura para guardar configuração de cada campo
struct ConfigCampo {
    nome: syn::Ident,
    tipo: syn::Type,
    obrigatorio: bool,
    valor_padrao: Option<proc_macro2::TokenStream>,
}

fn extrair_config_campo(campo: &syn::Field) -> ConfigCampo {
    let nome = campo.ident.clone().unwrap();
    let tipo = campo.ty.clone();
    let mut obrigatorio = true;
    let mut valor_padrao = None;

    // Procura atributo #[builder(padrao = "...")]
    for attr in &campo.attrs {
        if attr.path().is_ident("builder") {
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("padrao") {
                    let valor: syn::LitStr = meta.value()?.parse()?;
                    valor_padrao = Some(valor.value().parse().unwrap());
                    obrigatorio = false;
                }
                if meta.path.is_ident("opcional") {
                    obrigatorio = false;
                }
                Ok(())
            });
        }
    }

    ConfigCampo { nome, tipo, obrigatorio, valor_padrao }
}

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match impl_builder(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_builder(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;
    let nome_builder = syn::Ident::new(
        &format!("{nome}Builder"),
        nome.span(),
    );

    let campos = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(f) => &f.named,
            _ => return Err(syn::Error::new_spanned(
                nome, "Builder requer campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome, "Builder requer struct"
        )),
    };

    let configs: Vec<ConfigCampo> = campos.iter()
        .map(extrair_config_campo)
        .collect();

    // Campos do Builder (todos Option<T>)
    let campos_builder = configs.iter().map(|c| {
        let nome = &c.nome;
        let tipo = &c.tipo;
        quote! { #nome: Option<#tipo> }
    });

    // Inicialização do Builder
    let init_campos = configs.iter().map(|c| {
        let nome = &c.nome;
        if let Some(ref padrao) = c.valor_padrao {
            quote! { #nome: Some(#padrao) }
        } else {
            quote! { #nome: None }
        }
    });

    // Métodos setter
    let setters = configs.iter().map(|c| {
        let nome = &c.nome;
        let tipo = &c.tipo;
        quote! {
            pub fn #nome(mut self, valor: #tipo) -> Self {
                self.#nome = Some(valor);
                self
            }
        }
    });

    // Método build() com validação
    let validacoes = configs.iter()
        .filter(|c| c.obrigatorio)
        .map(|c| {
            let nome = &c.nome;
            let nome_str = nome.to_string();
            quote! {
                let #nome = self.#nome.ok_or_else(||
                    format!("Campo obrigatório ausente: {}", #nome_str)
                )?;
            }
        });

    let campos_obrigatorios: Vec<_> = configs.iter()
        .filter(|c| c.obrigatorio)
        .map(|c| &c.nome)
        .collect();

    let campos_opcionais: Vec<_> = configs.iter()
        .filter(|c| !c.obrigatorio)
        .map(|c| &c.nome)
        .collect();

    let construcao_opcionais = configs.iter()
        .filter(|c| !c.obrigatorio)
        .map(|c| {
            let nome = &c.nome;
            if let Some(ref padrao) = c.valor_padrao {
                quote! { #nome: self.#nome.unwrap_or(#padrao) }
            } else {
                quote! { #nome: self.#nome.unwrap_or_default() }
            }
        });

    let codigo = quote! {
        pub struct #nome_builder {
            #(#campos_builder,)*
        }

        impl #nome_builder {
            pub fn novo() -> Self {
                #nome_builder {
                    #(#init_campos,)*
                }
            }

            #(#setters)*

            pub fn build(self) -> Result<#nome, String> {
                #(#validacoes)*

                Ok(#nome {
                    #(#campos_obrigatorios,)*
                    #(#construcao_opcionais,)*
                })
            }
        }

        impl #nome {
            pub fn builder() -> #nome_builder {
                #nome_builder::novo()
            }
        }
    };

    Ok(codigo)
}

Uso:

use meu_derive::Builder;

#[derive(Builder, Debug)]
struct ConfiguracaoServidor {
    // Campos obrigatórios
    host: String,
    porta: u16,

    // Campos com valores padrão
    #[builder(padrao = "10")]
    max_conexoes: usize,

    #[builder(padrao = "30")]
    timeout_segundos: u64,

    #[builder(padrao = "false")]
    debug: bool,
}

fn main() {
    // Uso fluente do builder gerado
    let config = ConfiguracaoServidor::builder()
        .host("0.0.0.0".to_string())
        .porta(8080)
        .max_conexoes(100)
        .debug(true)
        .build()
        .unwrap();

    println!("{:?}", config);

    // Sem campos opcionais — usa os padrões
    let config_minima = ConfiguracaoServidor::builder()
        .host("localhost".to_string())
        .porta(3000)
        .build()
        .unwrap();

    println!("{:?}", config_minima);

    // Erro — campo obrigatório ausente
    let resultado = ConfiguracaoServidor::builder()
        .porta(8080)
        .build();

    println!("Sem host: {:?}", resultado);
    // Err("Campo obrigatório ausente: host")
}

Attribute macro: cronometrar

Attribute macros transformam o item que anotam. Vamos criar #[cronometrar] que envolve qualquer função com medição de tempo:

meu_derive/src/lib.rs (adição):

use syn::{ItemFn, parse_macro_input};

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

    match impl_cronometrar(args.into(), funcao) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_cronometrar(
    _args: TokenStream2,
    funcao: ItemFn,
) -> syn::Result<TokenStream2> {
    let nome = &funcao.sig.ident;
    let nome_str = nome.to_string();
    let visibilidade = &funcao.vis;
    let assinatura = &funcao.sig;
    let corpo = &funcao.block;
    let atributos = &funcao.attrs;

    // Verifica se é async
    let e_async = funcao.sig.asyncness.is_some();

    let codigo_cronometrado = if e_async {
        quote! {
            #(#atributos)*
            #visibilidade #assinatura {
                let __inicio = ::std::time::Instant::now();
                let __resultado = async move #corpo .await;
                let __duracao = __inicio.elapsed();
                println!(
                    "[cronometrar] {} concluída em {:?}",
                    #nome_str,
                    __duracao
                );
                __resultado
            }
        }
    } else {
        quote! {
            #(#atributos)*
            #visibilidade #assinatura {
                let __inicio = ::std::time::Instant::now();
                let __resultado = (|| #corpo)();
                let __duracao = __inicio.elapsed();
                println!(
                    "[cronometrar] {} concluída em {:?}",
                    #nome_str,
                    __duracao
                );
                __resultado
            }
        }
    };

    Ok(codigo_cronometrado)
}

Uso:

use meu_derive::cronometrar;

#[cronometrar]
fn calcular_primos(limite: u64) -> Vec<u64> {
    (2..=limite)
        .filter(|&n| (2..=(n as f64).sqrt() as u64)
            .all(|i| n % i != 0))
        .collect()
}

#[cronometrar]
fn ordenar_grande(mut dados: Vec<i32>) -> Vec<i32> {
    dados.sort_unstable();
    dados
}

// Também funciona com funções async
#[cronometrar]
async fn buscar_dados() -> String {
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    "dados retornados".to_string()
}

fn main() {
    let primos = calcular_primos(100_000);
    println!("Encontrados {} primos", primos.len());
    // [cronometrar] calcular_primos concluída em 15.234ms

    let dados: Vec<i32> = (0..100_000).rev().collect();
    let ordenado = ordenar_grande(dados);
    println!("Ordenados {} elementos", ordenado.len());
    // [cronometrar] ordenar_grande concluída em 4.891ms
}

Function-like macro: sql!

Macros function-like têm a mesma sintaxe de chamada que macros declarativas mas com o poder das procedurais:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let tokens: Vec<_> = input.into_iter().collect();

    // Extrai o SQL como string literal
    let sql_str = match tokens.first() {
        Some(proc_macro::TokenTree::Literal(lit)) => {
            let s = lit.to_string();
            // Remove aspas
            s.trim_matches('"').to_string()
        }
        _ => {
            return syn::Error::new(
                proc_macro2::Span::call_site(),
                "sql! espera uma string literal"
            )
            .to_compile_error()
            .into()
        }
    };

    // Validações em tempo de compilação
    let sql_upper = sql_str.to_uppercase();

    // Verifica palavras-chave perigosas
    let avisos: Vec<&str> = [
        ("DROP TABLE", "Operação destrutiva: DROP TABLE detectada"),
        ("DELETE FROM", "DELETE sem WHERE pode remover todos os dados"),
        ("TRUNCATE", "TRUNCATE remove todos os dados da tabela"),
    ]
    .iter()
    .filter(|(palavra, _)| sql_upper.contains(palavra))
    .map(|(_, aviso)| *aviso)
    .collect();

    // Contagem de parâmetros ($1, $2, ...)
    let mut num_params = 0usize;
    let mut i = 1;
    while sql_str.contains(&format!("${i}")) {
        num_params = i;
        i += 1;
    }

    let avisos_code = avisos.iter().map(|aviso| {
        quote! { compile_error!(#aviso); }
    });

    let codigo = quote! {
        {
            #(#avisos_code)*

            struct ConsultaValidada {
                sql: &'static str,
                num_parametros: usize,
            }

            ConsultaValidada {
                sql: #sql_str,
                num_parametros: #num_params,
            }
        }
    };

    codigo.into()
}

Uso:

use meu_derive::sql;

fn main() {
    let consulta = sql!("SELECT id, nome FROM usuarios WHERE id = $1");
    println!("SQL: {}", consulta.sql);
    println!("Parâmetros: {}", consulta.num_parametros);

    // Erro em tempo de compilação:
    // let perigosa = sql!("DROP TABLE usuarios");
    // error: Operação destrutiva: DROP TABLE detectada
}

Testando macros procedurais

# Em meu_derive/Cargo.toml
[dev-dependencies]
trybuild = "1"         # testa erros de compilação
prettyplease = "0.2"  # formata código gerado para legibilidade
// meu_derive/tests/testes_builder.rs
#[test]
fn builder_campos_obrigatorios() {
    // trybuild verifica que o código compila/falha como esperado
    let t = trybuild::TestCases::new();
    t.pass("tests/casos/builder_basico.rs");
    t.compile_fail("tests/casos/builder_sem_obrigatorio.rs");
}

// tests/casos/builder_basico.rs
use meu_derive::Builder;

#[derive(Builder, Debug)]
struct Ponto {
    x: f64,
    y: f64,
}

fn main() {
    let p = Ponto::builder()
        .x(1.0)
        .y(2.0)
        .build()
        .unwrap();

    assert_eq!(p.x, 1.0);
    assert_eq!(p.y, 2.0);
}

// tests/casos/builder_sem_obrigatorio.rs
use meu_derive::Builder;

#[derive(Builder, Debug)]
struct Ponto {
    x: f64,
    y: f64,
}

fn main() {
    // Deve resultar em Err em tempo de execução, não compilação
    // mas o tipo do erro deve ser verificável
    let resultado = Ponto::builder().x(1.0).build();
    assert!(resultado.is_err());
}

Inspecionar código gerado com cargo expand

cargo install cargo-expand

# Expande todas as macros e mostra o código resultante
cargo expand

# Expande apenas um módulo específico
cargo expand meu_modulo

Saída para #[derive(Builder)] struct Config { ... }:

// Código gerado e expandido:
pub struct ConfigBuilder {
    host: Option<String>,
    porta: Option<u16>,
    max_conexoes: Option<usize>,
    // ...
}

impl ConfigBuilder {
    pub fn novo() -> Self {
        ConfigBuilder {
            host: None,
            porta: None,
            max_conexoes: Some(10usize),
            // ...
        }
    }

    pub fn host(mut self, valor: String) -> Self {
        self.host = Some(valor);
        self
    }
    // ...

    pub fn build(self) -> Result<Config, String> {
        let host = self.host.ok_or_else(|| {
            format!("Campo obrigatório ausente: {}", "host")
        })?;
        // ...
    }
}

Um exemplo completo: derive Validar

Vamos criar uma macro que gera validação automática baseada em atributos de campo:

// meu_derive/src/lib.rs — macro Validar

#[proc_macro_derive(Validar, attributes(validar))]
pub fn derive_validar(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match impl_validar(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

struct RegraValidacao {
    campo: syn::Ident,
    tipo: syn::Type,
    regras: Vec<Regra>,
}

enum Regra {
    NaoVazio,
    MinLen(usize),
    MaxLen(usize),
    Min(f64),
    Max(f64),
    Email,
    Regex(String),
}

fn extrair_regras(campo: &syn::Field) -> RegraValidacao {
    let nome = campo.ident.clone().unwrap();
    let tipo = campo.ty.clone();
    let mut regras = Vec::new();

    for attr in &campo.attrs {
        if !attr.path().is_ident("validar") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("nao_vazio") {
                regras.push(Regra::NaoVazio);
            } else if meta.path.is_ident("email") {
                regras.push(Regra::Email);
            } else if meta.path.is_ident("min_len") {
                let valor: syn::LitInt = meta.value()?.parse()?;
                regras.push(Regra::MinLen(valor.base10_parse()?));
            } else if meta.path.is_ident("max_len") {
                let valor: syn::LitInt = meta.value()?.parse()?;
                regras.push(Regra::MaxLen(valor.base10_parse()?));
            } else if meta.path.is_ident("min") {
                let valor: syn::LitFloat = meta.value()?.parse()?;
                regras.push(Regra::Min(valor.base10_parse()?));
            } else if meta.path.is_ident("max") {
                let valor: syn::LitFloat = meta.value()?.parse()?;
                regras.push(Regra::Max(valor.base10_parse()?));
            }
            Ok(())
        });
    }

    RegraValidacao { campo: nome, tipo, regras }
}

fn gerar_verificacao(regra: &RegraValidacao) -> TokenStream2 {
    let campo = &regra.campo;
    let campo_str = campo.to_string();

    let verificacoes = regra.regras.iter().map(|r| match r {
        Regra::NaoVazio => quote! {
            if self.#campo.is_empty() {
                erros.push(format!("{}: não pode ser vazio", #campo_str));
            }
        },
        Regra::MinLen(n) => quote! {
            if self.#campo.len() < #n {
                erros.push(format!(
                    "{}: mínimo {} caracteres (tem {})",
                    #campo_str, #n, self.#campo.len()
                ));
            }
        },
        Regra::MaxLen(n) => quote! {
            if self.#campo.len() > #n {
                erros.push(format!(
                    "{}: máximo {} caracteres (tem {})",
                    #campo_str, #n, self.#campo.len()
                ));
            }
        },
        Regra::Min(n) => quote! {
            if (self.#campo as f64) < #n {
                erros.push(format!(
                    "{}: valor mínimo é {} (tem {})",
                    #campo_str, #n, self.#campo
                ));
            }
        },
        Regra::Max(n) => quote! {
            if (self.#campo as f64) > #n {
                erros.push(format!(
                    "{}: valor máximo é {} (tem {})",
                    #campo_str, #n, self.#campo
                ));
            }
        },
        Regra::Email => quote! {
            if !self.#campo.contains('@') || !self.#campo.contains('.') {
                erros.push(format!("{}: email inválido", #campo_str));
            }
        },
        Regra::Regex(_) => quote! {},
    });

    quote! { #(#verificacoes)* }
}

fn impl_validar(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;

    let campos = match &input.data {
        Data::Struct(d) => match &d.fields {
            Fields::Named(f) => &f.named,
            _ => return Err(syn::Error::new_spanned(
                nome, "Validar requer campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome, "Validar requer struct"
        )),
    };

    let regras: Vec<_> = campos.iter()
        .map(extrair_regras)
        .collect();

    let verificacoes = regras.iter().map(gerar_verificacao);

    let codigo = quote! {
        impl #nome {
            pub fn validar(&self) -> Result<(), Vec<String>> {
                let mut erros: Vec<String> = Vec::new();

                #(#verificacoes)*

                if erros.is_empty() {
                    Ok(())
                } else {
                    Err(erros)
                }
            }

            pub fn e_valido(&self) -> bool {
                self.validar().is_ok()
            }
        }
    };

    Ok(codigo)
}

Uso:

use meu_derive::Validar;

#[derive(Validar, Debug)]
struct FormularioCadastro {
    #[validar(nao_vazio, min_len = 2, max_len = 100)]
    nome: String,

    #[validar(nao_vazio, email)]
    email: String,

    #[validar(min_len = 8, max_len = 72)]
    senha: String,

    #[validar(min = 18.0, max = 120.0)]
    idade: u32,
}

fn main() {
    let formulario_valido = FormularioCadastro {
        nome: "Ana Silva".to_string(),
        email: "ana@exemplo.com".to_string(),
        senha: "MinhaS3nh@".to_string(),
        idade: 30,
    };

    match formulario_valido.validar() {
        Ok(())    => println!("✓ Formulário válido"),
        Err(erros) => println!("✗ Erros: {:?}", erros),
    }

    let formulario_invalido = FormularioCadastro {
        nome: "A".to_string(),                    // muito curto
        email: "email_invalido".to_string(),       // sem @
        senha: "123".to_string(),                  // muito curta
        idade: 15,                                 // menor de 18
    };

    match formulario_invalido.validar() {
        Ok(())     => println!("✓ Válido"),
        Err(erros) => {
            println!("✗ {} erro(s) encontrado(s):", erros.len());
            for erro in &erros {
                println!("  • {erro}");
            }
        }
    }
    // ✗ 4 erro(s) encontrado(s):
    //   • nome: mínimo 2 caracteres (tem 1)
    //   • email: email inválido
    //   • senha: mínimo 8 caracteres (tem 3)
    //   • idade: valor mínimo é 18 (tem 15)
}

Mensagens de erro de qualidade

Macros procedurais bem feitas produzem erros compreensíveis:

// Erro com span preciso — aponta para o local exato do problema
fn verificar_tipo_compativel(
    campo: &syn::Field
) -> syn::Result<()> {
    let tipo = &campo.ty;

    // Verifica se o tipo é String ou &str para validar .is_empty()
    let tipo_str = quote!(#tipo).to_string();
    if !["String", "str"].iter().any(|t| tipo_str.contains(t)) {
        return Err(syn::Error::new_spanned(
            tipo,
            format!(
                "Validar(nao_vazio) requer tipo String ou &str, mas encontrou: {}",
                tipo_str
            )
        ));
    }

    Ok(())
}

// O erro aparece assim no compilador:
// error: Validar(nao_vazio) requer tipo String ou &str, mas encontrou: u32
//  --> src/main.rs:8:5
//   |
// 8 |     #[validar(nao_vazio)]
//   |     ^^^^^^^^^^^^^^^^^^^^^
// 9 |     idade: u32,
//   |            ^^^

Quando usar macros procedurais

Macros procedurais são ferramentas poderosas mas com custo real: aumentam o tempo de compilação, tornam o código mais difícil de depurar, e adicionam complexidade ao projeto.

Use-as quando você precisa eliminar boilerplate repetitivo que traits ou generics não conseguem resolver, quando quer fazer verificações semânticas em tempo de compilação, ou quando está criando uma DSL que melhora significativamente a expressividade.

Não use quando um trait bem projetado resolve o problema, quando generics são suficientes, ou quando a macro economizaria apenas algumas linhas — o custo de manutenção raramente vale para economias pequenas.


Fontes e leituras recomendadas

  • "Procedural Macros" — The Rust Reference — especificação oficial — https://doc.rust-lang.org/reference/procedural-macros.html
  • syn documentation — parsing de AST Rust — https://docs.rs/syn
  • quote documentation — geração de código — https://docs.rs/quote
  • proc-macro2 documentation — https://docs.rs/proc-macro2
  • "proc-macro-workshop" — David Tolnay — exercícios práticos excelentes — https://github.com/dtolnay/proc-macro-workshop
  • trybuild crate — testes de macros com casos de erro — https://docs.rs/trybuild
  • "Macros in Rust: A tutorial with examples" — LogRocket — https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
  • Serde source code — a macro procedural mais estudada do ecossistema — https://github.com/serde-rs/serde

 

Comentários

Mais em Rust

Compilação Condicional e Feature Flags — Gerenciando Variações do Código
Compilação Condicional e Feature Flags — Gerenciando Variações do Código

Rust — Artigo #37 Compilação Condicional e Feature Flags — Gerenciando Variaç...

Construindo uma API REST com Axum
Construindo uma API REST com Axum

Chegamos ao primeiro artigo verdadeiramente orientado a projeto. Até aqui con...

Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust
Segurança e Auditoria — Fuzzing, Análise Estática e Hardening de Código Rust

Rust — Artigo #47 Segurança e Auditoria — Fuzzing, Análise Estática e Hardeni...