makectl 0.2.0

Generate and manage targets in your Makefiles
pub mod embedded;

#[derive(Debug, Clone)]
pub struct Template {
    pub id: String,
    pub language: String,
    pub name: String,
    pub description: String,
    pub content: String,
}

pub struct TemplateRegistry {
    templates: Vec<Template>,
}

impl TemplateRegistry {
    pub fn new() -> Self {
        let mut templates = Vec::new();
        for (lang, name, raw_content) in embedded::all_templates() {
            let (description, content) = parse_template_content(raw_content);
            templates.push(Template {
                id: format!("{}/{}", lang, name),
                language: lang.to_string(),
                name: name.to_string(),
                description,
                content,
            });
        }
        TemplateRegistry { templates }
    }

    pub fn get(&self, id: &str) -> Option<&Template> {
        self.templates.iter().find(|t| t.id == id)
    }

    pub fn list_all(&self) -> &[Template] {
        &self.templates
    }

    pub fn list_by_language(&self, lang: &str) -> Vec<&Template> {
        self.templates
            .iter()
            .filter(|t| t.language == lang)
            .collect()
    }

    #[allow(dead_code)]
    pub fn languages(&self) -> Vec<&str> {
        let mut langs: Vec<&str> = self.templates.iter().map(|t| t.language.as_str()).collect();
        langs.sort();
        langs.dedup();
        langs
    }
}

fn parse_template_content(raw: &str) -> (String, String) {
    let mut lines = raw.lines();
    let first_line = lines.next().unwrap_or("");

    if let Some(rest) = first_line.strip_prefix("## makectl: description=\"") {
        let description = rest.trim_end_matches('"').to_string();
        let content: String = lines.collect::<Vec<&str>>().join("\n");
        let content = content.trim_start_matches('\n').to_string();
        (description, content)
    } else {
        (String::new(), raw.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_loads_all_templates() {
        let reg = TemplateRegistry::new();
        assert!(reg.list_all().len() >= 30);
    }

    #[test]
    fn get_python_test() {
        let reg = TemplateRegistry::new();
        let t = reg.get("python/test").unwrap();
        assert_eq!(t.language, "python");
        assert_eq!(t.name, "test");
        assert!(!t.description.is_empty());
        assert!(t.content.contains("pytest"));
    }

    #[test]
    fn get_nonexistent_returns_none() {
        let reg = TemplateRegistry::new();
        assert!(reg.get("nonexistent/foo").is_none());
    }

    #[test]
    fn list_by_language_filters() {
        let reg = TemplateRegistry::new();
        let rust_templates = reg.list_by_language("rust");
        assert!(rust_templates.len() >= 6);
        assert!(rust_templates.iter().all(|t| t.language == "rust"));
    }

    #[test]
    fn languages_returns_all() {
        let reg = TemplateRegistry::new();
        let langs = reg.languages();
        assert!(langs.contains(&"generic"));
        assert!(langs.contains(&"python"));
        assert!(langs.contains(&"rust"));
        assert!(langs.contains(&"go"));
        assert!(langs.contains(&"node"));
    }

    #[test]
    fn all_templates_have_descriptions() {
        let reg = TemplateRegistry::new();
        for t in reg.list_all() {
            assert!(
                !t.description.is_empty(),
                "Template {} has no description",
                t.id
            );
        }
    }

    #[test]
    fn all_templates_have_phony() {
        let reg = TemplateRegistry::new();
        for t in reg.list_all() {
            assert!(
                t.content.contains(".PHONY:"),
                "Template {} missing .PHONY declaration",
                t.id
            );
        }
    }
}