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}