use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::core::security::Permission;
use crate::core::{PluginError, PluginResult};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct PluginSystemConfig {
pub system: SystemConfig,
pub plugins: HashMap<String, PluginConfig>,
pub global_permissions: Vec<Permission>,
pub autoload_plugins: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SystemConfig {
pub max_parallel_plugins: usize,
pub workspace: Option<PathBuf>,
pub debug: bool,
pub plugin_directories: Vec<PathBuf>,
pub max_execution_timeout: Option<u64>,
pub enable_sandbox: bool,
pub sandbox: SandboxConfig,
}
impl Default for SystemConfig {
fn default() -> Self {
Self {
max_parallel_plugins: 4,
workspace: None,
debug: false,
plugin_directories: vec![],
max_execution_timeout: Some(300), enable_sandbox: true,
sandbox: SandboxConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SandboxConfig {
pub temp_base_directory: Option<PathBuf>,
pub max_memory: Option<u64>,
pub max_execution_time: Option<u64>,
pub allowed_env_vars: Vec<String>,
pub network_isolation: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
temp_base_directory: None,
max_memory: Some(128 * 1024 * 1024), max_execution_time: Some(300), allowed_env_vars: vec!["PATH".to_string(), "HOME".to_string(), "USER".to_string()],
network_isolation: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PluginConfig {
pub enabled: bool,
pub config: serde_json::Value,
pub permissions: Vec<Permission>,
pub priority: i32,
pub sandbox_override: Option<SandboxConfig>,
pub dependencies: Vec<String>,
pub optional_dependencies: Vec<String>,
pub retry: RetryConfig,
}
impl Default for PluginConfig {
fn default() -> Self {
Self {
enabled: true,
config: serde_json::json!({}),
permissions: vec![],
priority: 0,
sandbox_override: None,
dependencies: vec![],
optional_dependencies: vec![],
retry: RetryConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RetryConfig {
pub max_attempts: u32,
pub delay_ms: u64,
pub backoff_multiplier: f64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
delay_ms: 1000, backoff_multiplier: 2.0,
max_delay_ms: 30000, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
pub suggestion: Option<String>,
}
impl ValidationError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
suggestion: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
}
#[derive(Debug, Default)]
pub struct ConfigValidator {
plugin_schemas: HashMap<String, serde_json::Value>,
}
impl ConfigValidator {
pub fn new() -> Self {
Self::default()
}
pub fn register_plugin_schema(&mut self, plugin_name: String, schema: serde_json::Value) {
self.plugin_schemas.insert(plugin_name, schema);
}
pub fn validate_system_config(&self, config: &PluginSystemConfig) -> PluginResult<()> {
let mut errors = Vec::new();
if let Err(system_errors) = self.validate_system_settings(&config.system) {
errors.extend(
system_errors
.into_iter()
.map(|e| format!("system.{}: {}", e.field, e.message)),
);
}
for (plugin_name, plugin_config) in &config.plugins {
if let Err(plugin_errors) = self.validate_plugin_config(plugin_name, plugin_config) {
errors.extend(
plugin_errors
.into_iter()
.map(|e| format!("plugins.{}.{}: {}", plugin_name, e.field, e.message)),
);
}
}
if let Err(dep_errors) = self.validate_plugin_dependencies(config) {
errors.extend(
dep_errors
.into_iter()
.map(|e| format!("dependencies.{}: {}", e.field, e.message)),
);
}
if errors.is_empty() {
Ok(())
} else {
Err(PluginError::ConfigurationError(format!(
"Configuration validation failed:\n{}",
errors.join("\n")
)))
}
}
fn validate_system_settings(&self, config: &SystemConfig) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if config.max_parallel_plugins == 0 {
errors.push(
ValidationError::new("max_parallel_plugins", "Must be greater than 0")
.with_suggestion("Set to a value like 4 or 8"),
);
}
for (i, dir) in config.plugin_directories.iter().enumerate() {
if !dir.exists() {
errors.push(
ValidationError::new(
format!("plugin_directories[{i}]"),
format!("Directory does not exist: {}", dir.display()),
)
.with_suggestion("Create the directory or remove it from the configuration"),
);
}
}
if let Some(timeout) = config.max_execution_timeout {
if timeout == 0 {
errors.push(
ValidationError::new(
"max_execution_timeout",
"Timeout must be greater than 0 seconds",
)
.with_suggestion("Set to a reasonable value like 300 (5 minutes)"),
);
}
}
if let Err(sandbox_errors) = self.validate_sandbox_config(&config.sandbox) {
for error in sandbox_errors {
errors.push(ValidationError::new(
format!("sandbox.{}", error.field),
error.message,
));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_sandbox_config(&self, config: &SandboxConfig) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if let Some(memory) = config.max_memory {
if memory < 1024 * 1024 {
errors.push(
ValidationError::new("max_memory", "Memory limit too low, minimum is 1MB")
.with_suggestion("Set to at least 1048576 bytes (1MB)"),
);
}
}
if let Some(time) = config.max_execution_time {
if time == 0 {
errors.push(ValidationError::new(
"max_execution_time",
"Execution time must be greater than 0 seconds",
));
}
}
if let Some(base_dir) = &config.temp_base_directory {
if !base_dir.exists() {
errors.push(
ValidationError::new(
"temp_base_directory",
format!("Base directory does not exist: {}", base_dir.display()),
)
.with_suggestion(
"Create the directory or remove the setting to use system temp",
),
);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_plugin_config(
&self,
plugin_name: &str,
config: &PluginConfig,
) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if let Some(schema) = self.plugin_schemas.get(plugin_name) {
if let Err(schema_errors) = self.validate_json_against_schema(&config.config, schema) {
errors.extend(schema_errors);
}
}
if let Err(retry_errors) = self.validate_retry_config(&config.retry) {
for error in retry_errors {
errors.push(ValidationError::new(
format!("retry.{}", error.field),
error.message,
));
}
}
if let Some(sandbox_config) = &config.sandbox_override {
if let Err(sandbox_errors) = self.validate_sandbox_config(sandbox_config) {
for error in sandbox_errors {
errors.push(ValidationError::new(
format!("sandbox_override.{}", error.field),
error.message,
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_retry_config(&self, config: &RetryConfig) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if config.backoff_multiplier <= 0.0 {
errors.push(
ValidationError::new("backoff_multiplier", "Must be greater than 0.0")
.with_suggestion("Use a value like 2.0 for exponential backoff"),
);
}
if config.max_delay_ms < config.delay_ms {
errors.push(ValidationError::new(
"max_delay_ms",
"Must be greater than or equal to delay_ms",
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_plugin_dependencies(
&self,
config: &PluginSystemConfig,
) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
for (plugin_name, plugin_config) in &config.plugins {
for dep in &plugin_config.dependencies {
if !config.plugins.contains_key(dep) && !config.autoload_plugins.contains(dep) {
errors.push(
ValidationError::new(
format!("{plugin_name}.dependencies"),
format!("Dependency '{dep}' is not configured"),
)
.with_suggestion(format!(
"Add '{dep}' to plugins configuration or autoload_plugins"
)),
);
}
}
for dep in &plugin_config.optional_dependencies {
if !config.plugins.contains_key(dep) && !config.autoload_plugins.contains(dep) {
eprintln!(
"Warning: Optional dependency '{dep}' for plugin '{plugin_name}' is not configured"
);
}
}
}
if let Err(cycle_error) = self.detect_circular_dependencies(config) {
errors.push(cycle_error);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn detect_circular_dependencies(
&self,
config: &PluginSystemConfig,
) -> Result<(), ValidationError> {
let mut visited = std::collections::HashSet::new();
let mut rec_stack = std::collections::HashSet::new();
for plugin_name in config.plugins.keys() {
if !visited.contains(plugin_name)
&& Self::has_cycle_dfs(plugin_name, config, &mut visited, &mut rec_stack)
{
return Err(ValidationError::new(
"circular_dependency",
format!("Circular dependency detected involving plugin '{plugin_name}'"),
)
.with_suggestion("Review plugin dependencies to remove circular references"));
}
}
Ok(())
}
fn has_cycle_dfs(
plugin: &str,
config: &PluginSystemConfig,
visited: &mut std::collections::HashSet<String>,
rec_stack: &mut std::collections::HashSet<String>,
) -> bool {
visited.insert(plugin.to_string());
rec_stack.insert(plugin.to_string());
if let Some(plugin_config) = config.plugins.get(plugin) {
for dep in &plugin_config.dependencies {
if !visited.contains(dep) {
if Self::has_cycle_dfs(dep, config, visited, rec_stack) {
return true;
}
} else if rec_stack.contains(dep) {
return true;
}
}
}
rec_stack.remove(plugin);
false
}
fn validate_json_against_schema(
&self,
_value: &serde_json::Value,
_schema: &serde_json::Value,
) -> Result<(), Vec<ValidationError>> {
Ok(())
}
pub fn get_validation_errors(&self, config: &PluginSystemConfig) -> Vec<String> {
match self.validate_system_config(config) {
Ok(()) => vec![],
Err(PluginError::ConfigurationError(msg)) => {
msg.lines().map(|line| line.to_string()).collect()
}
Err(e) => vec![e.to_string()],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_configurations() {
let system_config = SystemConfig::default();
assert_eq!(system_config.max_parallel_plugins, 4);
assert!(system_config.enable_sandbox);
let plugin_config = PluginConfig::default();
assert!(plugin_config.enabled);
assert_eq!(plugin_config.priority, 0);
let retry_config = RetryConfig::default();
assert_eq!(retry_config.max_attempts, 3);
assert_eq!(retry_config.delay_ms, 1000);
}
#[test]
fn test_plugin_system_config_serialization() {
let config = PluginSystemConfig::default();
let json_str = serde_json::to_string_pretty(&config).unwrap();
let deserialized: PluginSystemConfig = serde_json::from_str(&json_str).unwrap();
assert_eq!(
config.system.max_parallel_plugins,
deserialized.system.max_parallel_plugins
);
assert_eq!(
config.system.enable_sandbox,
deserialized.system.enable_sandbox
);
}
#[test]
fn test_config_validator_system_validation() {
let validator = ConfigValidator::new();
let valid_config = SystemConfig::default();
assert!(validator.validate_system_settings(&valid_config).is_ok());
let invalid_config = SystemConfig {
max_parallel_plugins: 0,
..Default::default()
};
assert!(validator.validate_system_settings(&invalid_config).is_err());
}
#[test]
fn test_config_validator_sandbox_validation() {
let validator = ConfigValidator::new();
let valid_config = SandboxConfig::default();
assert!(validator.validate_sandbox_config(&valid_config).is_ok());
let invalid_config = SandboxConfig {
max_memory: Some(1000), ..Default::default()
};
assert!(validator.validate_sandbox_config(&invalid_config).is_err());
}
#[test]
fn test_config_validator_plugin_dependencies() {
let validator = ConfigValidator::new();
let mut config = PluginSystemConfig::default();
let plugin_a = PluginConfig::default();
let plugin_b = PluginConfig {
dependencies: vec!["plugin-a".to_string()],
..Default::default()
};
config.plugins.insert("plugin-a".to_string(), plugin_a);
config.plugins.insert("plugin-b".to_string(), plugin_b);
assert!(validator.validate_plugin_dependencies(&config).is_ok());
let plugin_c = PluginConfig {
dependencies: vec!["non-existent".to_string()],
..Default::default()
};
config.plugins.insert("plugin-c".to_string(), plugin_c);
assert!(validator.validate_plugin_dependencies(&config).is_err());
}
#[test]
fn test_circular_dependency_detection() {
let validator = ConfigValidator::new();
let mut config = PluginSystemConfig::default();
let plugin_a = PluginConfig {
dependencies: vec!["plugin-b".to_string()],
..Default::default()
};
let plugin_b = PluginConfig {
dependencies: vec!["plugin-a".to_string()],
..Default::default()
};
config.plugins.insert("plugin-a".to_string(), plugin_a);
config.plugins.insert("plugin-b".to_string(), plugin_b);
assert!(validator.validate_plugin_dependencies(&config).is_err());
}
#[test]
fn test_retry_config_validation() {
let validator = ConfigValidator::new();
let valid_config = RetryConfig::default();
assert!(validator.validate_retry_config(&valid_config).is_ok());
let invalid_config = RetryConfig {
backoff_multiplier: 0.0,
..Default::default()
};
assert!(validator.validate_retry_config(&invalid_config).is_err());
let invalid_config2 = RetryConfig {
delay_ms: 5000,
max_delay_ms: 1000,
..Default::default()
};
assert!(validator.validate_retry_config(&invalid_config2).is_err());
}
#[test]
fn test_validation_error_with_suggestion() {
let error = ValidationError::new("test_field", "Test error message")
.with_suggestion("Try this fix");
assert_eq!(error.field, "test_field");
assert_eq!(error.message, "Test error message");
assert_eq!(error.suggestion, Some("Try this fix".to_string()));
}
}