Files
weevil/src/templates/mod.rs
Eric Ratliff 70a1acc2a1 feat: Weevil v1.0.0-beta1 - FTC Project Generator
Cross-platform tool for generating clean, testable FTC robot projects
without editing the SDK installation.

Features:
- Standalone project generation with proper separation from SDK
- Per-project SDK configuration via .weevil.toml
- Local unit testing support (no robot required)
- Cross-platform build/deploy scripts (Linux/macOS/Windows)
- Project upgrade system preserving user code
- Configuration management commands
- Comprehensive test suite (11 passing tests)
- Zero-warning builds

Architecture:
- Pure Rust implementation with embedded Gradle wrapper
- Projects use deployToSDK task to copy code to FTC SDK TeamCode
- Git-ready projects with automatic initialization
- USB and WiFi deployment with auto-detection

Commands:
- weevil new <name> - Create new project
- weevil upgrade <path> - Update project infrastructure
- weevil config <path> - View/modify project configuration
- weevil sdk status/install/update - Manage SDKs

Addresses the core problem: FTC's SDK structure forces students to
edit framework internals instead of separating concerns like industry
standard practices. Weevil enables proper software engineering workflows
for robotics education.
2026-01-25 00:17:51 -06:00

101 lines
2.7 KiB
Rust

use include_dir::{include_dir, Dir};
use std::path::Path;
use anyhow::{Result, Context};
use tera::{Tera, Context as TeraContext};
use std::fs;
// Embed all template files at compile time
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
pub struct TemplateEngine {
#[allow(dead_code)]
tera: Tera,
}
impl TemplateEngine {
#[allow(dead_code)]
pub fn new() -> Result<Self> {
let mut tera = Tera::default();
// Load all templates from embedded directory
for file in TEMPLATES_DIR.files() {
let path = file.path().to_string_lossy();
let contents = file.contents_utf8()
.context("Template must be valid UTF-8")?;
tera.add_raw_template(&path, contents)?;
}
Ok(Self { tera })
}
#[allow(dead_code)]
pub fn render_to_file(
&self,
template_name: &str,
output_path: &Path,
context: &TeraContext,
) -> Result<()> {
let rendered = self.tera.render(template_name, context)?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output_path, rendered)?;
Ok(())
}
#[allow(dead_code)]
pub fn extract_static_file(&self, template_path: &str, output_path: &Path) -> Result<()> {
let file = TEMPLATES_DIR
.get_file(template_path)
.context(format!("Template not found: {}", template_path))?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output_path, file.contents())?;
Ok(())
}
#[allow(dead_code)]
pub fn list_templates(&self) -> Vec<String> {
TEMPLATES_DIR
.files()
.map(|f| f.path().to_string_lossy().to_string())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_template_engine_creation() {
let engine = TemplateEngine::new();
assert!(engine.is_ok());
}
#[test]
fn test_list_templates() {
let engine = TemplateEngine::new().unwrap();
let templates = engine.list_templates();
assert!(!templates.is_empty());
}
#[test]
fn test_render_template() {
let _engine = TemplateEngine::new().unwrap();
let temp = TempDir::new().unwrap();
let _output = temp.path().join("test.txt");
let mut context = TeraContext::new();
context.insert("project_name", "TestRobot");
// This will fail until we add templates, but shows the pattern
// engine.render_to_file("README.md", &output, &context).unwrap();
}
}