use crate::config_lib::{ConfigError, GgenConfig, Result};
use star_toml::Validate;
use std::collections::HashSet;
pub struct ConfigValidator<'a> {
config: &'a GgenConfig,
errors: Vec<String>,
}
impl<'a> ConfigValidator<'a> {
#[must_use]
pub const fn new(config: &'a GgenConfig) -> Self {
Self {
config,
errors: Vec::new(),
}
}
pub fn validate(config: &'a GgenConfig) -> Result<()> {
config.check().map_err(|errs| {
ConfigError::Validation(crate::config_lib::error::format_star_toml_errors(&errs))
})
}
fn validate_all(&mut self) -> Result<()> {
if let Err(errs) = self.config.check() {
for err in errs.errors() {
let formatted = crate::config_lib::error::format_single_star_toml_error(err);
self.errors.push(formatted);
}
}
if self.errors.is_empty() {
Ok(())
} else {
Err(ConfigError::Validation(self.errors.join("; ")))
}
}
fn validate_project(&mut self) {
let project = &self.config.project;
if project.name.is_empty() {
self.errors.push("Project name cannot be empty".to_string());
}
if !is_valid_version(&project.version) {
self.errors.push(format!(
"Invalid version format: '{}'. Expected semver format (e.g., 1.0.0)",
project.version
));
}
}
fn validate_ai(&mut self) {
if let Some(ai) = &self.config.ai {
let valid_providers = ["openai", "ollama", "anthropic", "cohere", "huggingface"];
if !valid_providers.contains(&ai.provider.as_str()) {
self.errors.push(format!(
"Unknown AI provider: '{}'. Valid providers: {:?}",
ai.provider, valid_providers
));
}
if !(0.0..=1.0).contains(&ai.temperature) {
self.errors.push(format!(
"AI temperature must be between 0.0 and 1.0, got {}",
ai.temperature
));
}
if ai.max_tokens == 0 {
self.errors
.push("AI max_tokens must be greater than 0".to_string());
}
if ai.timeout == 0 {
self.errors
.push("AI timeout must be greater than 0".to_string());
}
if let Some(validation) = &ai.validation {
if !(0.0..=1.0).contains(&validation.quality_threshold) {
self.errors.push(format!(
"AI validation quality_threshold must be between 0.0 and 1.0, got {}",
validation.quality_threshold
));
}
}
}
}
fn validate_templates(&mut self) {
if let Some(templates) = &self.config.templates {
if let Some(dir) = &templates.directory {
if dir.is_empty() {
self.errors
.push("Templates directory cannot be empty".to_string());
}
}
}
}
const fn validate_security() {
}
fn validate_performance(&mut self) {
if let Some(perf) = &self.config.performance {
if perf.parallel_execution && perf.max_workers == 0 {
self.errors.push(
"Performance max_workers must be greater than 0 when parallel_execution is enabled"
.to_string(),
);
}
if let Some(cache_size) = &perf.cache_size {
if !is_valid_size_format(cache_size) {
self.errors.push(format!(
"Invalid cache_size format: '{cache_size}'. Expected format like '1GB', '512MB'"
));
}
}
}
}
fn validate_logging(&mut self) {
if let Some(logging) = &self.config.logging {
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&logging.level.to_lowercase().as_str()) {
self.errors.push(format!(
"Invalid log level: '{}'. Valid levels: {:?}",
logging.level, valid_levels
));
}
let valid_formats = ["json", "text", "pretty"];
if !valid_formats.contains(&logging.format.to_lowercase().as_str()) {
self.errors.push(format!(
"Invalid log format: '{}'. Valid formats: {:?}",
logging.format, valid_formats
));
}
}
}
fn validate_mcp(&mut self) {
if let Some(mcp) = &self.config.mcp {
if let Some(transport) = &mcp.transport {
let valid_transports = ["stdio", "http", "websocket"];
if !valid_transports.contains(&transport.transport_type.as_str()) {
self.errors.push(format!(
"Invalid MCP transport type: '{}'. Valid types: {:?}",
transport.transport_type, valid_transports
));
}
if let Some(port) = transport.port {
if port == 0 {
self.errors.push(format!(
"Invalid MCP port: {port}. Must be between 1 and 65535"
));
}
}
}
if mcp.tool_timeout_ms == 0 {
self.errors
.push("MCP tool_timeout_ms must be greater than 0".to_string());
}
if mcp.max_concurrent_requests == 0 {
self.errors
.push("MCP max_concurrent_requests must be greater than 0".to_string());
}
}
}
fn validate_a2a(&mut self) {
if let Some(a2a) = &self.config.a2a {
if let Some(transport) = &a2a.transport {
let valid_transports = ["memory", "http", "websocket", "amqp"];
if !valid_transports.contains(&transport.transport_type.as_str()) {
self.errors.push(format!(
"Invalid A2A transport type: '{}'. Valid types: {:?}",
transport.transport_type, valid_transports
));
}
if let Some(port) = transport.port {
if port == 0 {
self.errors.push(format!(
"Invalid A2A port: {port}. Must be between 1 and 65535"
));
}
}
}
if let Some(orchestration) = &a2a.orchestration {
let valid_modes = ["centralized", "decentralized", "hierarchical"];
if !valid_modes.contains(&orchestration.mode.as_str()) {
self.errors.push(format!(
"Invalid A2A orchestration mode: '{}'. Valid modes: {:?}",
orchestration.mode, valid_modes
));
}
if orchestration.consensus_enabled {
if let Some(algorithm) = &orchestration.consensus_algorithm {
let valid_algorithms = ["raft", "pbft", "naive"];
if !valid_algorithms.contains(&algorithm.as_str()) {
self.errors.push(format!(
"Invalid A2A consensus algorithm: '{algorithm}'. Valid: {valid_algorithms:?}"
));
}
}
}
}
}
}
}
fn is_valid_version(version: &str) -> bool {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return false;
}
parts.iter().all(|part| part.parse::<u32>().is_ok())
}
fn is_valid_size_format(size: &str) -> bool {
let size = size.to_uppercase();
let valid_suffixes = ["B", "KB", "MB", "GB", "TB"];
valid_suffixes.iter().any(|suffix| {
size.strip_suffix(suffix)
.is_some_and(|num_str| num_str.parse::<u32>().is_ok())
})
}
#[allow(dead_code)]
fn has_duplicates<T: Eq + std::hash::Hash>(items: &[T]) -> bool {
let mut seen = HashSet::new();
items.iter().any(|item| !seen.insert(item))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::config_lib::{ConfigLoader, ProjectConfig};
#[test]
fn test_valid_minimal_config() {
let config = GgenConfig {
project: ProjectConfig {
name: "test".to_string(),
version: "1.0.0".to_string(),
description: None,
authors: None,
license: None,
repository: None,
},
..Default::default()
};
assert!(ConfigValidator::validate(&config).is_ok());
}
#[test]
fn test_invalid_empty_name() {
let config = GgenConfig {
project: ProjectConfig {
name: String::new(),
version: "1.0.0".to_string(),
description: None,
authors: None,
license: None,
repository: None,
},
..Default::default()
};
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_invalid_version() {
let config = GgenConfig {
project: ProjectConfig {
name: "test".to_string(),
version: "invalid".to_string(),
description: None,
authors: None,
license: None,
repository: None,
},
..Default::default()
};
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_version_validation() {
assert!(is_valid_version("1.0.0"));
assert!(is_valid_version("0.1.0"));
assert!(is_valid_version("10.20.30"));
assert!(!is_valid_version("1.0"));
assert!(!is_valid_version("invalid"));
assert!(!is_valid_version("1.0.0.0"));
}
#[test]
fn test_size_format_validation() {
assert!(is_valid_size_format("1GB"));
assert!(is_valid_size_format("512MB"));
assert!(is_valid_size_format("100kb"));
assert!(!is_valid_size_format("invalid"));
assert!(!is_valid_size_format("GB"));
assert!(!is_valid_size_format("100"));
}
#[test]
fn test_validate_ai_temperature() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[ai]
provider = "openai"
model = "gpt-4"
temperature = 1.5
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_validate_log_level() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[logging]
level = "invalid"
format = "json"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_validate_mcp_transport_type() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[mcp]
enabled = true
[mcp.transport]
transport_type = "invalid"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_validate_a2a_orchestration_mode() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[a2a]
enabled = true
[a2a.orchestration]
mode = "invalid"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_err());
}
#[test]
fn test_valid_mcp_config() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[mcp]
enabled = true
name = "test-mcp"
version = "0.1.0"
[mcp.transport]
transport_type = "stdio"
[mcp.zai]
enabled = true
provider_url = "http://localhost:8080"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_ok());
}
#[test]
fn test_valid_a2a_config() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[a2a]
enabled = true
agent_id = "agent-001"
agent_name = "TestAgent"
agent_type = "coordinator"
[a2a.transport]
transport_type = "memory"
[a2a.orchestration]
mode = "decentralized"
consensus_enabled = true
consensus_algorithm = "raft"
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert!(ConfigValidator::validate(&config).is_ok());
}
#[test]
fn test_env_zai_override() {
let toml = r#"
[project]
name = "test"
version = "1.0.0"
[ai]
provider = "anthropic"
model = "claude-3-opus-20240229"
[env.zai]
"ai.provider" = "zai"
"ai.model" = "zai-chat"
"mcp.enabled" = true
"#;
let config = ConfigLoader::from_str(toml).unwrap();
assert_eq!(config.ai.as_ref().unwrap().provider, "anthropic");
assert_eq!(config.ai.as_ref().unwrap().model, "claude-3-opus-20240229");
let mut config_with_zai = ConfigLoader::from_str(toml).unwrap();
if let Some(ai_config) = config_with_zai.ai.as_mut() {
ai_config.provider = "zai".to_string();
ai_config.model = "zai-chat".to_string();
}
if config_with_zai.mcp.is_none() {
config_with_zai.mcp = Some(crate::config_lib::schema::McpConfig {
name: None,
version: None,
tool_timeout_ms: 30000,
max_concurrent_requests: 100,
transport: None,
tools: None,
zai: None,
enabled: true,
discovery: None,
});
} else if let Some(mcp) = config_with_zai.mcp.as_mut() {
mcp.enabled = true;
}
assert_eq!(config_with_zai.ai.as_ref().unwrap().provider, "zai");
assert_eq!(config_with_zai.ai.as_ref().unwrap().model, "zai-chat");
}
}