Skip to main content

textfsm_macros/
lib.rs

1//! Compile-time template validation macros for TextFSM.
2//!
3//! These macros validate TextFSM template files at compile time, providing
4//! early feedback on template errors without any runtime cost.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use textfsm_macros::{validate_template, validate_templates};
10//!
11//! // Validate a single template file
12//! validate_template!("templates/cisco_show_version.textfsm");
13//!
14//! // Validate all .textfsm files in a directory
15//! validate_templates!("templates/");
16//! ```
17
18use proc_macro::TokenStream;
19use quote::quote;
20use std::path::PathBuf;
21use syn::{parse_macro_input, LitStr};
22
23/// Validates a single TextFSM template file at compile time.
24///
25/// This macro reads and parses the specified template file during compilation.
26/// If the template is invalid, a compile error is emitted with details about
27/// the parsing error. If valid, the macro expands to nothing (zero runtime cost).
28///
29/// # Arguments
30///
31/// * `path` - A string literal path to the template file, relative to the
32///   calling crate's `Cargo.toml` directory.
33///
34/// # Example
35///
36/// ```rust,ignore
37/// validate_template!("templates/cisco_show_version.textfsm");
38/// ```
39///
40/// # Compile-time Errors
41///
42/// If the template is invalid, you'll see a compile error like:
43///
44/// ```text
45/// error: Template validation failed for 'templates/bad.textfsm':
46///        invalid rule at line 5: unknown variable 'Interfce'
47/// ```
48#[proc_macro]
49pub fn validate_template(input: TokenStream) -> TokenStream {
50    let path_lit = parse_macro_input!(input as LitStr);
51    let path_str = path_lit.value();
52
53    // Get the calling crate's manifest directory
54    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
55        Ok(dir) => PathBuf::from(dir),
56        Err(_) => {
57            return syn::Error::new_spanned(
58                &path_lit,
59                "CARGO_MANIFEST_DIR not set; cannot resolve template path",
60            )
61            .to_compile_error()
62            .into();
63        }
64    };
65
66    let full_path = manifest_dir.join(&path_str);
67
68    // Read the template file
69    let content = match std::fs::read_to_string(&full_path) {
70        Ok(c) => c,
71        Err(e) => {
72            let msg = format!(
73                "Failed to read template file '{}': {}",
74                full_path.display(),
75                e
76            );
77            return syn::Error::new_spanned(&path_lit, msg)
78                .to_compile_error()
79                .into();
80        }
81    };
82
83    // Parse and validate the template
84    if let Err(e) = textfsm_core::Template::parse_str(&content) {
85        let msg = format!(
86            "Template validation failed for '{}':\n       {}",
87            path_str, e
88        );
89        return syn::Error::new_spanned(&path_lit, msg)
90            .to_compile_error()
91            .into();
92    }
93
94    // Success: expand to nothing
95    TokenStream::from(quote! {})
96}
97
98/// Validates all TextFSM template files in a directory at compile time.
99///
100/// This macro finds all `.textfsm` files in the specified directory (recursively)
101/// and validates each one during compilation. If any template is invalid, a
102/// compile error is emitted. If all are valid, the macro expands to nothing.
103///
104/// # Arguments
105///
106/// * `path` - A string literal path to the templates directory, relative to the
107///   calling crate's `Cargo.toml` directory.
108///
109/// # Example
110///
111/// ```rust,ignore
112/// validate_templates!("templates/");
113/// ```
114///
115/// # Compile-time Errors
116///
117/// If any template is invalid, you'll see a compile error like:
118///
119/// ```text
120/// error: Template validation failed for 'templates/bad.textfsm':
121///        missing required 'Start' state
122/// ```
123#[proc_macro]
124pub fn validate_templates(input: TokenStream) -> TokenStream {
125    let path_lit = parse_macro_input!(input as LitStr);
126    let path_str = path_lit.value();
127
128    // Get the calling crate's manifest directory
129    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
130        Ok(dir) => PathBuf::from(dir),
131        Err(_) => {
132            return syn::Error::new_spanned(
133                &path_lit,
134                "CARGO_MANIFEST_DIR not set; cannot resolve template path",
135            )
136            .to_compile_error()
137            .into();
138        }
139    };
140
141    let full_path = manifest_dir.join(&path_str);
142
143    // Check directory exists
144    if !full_path.is_dir() {
145        let msg = format!(
146            "Template directory not found: '{}'",
147            full_path.display()
148        );
149        return syn::Error::new_spanned(&path_lit, msg)
150            .to_compile_error()
151            .into();
152    }
153
154    // Find all .textfsm files
155    let pattern = full_path.join("**/*.textfsm");
156    let pattern_str = pattern.to_string_lossy();
157
158    let entries = match glob::glob(&pattern_str) {
159        Ok(paths) => paths,
160        Err(e) => {
161            let msg = format!("Invalid glob pattern: {}", e);
162            return syn::Error::new_spanned(&path_lit, msg)
163                .to_compile_error()
164                .into();
165        }
166    };
167
168    let mut errors = Vec::new();
169    let mut count = 0;
170
171    for entry in entries {
172        let file_path = match entry {
173            Ok(p) => p,
174            Err(e) => {
175                errors.push(format!("Glob error: {}", e));
176                continue;
177            }
178        };
179
180        count += 1;
181
182        // Read the template file
183        let content = match std::fs::read_to_string(&file_path) {
184            Ok(c) => c,
185            Err(e) => {
186                errors.push(format!(
187                    "Failed to read '{}': {}",
188                    file_path.display(),
189                    e
190                ));
191                continue;
192            }
193        };
194
195        // Parse and validate
196        if let Err(e) = textfsm_core::Template::parse_str(&content) {
197            // Get relative path for nicer error messages
198            let relative = file_path
199                .strip_prefix(&manifest_dir)
200                .unwrap_or(&file_path);
201            errors.push(format!(
202                "Validation failed for '{}':\n       {}",
203                relative.display(),
204                e
205            ));
206        }
207    }
208
209    if !errors.is_empty() {
210        let msg = format!(
211            "Template validation errors:\n\n{}",
212            errors.join("\n\n")
213        );
214        return syn::Error::new_spanned(&path_lit, msg)
215            .to_compile_error()
216            .into();
217    }
218
219    if count == 0 {
220        let msg = format!(
221            "No .textfsm files found in '{}'",
222            full_path.display()
223        );
224        return syn::Error::new_spanned(&path_lit, msg)
225            .to_compile_error()
226            .into();
227    }
228
229    // Success: expand to nothing
230    TokenStream::from(quote! {})
231}
232
233/// Validates an index file and all templates it references at compile time.
234///
235/// This macro parses the index file, extracts all template file names from the
236/// Template column, and validates each template during compilation. If the index
237/// file is invalid or any referenced template is invalid, a compile error is emitted.
238///
239/// # Arguments
240///
241/// * `path` - A string literal path to the index file, relative to the
242///   calling crate's `Cargo.toml` directory. The templates are expected to be
243///   in the same directory as the index file.
244///
245/// # Example
246///
247/// ```rust,ignore
248/// use textfsm_macros::validate_index;
249///
250/// // Validate index and all referenced templates
251/// validate_index!("templates/index");
252/// ```
253///
254/// # Compile-time Errors
255///
256/// If the index or any template is invalid, you'll see a compile error like:
257///
258/// ```text
259/// error: Index validation failed for 'templates/index':
260///        Template 'cisco_show_version.textfsm' (line 3): invalid rule at line 5
261/// ```
262#[proc_macro]
263pub fn validate_index(input: TokenStream) -> TokenStream {
264    let path_lit = parse_macro_input!(input as LitStr);
265    let path_str = path_lit.value();
266
267    // Get the calling crate's manifest directory
268    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
269        Ok(dir) => PathBuf::from(dir),
270        Err(_) => {
271            return syn::Error::new_spanned(
272                &path_lit,
273                "CARGO_MANIFEST_DIR not set; cannot resolve index path",
274            )
275            .to_compile_error()
276            .into();
277        }
278    };
279
280    let full_path = manifest_dir.join(&path_str);
281
282    // Read the index file
283    let content = match std::fs::read_to_string(&full_path) {
284        Ok(c) => c,
285        Err(e) => {
286            let msg = format!(
287                "Failed to read index file '{}': {}",
288                full_path.display(),
289                e
290            );
291            return syn::Error::new_spanned(&path_lit, msg)
292                .to_compile_error()
293                .into();
294        }
295    };
296
297    // Parse the index file
298    let index = match textfsm_core::Index::parse_str(&content) {
299        Ok(idx) => idx,
300        Err(e) => {
301            let msg = format!(
302                "Index validation failed for '{}':\n       {}",
303                path_str, e
304            );
305            return syn::Error::new_spanned(&path_lit, msg)
306                .to_compile_error()
307                .into();
308        }
309    };
310
311    // Get the directory containing the index file (templates are relative to this)
312    let template_dir = full_path.parent().unwrap_or(&manifest_dir);
313
314    // Collect all unique template names with their line numbers
315    let mut template_lines: std::collections::HashMap<String, usize> =
316        std::collections::HashMap::new();
317    for entry in index.entries() {
318        for template in entry.templates() {
319            template_lines
320                .entry(template.to_string())
321                .or_insert(entry.line_num());
322        }
323    }
324
325    // Validate each template
326    let mut errors = Vec::new();
327
328    for (template_name, line_num) in &template_lines {
329        let template_path = template_dir.join(template_name);
330
331        // Read the template file
332        let template_content = match std::fs::read_to_string(&template_path) {
333            Ok(c) => c,
334            Err(e) => {
335                errors.push(format!(
336                    "Template '{}' (index line {}): failed to read: {}",
337                    template_name, line_num, e
338                ));
339                continue;
340            }
341        };
342
343        // Parse and validate the template
344        if let Err(e) = textfsm_core::Template::parse_str(&template_content) {
345            errors.push(format!(
346                "Template '{}' (index line {}): {}",
347                template_name, line_num, e
348            ));
349        }
350    }
351
352    if !errors.is_empty() {
353        let msg = format!(
354            "Index validation failed for '{}':\n\n{}",
355            path_str,
356            errors.join("\n\n")
357        );
358        return syn::Error::new_spanned(&path_lit, msg)
359            .to_compile_error()
360            .into();
361    }
362
363    // Success: expand to nothing
364    TokenStream::from(quote! {})
365}