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}