cnab_derive/
lib.rs

1//! # Derive Macro para FixedWidth
2//!
3//! Este crate fornece a macro procedural `#[derive(FixedWidth)]` que gera automaticamente
4//! a implementação da trait `FixedWidthParse` do crate `cnab_fixed_width`.
5//!
6//! # Exemplo de Uso
7//!
8//! ```ignore
9//! use fixedwidth_derive::FixedWidth;
10//!
11//! #[derive(FixedWidth)]
12//! struct Header {
13//!     #[fw(pos = "1..3", numeric)]
14//!     banco: u32,
15//!
16//!     #[fw(pos = "4..8", numeric)]
17//!     lote: u32,
18//!
19//!     #[fw(pos = "10..20", alpha)]
20//!     texto: String,
21//! }
22//! ```
23
24
25use proc_macro::TokenStream;
26use quote::quote;
27use syn::{parse_macro_input, DeriveInput, Data, Fields};
28
29/// Estrutura intermediária para armazenar os dados de um campo
30/// extraídos da AST (Abstract Syntax Tree) do código do usuário.
31struct ParsedField {
32    /// Nome do campo na struct (Identificador).
33    ident: syn::Ident,
34    /// Tipo do campo (ex: String, i64, f64).
35    ty: syn::Type,
36    /// Posição inicial (1-based).
37    pos_start: usize,
38    /// Posição final (1-based).
39    pos_end: usize,
40    /// Tipo de formatação CNAB (Alpha, Numeric, Decimal).
41    kind: FieldKindMacro,
42}
43
44/// Representação interna dos tipos de campos suportados pela macro.
45enum FieldKindMacro {
46    Alpha,
47    Numeric,
48    Decimal { scale: u8 },
49}
50
51/// Helper para parsear a string de posição "start..end".
52///
53/// Espera o formato "1..10" (inclusive).
54/// Retorna erro se o formato for inválido ou se start for 0.
55fn parse_pos(lit: &syn::LitStr) -> syn::Result<(usize, usize)> {
56    let s = lit.value();
57    let parts: Vec<_> = s.split("..").collect();
58
59    if parts.len() != 2 {
60        return Err(syn::Error::new_spanned(lit, "pos deve estar no formato start..end"));
61    }
62    let start = parts[0].parse::<usize>().map_err(|_| syn::Error::new_spanned(lit, "start inválido"))?;
63    let end = parts[1].parse::<usize>().map_err(|_| syn::Error::new_spanned(lit, "end inválido"))?;
64
65    if start == 0 || end < start {
66        return Err(syn::Error::new_spanned(lit, "pos inválido: start deve ser >=1 e end >= start"));
67    }
68    Ok((start, end))
69}
70
71// --- A MACRO ---
72
73/// Ponto de entrada da Macro Derive.
74///
75/// Esta função:
76/// 1. Lê a struct de entrada.
77/// 2. Itera sobre os campos procurando atributos `#[fw(...)]`.
78/// 3. Valida se há sobreposição de posições.
79/// 4. Gera o código Rust que implementa `FixedWidthParse`.
80#[proc_macro_derive(FixedWidth, attributes(fw))]
81pub fn derive_fixed_width(input: TokenStream) -> TokenStream {
82    // 1. Parse da entrada (Código do usuário)
83    let input = parse_macro_input!(input as DeriveInput);
84    let name = &input.ident;
85
86    // Garante que é aplicado apenas em Structs com campos nomeados
87    let fields = match &input.data {
88        Data::Struct(data) => match &data.fields {
89            Fields::Named(fields) => &fields.named,
90            _ => return syn::Error::new_spanned(&input.ident, "Apenas campos nomeados suportados").to_compile_error().into(),
91        },
92        _ => return syn::Error::new_spanned(&input.ident, "Apenas structs suportadas").to_compile_error().into(),
93    };
94
95    let mut parsed_fields = Vec::new();
96
97    // 2. Extração dos Metadados
98    for field in fields {
99        let ident = field.ident.clone().unwrap();
100        let ty = field.ty.clone();
101        let mut pos = None;
102        let mut kind = None;
103
104        // Itera sobre os atributos do campo (ex: #[fw(...)])
105        for attr in &field.attrs {
106            if attr.path().is_ident("fw") {
107                attr.parse_nested_meta(|meta| {
108                    let name = meta.path.get_ident().map(|i| i.to_string());
109                    match name.as_deref() {
110                        // Atributo: pos = "1..10"
111                        Some("pos") => {
112                            let lit: syn::LitStr = meta.value()?.parse()?;
113                            pos = Some(parse_pos(&lit)?);
114                        }
115                        // Atributo: alpha
116                        Some("alpha") => kind = Some(FieldKindMacro::Alpha),
117                        // Atributo: numeric
118                        Some("numeric") => kind = Some(FieldKindMacro::Numeric),
119                        // Atributo: decimal = 2
120                        Some("decimal") => {
121                            let lit: syn::LitInt = meta.value()?.parse()?;
122                            kind = Some(FieldKindMacro::Decimal { scale: lit.base10_parse::<u8>()? });
123                        }
124                        _ => return Err(syn::Error::new_spanned(meta.path, "atributo fw desconhecido")),
125                    }
126                    Ok(())
127                }).expect("parse failed");
128            }
129        }
130
131        // Valida se os atributos obrigatórios foram preenchidos
132        let (start, end) = pos.expect("campo sem pos definido (ex: pos = \"1..10\")");
133        let kind = kind.expect("campo sem tipo definido (use alpha, numeric ou decimal)");
134
135        parsed_fields.push(ParsedField { ident, ty, pos_start: start, pos_end: end, kind });
136    }
137
138    // 3. Validação de Sobreposição (Overlap Check)
139    // Compara cada campo com todos os campos subsequentes para garantir integridade.
140    for (i, f1) in parsed_fields.iter().enumerate() {
141        for f2 in &parsed_fields[i + 1..] {
142
143            let overlap_start = std::cmp::max(f1.pos_start, f2.pos_start);
144            let overlap_end = std::cmp::min(f1.pos_end, f2.pos_end);
145
146            // Se o início da intersecção for menor ou igual ao fim, houve colisão.
147            if overlap_start <= overlap_end {
148                let err = syn::Error::new_spanned(
149                    &f2.ident, // Aponta o erro no editor para o segundo campo
150                    format!(
151                        "Conflito de Posição detectado!\nCampo A: '{}' ocupa {}..{}\nCampo B: '{}' ocupa {}..{}\nSobreposição nas posições: {}..{}",
152                        f1.ident, f1.pos_start, f1.pos_end,
153                        f2.ident, f2.pos_start, f2.pos_end,
154                        overlap_start, overlap_end
155                    )
156                );
157
158                return err.to_compile_error().into();
159            }
160        }
161    }
162
163    // --- GERAÇÃO DO CÓDIGO FINAL ---
164
165    // 4. Gera o vetor de FieldSpec (Definição do Layout)
166    // Isso cria o `vec![ FieldSpec { ... }, ... ]` que será usado em tempo de execução.
167    let field_specs = parsed_fields.iter().map(|f| {
168        let name = f.ident.to_string(); // String em compile-time
169        let start = f.pos_start;
170        let end = f.pos_end;
171
172        let kind = match &f.kind {
173            FieldKindMacro::Alpha => quote!(cnab_fixed_width::FieldKind::Alpha),
174            FieldKindMacro::Numeric => quote!(cnab_fixed_width::FieldKind::Numeric),
175            FieldKindMacro::Decimal { scale } => quote!(cnab_fixed_width::FieldKind::Decimal { scale: #scale }),
176        };
177
178        // Note o uso de `#name` direto, resultando em &'static str no código final
179        quote! {
180            cnab_fixed_width::FieldSpec {
181                name: #name,
182                pos: cnab_fixed_width::FieldPos { start: #start, end: #end },
183                kind: #kind,
184            }
185        }
186    });
187
188    // 5. Gera a inicialização da Struct (Mapeamento Value -> Struct Field)
189    // Converte os valores genéricos (Value::Numeric) para os tipos concretos (u32, i64, f64).
190    let field_inits = parsed_fields.iter().map(|f| {
191        let ident = &f.ident;
192        let name = ident.to_string();
193        let ty = &f.ty;
194
195        match f.kind {
196            FieldKindMacro::Alpha => quote! {
197                // Extrai string, garante UTF-8 válido e converte para String owned
198                #ident: parsed[#name].as_str()
199                    .ok_or(cnab_fixed_width::FixedWidthError::InvalidUtf8)?
200                    .to_string()
201            },
202            FieldKindMacro::Numeric => quote! {
203                // Extrai i64 e faz cast para o tipo do campo (ex: u32, i32, usize)
204                // Se falhar o tipo no core (ex: Alpha onde devia ser Num), retorna erro InvalidNumeric
205                #ident: parsed[#name].as_i64().ok_or(
206                    cnab_fixed_width::FixedWidthError::InvalidNumeric {
207                        field: #name,
208                        snippet: String::new(),
209                    }
210                )? as #ty
211            },
212            FieldKindMacro::Decimal { scale: _ } => quote! {
213                // Extrai f64 (já ajustado pela escala no core)
214                #ident: parsed[#name].as_f64().ok_or(
215                    cnab_fixed_width::FixedWidthError::InvalidNumeric {
216                        field: #name,
217                        snippet: String::new(),
218                    }
219                )?
220            },
221        }
222    });
223
224    // 6. Bloco final de implementação
225    quote! {
226        impl cnab_fixed_width::FixedWidthParse for #name {
227            fn parse(line: &str) -> cnab_fixed_width::Result<Self> {
228                // Criação da lista de especificações (barato pois são literais estáticos)
229                let fields = vec![ #(#field_specs),* ];
230
231                // Chamada ao parser genérico do Core
232                let parsed = cnab_fixed_width::parse_line(line, &fields)?;
233
234                // Construção da Struct segura
235                Ok(Self {
236                    #(#field_inits),*
237                })
238            }
239        }
240    }.into()
241}