#![cfg_attr(coverage_nightly, coverage(off))]
use super::error::{ScaffoldError, ScaffoldResult};
use super::generator::TemplateGenerator;
use super::templates::{AgentTemplate, MCPServerTemplate, StateMachineTemplate};
use anyhow::Result;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use url::Url;
pub struct TemplateRegistry {
builtin: HashMap<String, Arc<dyn TemplateGenerator>>,
custom: HashMap<String, PathBuf>,
remote: HashMap<String, Url>,
}
impl TemplateRegistry {
#[must_use]
pub fn new() -> Self {
let mut builtin = HashMap::new();
builtin.insert(
"mcp-server".to_string(),
Arc::new(MCPServerTemplate::default()) as Arc<dyn TemplateGenerator>,
);
builtin.insert(
"state-machine".to_string(),
Arc::new(StateMachineTemplate::default()) as Arc<dyn TemplateGenerator>,
);
builtin.insert(
"calculator".to_string(),
Arc::new(DeterministicCalculatorTemplate::default()) as Arc<dyn TemplateGenerator>,
);
builtin.insert(
"hybrid".to_string(),
Arc::new(HybridAnalyzerTemplate::default()) as Arc<dyn TemplateGenerator>,
);
Self {
builtin,
custom: HashMap::new(),
remote: HashMap::new(),
}
}
pub fn get(&self, template: &AgentTemplate) -> ScaffoldResult<Arc<dyn TemplateGenerator>> {
match template {
AgentTemplate::MCPToolServer => self
.builtin
.get("mcp-server")
.cloned()
.ok_or_else(|| ScaffoldError::TemplateNotFound("mcp-server".to_string())),
AgentTemplate::StateMachineWorkflow => self
.builtin
.get("state-machine")
.cloned()
.ok_or_else(|| ScaffoldError::TemplateNotFound("state-machine".to_string())),
AgentTemplate::DeterministicCalculator => self
.builtin
.get("calculator")
.cloned()
.ok_or_else(|| ScaffoldError::TemplateNotFound("calculator".to_string())),
AgentTemplate::HybridAnalyzer => self
.builtin
.get("hybrid")
.cloned()
.ok_or_else(|| ScaffoldError::TemplateNotFound("hybrid".to_string())),
AgentTemplate::CustomAgent(path) => self.load_custom_template(path),
}
}
#[must_use]
pub fn list_available(&self) -> Vec<String> {
let mut templates: Vec<String> = self.builtin.keys().cloned().collect();
templates.extend(self.custom.keys().cloned());
templates.extend(self.remote.keys().cloned());
templates.sort();
templates
}
pub fn register_custom(&mut self, name: impl Into<String>, path: impl Into<PathBuf>) {
self.custom.insert(name.into(), path.into());
}
pub fn register_remote(&mut self, name: impl Into<String>, url: Url) {
self.remote.insert(name.into(), url);
}
pub async fn fetch_remote(&self, name: &str) -> ScaffoldResult<Arc<dyn TemplateGenerator>> {
let url = self
.remote
.get(name)
.ok_or_else(|| ScaffoldError::TemplateNotFound(format!("Remote template '{name}'")))?;
Err(ScaffoldError::NetworkError(format!(
"Remote template '{url}' requires network access. Please use a local or built-in template"
)))
}
pub fn validate_template_file(&self, path: &Path) -> Result<()> {
if !path.exists() {
return Err(ScaffoldError::TemplateNotFound(format!("{}", path.display())).into());
}
Ok(())
}
fn load_custom_template(&self, path: &Path) -> ScaffoldResult<Arc<dyn TemplateGenerator>> {
if !path.exists() {
return Err(ScaffoldError::TemplateNotFound(format!(
"{}",
path.display()
)));
}
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext == "toml" || ext == "yaml")
{
Err(ScaffoldError::InvalidTemplate(format!(
"Custom template support requires template file parsing: {}",
path.display()
)))
} else {
Err(ScaffoldError::InvalidTemplate(format!(
"Template file must be a TOML or YAML file: {}",
path.display()
)))
}
}
#[must_use]
pub fn has_template(&self, name: &str) -> bool {
self.builtin.contains_key(name)
|| self.custom.contains_key(name)
|| self.remote.contains_key(name)
}
#[must_use]
pub fn get_template_info(&self, name: &str) -> Option<TemplateInfo> {
self.builtin.get(name).map(|gen| TemplateInfo {
name: gen.name().to_string(),
description: gen.description().to_string(),
source: TemplateSource::Builtin,
})
}
}
impl Default for TemplateRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TemplateInfo {
pub name: String,
pub description: String,
pub source: TemplateSource,
}
#[derive(Debug, Clone)]
pub enum TemplateSource {
Builtin,
Custom(PathBuf),
Remote(Url),
}
use super::context::AgentContext;
use super::generator::GeneratedFiles;
use async_trait::async_trait;
struct DeterministicCalculatorTemplate {
name: String,
description: String,
}
impl Default for DeterministicCalculatorTemplate {
fn default() -> Self {
Self {
name: "calculator".to_string(),
description: "Deterministic calculator agent with verified operations".to_string(),
}
}
}
#[async_trait]
impl TemplateGenerator for DeterministicCalculatorTemplate {
fn generate(&self, ctx: &AgentContext) -> Result<GeneratedFiles> {
let mut files = GeneratedFiles::new();
files.add_text_file(
"Cargo.toml",
format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
"#,
ctx.name
),
);
files.add_text_file(
"src/main.rs",
format!(
r#"//! {} - Deterministic Calculator Agent
fn main() {{
println!("Calculator agent: {{}}", "{}");
}}
"#,
ctx.name, ctx.name
),
);
Ok(files)
}
fn validate_context(&self, ctx: &AgentContext) -> Result<()> {
if ctx.name.is_empty() {
anyhow::bail!("Agent name is required");
}
Ok(())
}
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
}
struct HybridAnalyzerTemplate {
name: String,
description: String,
}
impl Default for HybridAnalyzerTemplate {
fn default() -> Self {
Self {
name: "hybrid".to_string(),
description: "Hybrid agent with deterministic core and probabilistic wrapper"
.to_string(),
}
}
}
#[async_trait]
impl TemplateGenerator for HybridAnalyzerTemplate {
fn generate(&self, ctx: &AgentContext) -> Result<GeneratedFiles> {
if ctx.deterministic_core.is_none() || ctx.probabilistic_wrapper.is_none() {
anyhow::bail!("Hybrid agents require both deterministic core and probabilistic wrapper specifications");
}
let mut files = GeneratedFiles::new();
files.add_text_file(
"Cargo.toml",
format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
async-trait = "0.1"
tokio = {{ version = "1.40", features = ["full"] }}
serde = {{ version = "1.0", features = ["derive"] }}
serde_json = "1.0"
"#,
ctx.name
),
);
files.add_text_file(
"src/main.rs",
format!(
r#"//! {} - Hybrid Analyzer Agent
#[tokio::main]
async fn main() {{
println!("Hybrid analyzer agent: {{}}", "{}");
}}
"#,
ctx.name, ctx.name
),
);
files.add_text_file(
"src/core.rs",
r#"//! Deterministic core implementation.
pub fn deterministic_analyze(input: &str) -> String {
// Deterministic implementation
format!("Analyzed: {}", input)
}
"#
.to_string(),
);
files.add_text_file(
"src/wrapper.rs",
r#"//! Probabilistic wrapper implementation.
pub async fn probabilistic_enhance(input: &str) -> String {
// Probabilistic enhancement
format!("Enhanced: {}", input)
}
"#
.to_string(),
);
Ok(files)
}
fn validate_context(&self, ctx: &AgentContext) -> Result<()> {
if ctx.name.is_empty() {
anyhow::bail!("Agent name is required");
}
if ctx.deterministic_core.is_none() || ctx.probabilistic_wrapper.is_none() {
anyhow::bail!("Hybrid agents require both core and wrapper specifications");
}
Ok(())
}
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::scaffold::agent::templates::AgentTemplate;
#[test]
fn test_registry_creation() {
let registry = TemplateRegistry::new();
assert!(registry.has_template("mcp-server"));
assert!(registry.has_template("state-machine"));
assert!(registry.has_template("calculator"));
assert!(registry.has_template("hybrid"));
}
#[test]
fn test_list_available() {
let registry = TemplateRegistry::new();
let templates = registry.list_available();
assert!(templates.contains(&"mcp-server".to_string()));
assert!(templates.contains(&"state-machine".to_string()));
assert_eq!(templates.len(), 4);
}
#[test]
fn test_get_template() {
let registry = TemplateRegistry::new();
let result = registry.get(&AgentTemplate::MCPToolServer);
assert!(result.is_ok());
let result = registry.get(&AgentTemplate::StateMachineWorkflow);
assert!(result.is_ok());
}
#[test]
fn test_register_custom() {
let mut registry = TemplateRegistry::new();
registry.register_custom("my-template", "/path/to/template");
assert!(registry.has_template("my-template"));
}
#[test]
fn test_register_remote() {
let mut registry = TemplateRegistry::new();
let url = Url::parse("https://example.com/template").unwrap();
registry.register_remote("remote-template", url);
assert!(registry.has_template("remote-template"));
}
#[test]
fn test_template_info() {
let registry = TemplateRegistry::new();
let info = registry.get_template_info("mcp-server");
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.name, "mcp-server");
assert!(matches!(info.source, TemplateSource::Builtin));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}