Skip to main content
Glama
fmt.rs6.23 kB
//! Terraform fmt operations for code formatting. use serde::{Deserialize, Serialize}; use std::path::Path; use std::process::Command; /// Format check result for a single file #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileFormatResult { pub file: String, pub formatted: bool, pub diff: Option<String>, } /// Overall format result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FormatResult { pub success: bool, pub files_checked: i32, pub files_formatted: i32, pub files_unchanged: i32, pub file_results: Vec<FileFormatResult>, pub message: String, } /// Check formatting without making changes pub fn check_format( terraform_path: &Path, project_dir: &Path, file: Option<&str>, ) -> anyhow::Result<FormatResult> { format_internal(terraform_path, project_dir, file, true, false) } /// Format files and show diff pub fn format_with_diff( terraform_path: &Path, project_dir: &Path, file: Option<&str>, ) -> anyhow::Result<FormatResult> { format_internal(terraform_path, project_dir, file, false, true) } /// Format files in place pub fn format_files( terraform_path: &Path, project_dir: &Path, file: Option<&str>, ) -> anyhow::Result<FormatResult> { format_internal(terraform_path, project_dir, file, false, false) } /// Internal format implementation fn format_internal( terraform_path: &Path, project_dir: &Path, file: Option<&str>, check_only: bool, show_diff: bool, ) -> anyhow::Result<FormatResult> { let mut cmd = Command::new(terraform_path); cmd.arg("fmt"); if check_only { cmd.arg("-check"); } if show_diff { cmd.arg("-diff"); } // List files that would be formatted cmd.arg("-list=true"); // Recursive formatting cmd.arg("-recursive"); // If a specific file is provided, use it if let Some(file_path) = file { cmd.arg(file_path); } cmd.current_dir(project_dir); let output = cmd.output()?; let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Parse the output let mut file_results = Vec::new(); let mut files_formatted = 0; // stdout contains list of formatted/unformatted files for line in stdout.lines() { let line = line.trim(); if line.is_empty() { continue; } // terraform fmt -list=true outputs filenames that were/would be formatted file_results.push(FileFormatResult { file: line.to_string(), formatted: true, diff: None, }); files_formatted += 1; } // If we're doing a diff check, parse the diff output if show_diff && !stderr.is_empty() { // Diffs are written to stdout when using -diff if let Some(last_result) = file_results.last_mut() { last_result.diff = Some(stderr.to_string()); } } // Count unchanged files by listing all .tf files let all_tf_files = count_tf_files(project_dir); let files_unchanged = all_tf_files.saturating_sub(files_formatted); let success = output.status.success() || (!check_only && output.status.code() == Some(0)); let message = if check_only { if output.status.success() { "All files are properly formatted".to_string() } else { format!("{} files need formatting", files_formatted) } } else if files_formatted > 0 { format!("Formatted {} files", files_formatted) } else { "No files needed formatting".to_string() }; Ok(FormatResult { success, files_checked: all_tf_files as i32, files_formatted: files_formatted as i32, files_unchanged: files_unchanged as i32, file_results, message, }) } /// Count .tf files in a directory (recursive) fn count_tf_files(dir: &Path) -> usize { let mut count = 0; if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { // Skip hidden directories and common non-terraform directories if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name.starts_with('.') || name == "node_modules" || name == "vendor" { continue; } } count += count_tf_files(&path); } else if path.is_file() { if let Some(ext) = path.extension() { if ext == "tf" { count += 1; } } } } } count } /// Get format style recommendations #[allow(dead_code)] pub fn get_format_recommendations() -> Vec<String> { vec![ "Use 2-space indentation for nested blocks".to_string(), "Align equals signs in attribute assignments within a block".to_string(), "Use lowercase for resource types and attribute names".to_string(), "Place the opening brace on the same line as the block header".to_string(), "Use blank lines to separate logical groups of attributes".to_string(), "Order meta-arguments (count, for_each, lifecycle) before resource-specific arguments" .to_string(), "Keep line length under 120 characters for readability".to_string(), ] } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; #[test] fn test_count_tf_files() { let temp_dir = TempDir::new().unwrap(); // Create some .tf files fs::write(temp_dir.path().join("main.tf"), "").unwrap(); fs::write(temp_dir.path().join("variables.tf"), "").unwrap(); fs::write(temp_dir.path().join("other.txt"), "").unwrap(); let count = count_tf_files(temp_dir.path()); assert_eq!(count, 2); } #[test] fn test_format_recommendations() { let recommendations = get_format_recommendations(); assert!(!recommendations.is_empty()); assert!(recommendations.iter().any(|r| r.contains("indentation"))); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server