textfsm-macros 0.3.1

Compile-time template validation macros for TextFSM
Documentation
//! Compile-time template validation macros for TextFSM.
//!
//! These macros validate TextFSM template files at compile time, providing
//! early feedback on template errors without any runtime cost.
//!
//! # Example
//!
//! ```rust,ignore
//! use textfsm_macros::{validate_template, validate_templates};
//!
//! // Validate a single template file
//! validate_template!("templates/cisco_show_version.textfsm");
//!
//! // Validate all .textfsm files in a directory
//! validate_templates!("templates/");
//! ```

use proc_macro::TokenStream;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, LitStr};

/// Validates a single TextFSM template file at compile time.
///
/// This macro reads and parses the specified template file during compilation.
/// If the template is invalid, a compile error is emitted with details about
/// the parsing error. If valid, the macro expands to nothing (zero runtime cost).
///
/// # Arguments
///
/// * `path` - A string literal path to the template file, relative to the
///   calling crate's `Cargo.toml` directory.
///
/// # Example
///
/// ```rust,ignore
/// validate_template!("templates/cisco_show_version.textfsm");
/// ```
///
/// # Compile-time Errors
///
/// If the template is invalid, you'll see a compile error like:
///
/// ```text
/// error: Template validation failed for 'templates/bad.textfsm':
///        invalid rule at line 5: unknown variable 'Interfce'
/// ```
#[proc_macro]
pub fn validate_template(input: TokenStream) -> TokenStream {
    let path_lit = parse_macro_input!(input as LitStr);
    let path_str = path_lit.value();

    // Get the calling crate's manifest directory
    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
        Ok(dir) => PathBuf::from(dir),
        Err(_) => {
            return syn::Error::new_spanned(
                &path_lit,
                "CARGO_MANIFEST_DIR not set; cannot resolve template path",
            )
            .to_compile_error()
            .into();
        }
    };

    let full_path = manifest_dir.join(&path_str);

    // Read the template file
    let content = match std::fs::read_to_string(&full_path) {
        Ok(c) => c,
        Err(e) => {
            let msg = format!(
                "Failed to read template file '{}': {}",
                full_path.display(),
                e
            );
            return syn::Error::new_spanned(&path_lit, msg)
                .to_compile_error()
                .into();
        }
    };

    // Parse and validate the template
    if let Err(e) = textfsm_core::Template::parse_str(&content) {
        let msg = format!(
            "Template validation failed for '{}':\n       {}",
            path_str, e
        );
        return syn::Error::new_spanned(&path_lit, msg)
            .to_compile_error()
            .into();
    }

    // Success: expand to nothing
    TokenStream::from(quote! {})
}

/// Validates all TextFSM template files in a directory at compile time.
///
/// This macro finds all `.textfsm` files in the specified directory (recursively)
/// and validates each one during compilation. If any template is invalid, a
/// compile error is emitted. If all are valid, the macro expands to nothing.
///
/// # Arguments
///
/// * `path` - A string literal path to the templates directory, relative to the
///   calling crate's `Cargo.toml` directory.
///
/// # Example
///
/// ```rust,ignore
/// validate_templates!("templates/");
/// ```
///
/// # Compile-time Errors
///
/// If any template is invalid, you'll see a compile error like:
///
/// ```text
/// error: Template validation failed for 'templates/bad.textfsm':
///        missing required 'Start' state
/// ```
#[proc_macro]
pub fn validate_templates(input: TokenStream) -> TokenStream {
    let path_lit = parse_macro_input!(input as LitStr);
    let path_str = path_lit.value();

    // Get the calling crate's manifest directory
    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
        Ok(dir) => PathBuf::from(dir),
        Err(_) => {
            return syn::Error::new_spanned(
                &path_lit,
                "CARGO_MANIFEST_DIR not set; cannot resolve template path",
            )
            .to_compile_error()
            .into();
        }
    };

    let full_path = manifest_dir.join(&path_str);

    // Check directory exists
    if !full_path.is_dir() {
        let msg = format!(
            "Template directory not found: '{}'",
            full_path.display()
        );
        return syn::Error::new_spanned(&path_lit, msg)
            .to_compile_error()
            .into();
    }

    // Find all .textfsm files
    let pattern = full_path.join("**/*.textfsm");
    let pattern_str = pattern.to_string_lossy();

    let entries = match glob::glob(&pattern_str) {
        Ok(paths) => paths,
        Err(e) => {
            let msg = format!("Invalid glob pattern: {}", e);
            return syn::Error::new_spanned(&path_lit, msg)
                .to_compile_error()
                .into();
        }
    };

    let mut errors = Vec::new();
    let mut count = 0;

    for entry in entries {
        let file_path = match entry {
            Ok(p) => p,
            Err(e) => {
                errors.push(format!("Glob error: {}", e));
                continue;
            }
        };

        count += 1;

        // Read the template file
        let content = match std::fs::read_to_string(&file_path) {
            Ok(c) => c,
            Err(e) => {
                errors.push(format!(
                    "Failed to read '{}': {}",
                    file_path.display(),
                    e
                ));
                continue;
            }
        };

        // Parse and validate
        if let Err(e) = textfsm_core::Template::parse_str(&content) {
            // Get relative path for nicer error messages
            let relative = file_path
                .strip_prefix(&manifest_dir)
                .unwrap_or(&file_path);
            errors.push(format!(
                "Validation failed for '{}':\n       {}",
                relative.display(),
                e
            ));
        }
    }

    if !errors.is_empty() {
        let msg = format!(
            "Template validation errors:\n\n{}",
            errors.join("\n\n")
        );
        return syn::Error::new_spanned(&path_lit, msg)
            .to_compile_error()
            .into();
    }

    if count == 0 {
        let msg = format!(
            "No .textfsm files found in '{}'",
            full_path.display()
        );
        return syn::Error::new_spanned(&path_lit, msg)
            .to_compile_error()
            .into();
    }

    // Success: expand to nothing
    TokenStream::from(quote! {})
}

/// Validates an index file and all templates it references at compile time.
///
/// This macro parses the index file, extracts all template file names from the
/// Template column, and validates each template during compilation. If the index
/// file is invalid or any referenced template is invalid, a compile error is emitted.
///
/// # Arguments
///
/// * `path` - A string literal path to the index file, relative to the
///   calling crate's `Cargo.toml` directory. The templates are expected to be
///   in the same directory as the index file.
///
/// # Example
///
/// ```rust,ignore
/// use textfsm_macros::validate_index;
///
/// // Validate index and all referenced templates
/// validate_index!("templates/index");
/// ```
///
/// # Compile-time Errors
///
/// If the index or any template is invalid, you'll see a compile error like:
///
/// ```text
/// error: Index validation failed for 'templates/index':
///        Template 'cisco_show_version.textfsm' (line 3): invalid rule at line 5
/// ```
#[proc_macro]
pub fn validate_index(input: TokenStream) -> TokenStream {
    let path_lit = parse_macro_input!(input as LitStr);
    let path_str = path_lit.value();

    // Get the calling crate's manifest directory
    let manifest_dir = match std::env::var("CARGO_MANIFEST_DIR") {
        Ok(dir) => PathBuf::from(dir),
        Err(_) => {
            return syn::Error::new_spanned(
                &path_lit,
                "CARGO_MANIFEST_DIR not set; cannot resolve index path",
            )
            .to_compile_error()
            .into();
        }
    };

    let full_path = manifest_dir.join(&path_str);

    // Read the index file
    let content = match std::fs::read_to_string(&full_path) {
        Ok(c) => c,
        Err(e) => {
            let msg = format!(
                "Failed to read index file '{}': {}",
                full_path.display(),
                e
            );
            return syn::Error::new_spanned(&path_lit, msg)
                .to_compile_error()
                .into();
        }
    };

    // Parse the index file
    let index = match textfsm_core::Index::parse_str(&content) {
        Ok(idx) => idx,
        Err(e) => {
            let msg = format!(
                "Index validation failed for '{}':\n       {}",
                path_str, e
            );
            return syn::Error::new_spanned(&path_lit, msg)
                .to_compile_error()
                .into();
        }
    };

    // Get the directory containing the index file (templates are relative to this)
    let template_dir = full_path.parent().unwrap_or(&manifest_dir);

    // Collect all unique template names with their line numbers
    let mut template_lines: std::collections::HashMap<String, usize> =
        std::collections::HashMap::new();
    for entry in index.entries() {
        for template in entry.templates() {
            template_lines
                .entry(template.to_string())
                .or_insert(entry.line_num());
        }
    }

    // Validate each template
    let mut errors = Vec::new();

    for (template_name, line_num) in &template_lines {
        let template_path = template_dir.join(template_name);

        // Read the template file
        let template_content = match std::fs::read_to_string(&template_path) {
            Ok(c) => c,
            Err(e) => {
                errors.push(format!(
                    "Template '{}' (index line {}): failed to read: {}",
                    template_name, line_num, e
                ));
                continue;
            }
        };

        // Parse and validate the template
        if let Err(e) = textfsm_core::Template::parse_str(&template_content) {
            errors.push(format!(
                "Template '{}' (index line {}): {}",
                template_name, line_num, e
            ));
        }
    }

    if !errors.is_empty() {
        let msg = format!(
            "Index validation failed for '{}':\n\n{}",
            path_str,
            errors.join("\n\n")
        );
        return syn::Error::new_spanned(&path_lit, msg)
            .to_compile_error()
            .into();
    }

    // Success: expand to nothing
    TokenStream::from(quote! {})
}