use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::core::config::SandboxConfig as ConfigSandboxConfig;
use crate::core::{ConfigValidator, PluginError, PluginResult, PluginSystemConfig, SystemConfig};
#[derive(Debug, Clone)]
pub enum ConfigSource {
File(PathBuf),
Json(String),
Toml(String),
Yaml(String),
Environment { prefix: String },
Multiple(Vec<ConfigSource>),
}
#[derive(Debug, Clone)]
pub struct LoadOptions {
pub validate: bool,
pub use_defaults: bool,
pub allow_missing: bool,
pub expand_env_vars: bool,
}
impl Default for LoadOptions {
fn default() -> Self {
Self {
validate: true,
use_defaults: true,
allow_missing: false,
expand_env_vars: true,
}
}
}
#[derive(Debug)]
pub struct ConfigLoader {
validator: ConfigValidator,
options: LoadOptions,
}
impl ConfigLoader {
pub fn new() -> Self {
Self {
validator: ConfigValidator::new(),
options: LoadOptions::default(),
}
}
pub fn with_options(options: LoadOptions) -> Self {
Self {
validator: ConfigValidator::new(),
options,
}
}
pub fn with_validator(validator: ConfigValidator) -> Self {
Self {
validator,
options: LoadOptions::default(),
}
}
pub async fn load(&self, source: ConfigSource) -> PluginResult<PluginSystemConfig> {
let mut config = if self.options.use_defaults {
PluginSystemConfig::default()
} else {
PluginSystemConfig {
system: SystemConfig {
max_parallel_plugins: 1, workspace: None,
debug: false,
plugin_directories: vec![],
max_execution_timeout: None,
enable_sandbox: false,
sandbox: Default::default(),
},
plugins: HashMap::new(),
global_permissions: vec![],
autoload_plugins: vec![],
}
};
let loaded_config = self.load_from_source(source).await?;
self.merge_configs(&mut config, loaded_config)?;
if self.options.expand_env_vars {
self.expand_environment_variables(&mut config)?;
}
if self.options.validate {
self.validator.validate_system_config(&config)?;
}
Ok(config)
}
fn load_from_source(
&self,
source: ConfigSource,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = PluginResult<PluginSystemConfig>> + '_>>
{
Box::pin(async move {
match source {
ConfigSource::File(path) => self.load_from_file(&path).await,
ConfigSource::Json(json_str) => self.load_from_json(&json_str),
ConfigSource::Toml(toml_str) => self.load_from_toml(&toml_str),
ConfigSource::Yaml(yaml_str) => self.load_from_yaml(&yaml_str),
ConfigSource::Environment { prefix } => self.load_from_environment(&prefix),
ConfigSource::Multiple(sources) => {
let mut merged_config = PluginSystemConfig::default();
for source in sources {
let source_config = self.load_from_source(source).await?;
self.merge_configs(&mut merged_config, source_config)?;
}
Ok(merged_config)
}
}
})
}
async fn load_from_file(&self, path: &Path) -> PluginResult<PluginSystemConfig> {
if !path.exists() {
if self.options.allow_missing {
return Ok(PluginSystemConfig::default());
} else {
return Err(PluginError::ConfigurationError(format!(
"Configuration file not found: {}",
path.display()
)));
}
}
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
PluginError::ConfigurationError(format!(
"Failed to read configuration file {}: {}",
path.display(),
e
))
})?;
match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => self.load_from_json(&content),
Some("toml") => self.load_from_toml(&content),
Some("yaml") | Some("yml") => self.load_from_yaml(&content),
_ => {
self.load_from_json(&content)
.or_else(|_| self.load_from_toml(&content))
.or_else(|_| self.load_from_yaml(&content))
.map_err(|_| {
PluginError::ConfigurationError(format!(
"Unable to parse configuration file {} (tried JSON, TOML, and YAML)",
path.display()
))
})
}
}
}
fn load_from_json(&self, json_str: &str) -> PluginResult<PluginSystemConfig> {
serde_json::from_str(json_str).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to parse JSON configuration: {e}"))
})
}
fn load_from_toml(&self, toml_str: &str) -> PluginResult<PluginSystemConfig> {
toml::from_str(toml_str).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to parse TOML configuration: {e}"))
})
}
fn load_from_yaml(&self, yaml_str: &str) -> PluginResult<PluginSystemConfig> {
serde_yaml::from_str(yaml_str).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to parse YAML configuration: {e}"))
})
}
fn load_from_environment(&self, prefix: &str) -> PluginResult<PluginSystemConfig> {
let mut config = PluginSystemConfig::default();
if let Ok(max_parallel) = std::env::var(format!("{prefix}_MAX_PARALLEL_PLUGINS")) {
config.system.max_parallel_plugins = max_parallel.parse().map_err(|e| {
PluginError::ConfigurationError(format!(
"Invalid value for {prefix}_MAX_PARALLEL_PLUGINS: {e}"
))
})?;
}
if let Ok(debug) = std::env::var(format!("{prefix}_DEBUG")) {
config.system.debug = debug.parse().unwrap_or(false);
}
if let Ok(workspace) = std::env::var(format!("{prefix}_WORKSPACE")) {
config.system.workspace = Some(PathBuf::from(workspace));
}
if let Ok(enable_sandbox) = std::env::var(format!("{prefix}_ENABLE_SANDBOX")) {
config.system.enable_sandbox = enable_sandbox.parse().unwrap_or(true);
}
if let Ok(plugin_dirs) = std::env::var(format!("{prefix}_PLUGIN_DIRECTORIES")) {
config.system.plugin_directories = plugin_dirs
.split(':')
.map(|s| PathBuf::from(s.trim()))
.collect();
}
Ok(config)
}
fn merge_configs(
&self,
target: &mut PluginSystemConfig,
source: PluginSystemConfig,
) -> PluginResult<()> {
self.merge_system_config(&mut target.system, source.system);
for (name, plugin_config) in source.plugins {
target.plugins.insert(name, plugin_config);
}
target.global_permissions.extend(source.global_permissions);
target.autoload_plugins.extend(source.autoload_plugins);
Ok(())
}
fn merge_system_config(&self, target: &mut SystemConfig, source: SystemConfig) {
if source.max_parallel_plugins != SystemConfig::default().max_parallel_plugins {
target.max_parallel_plugins = source.max_parallel_plugins;
}
if source.workspace.is_some() {
target.workspace = source.workspace;
}
if source.debug != SystemConfig::default().debug {
target.debug = source.debug;
}
if !source.plugin_directories.is_empty() {
target.plugin_directories.extend(source.plugin_directories);
}
if source.max_execution_timeout.is_some() {
target.max_execution_timeout = source.max_execution_timeout;
}
if source.enable_sandbox != SystemConfig::default().enable_sandbox {
target.enable_sandbox = source.enable_sandbox;
}
self.merge_sandbox_config(&mut target.sandbox, source.sandbox);
}
fn merge_sandbox_config(&self, target: &mut ConfigSandboxConfig, source: ConfigSandboxConfig) {
if source.temp_base_directory.is_some() {
target.temp_base_directory = source.temp_base_directory;
}
if source.max_memory.is_some() {
target.max_memory = source.max_memory;
}
if source.max_execution_time.is_some() {
target.max_execution_time = source.max_execution_time;
}
if !source.allowed_env_vars.is_empty() {
target.allowed_env_vars = source.allowed_env_vars;
}
if source.network_isolation != ConfigSandboxConfig::default().network_isolation {
target.network_isolation = source.network_isolation;
}
}
fn expand_environment_variables(&self, config: &mut PluginSystemConfig) -> PluginResult<()> {
if let Some(workspace) = &config.system.workspace {
config.system.workspace = Some(self.expand_env_in_path(workspace)?);
}
config.system.plugin_directories = config
.system
.plugin_directories
.iter()
.map(|path| self.expand_env_in_path(path))
.collect::<Result<Vec<_>, _>>()?;
if let Some(temp_base) = &config.system.sandbox.temp_base_directory {
config.system.sandbox.temp_base_directory = Some(self.expand_env_in_path(temp_base)?);
}
Ok(())
}
fn expand_env_in_path(&self, path: &Path) -> PluginResult<PathBuf> {
let path_str = path.to_string_lossy();
let expanded = shellexpand::full(&path_str).map_err(|e| {
PluginError::ConfigurationError(format!(
"Failed to expand environment variables in path '{path_str}': {e}"
))
})?;
Ok(PathBuf::from(expanded.as_ref()))
}
pub async fn save(&self, config: &PluginSystemConfig, path: &Path) -> PluginResult<()> {
let content = match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => serde_json::to_string_pretty(config).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to serialize to JSON: {e}"))
})?,
Some("toml") => toml::to_string_pretty(config).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to serialize to TOML: {e}"))
})?,
Some("yaml") | Some("yml") => serde_yaml::to_string(config).map_err(|e| {
PluginError::ConfigurationError(format!("Failed to serialize to YAML: {e}"))
})?,
_ => {
return Err(PluginError::ConfigurationError(format!(
"Unsupported file format for path: {}",
path.display()
)));
}
};
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
PluginError::ConfigurationError(format!(
"Failed to create directory {}: {}",
parent.display(),
e
))
})?;
}
tokio::fs::write(path, content).await.map_err(|e| {
PluginError::ConfigurationError(format!(
"Failed to write configuration to {}: {}",
path.display(),
e
))
})
}
pub fn validator(&self) -> &ConfigValidator {
&self.validator
}
pub fn validator_mut(&mut self) -> &mut ConfigValidator {
&mut self.validator
}
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_load_from_json_string() {
let loader = ConfigLoader::new();
let json_config = r#"
{
"system": {
"max_parallel_plugins": 8,
"debug": true
},
"plugins": {
"test-plugin": {
"enabled": true,
"priority": 10
}
}
}
"#;
let config = loader
.load(ConfigSource::Json(json_config.to_string()))
.await
.unwrap();
assert_eq!(config.system.max_parallel_plugins, 8);
assert!(config.system.debug);
assert!(config.plugins.contains_key("test-plugin"));
}
#[tokio::test]
async fn test_load_from_toml_string() {
let loader = ConfigLoader::new();
let toml_config = r#"
[system]
max_parallel_plugins = 6
debug = false
[plugins.test-plugin]
enabled = true
priority = 5
"#;
let config = loader
.load(ConfigSource::Toml(toml_config.to_string()))
.await
.unwrap();
assert_eq!(config.system.max_parallel_plugins, 6);
assert!(!config.system.debug);
assert!(config.plugins.contains_key("test-plugin"));
}
#[tokio::test]
async fn test_load_from_yaml_string() {
let loader = ConfigLoader::new();
let yaml_config = r#"
system:
max_parallel_plugins: 12
debug: true
plugins:
test-plugin:
enabled: true
priority: 15
"#;
let config = loader
.load(ConfigSource::Yaml(yaml_config.to_string()))
.await
.unwrap();
assert_eq!(config.system.max_parallel_plugins, 12);
assert!(config.system.debug);
assert!(config.plugins.contains_key("test-plugin"));
}
#[tokio::test]
async fn test_load_from_environment() {
let loader = ConfigLoader::new();
std::env::set_var("TEST_MAX_PARALLEL_PLUGINS", "16");
std::env::set_var("TEST_DEBUG", "true");
std::env::set_var("TEST_ENABLE_SANDBOX", "false");
let config = loader
.load(ConfigSource::Environment {
prefix: "TEST".to_string(),
})
.await
.unwrap();
assert_eq!(config.system.max_parallel_plugins, 16);
assert!(config.system.debug);
assert!(!config.system.enable_sandbox);
std::env::remove_var("TEST_MAX_PARALLEL_PLUGINS");
std::env::remove_var("TEST_DEBUG");
std::env::remove_var("TEST_ENABLE_SANDBOX");
}
#[tokio::test]
async fn test_load_from_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("test_config.json");
let json_config = serde_json::json!({
"system": {
"max_parallel_plugins": 20,
"debug": true
},
"plugins": {
"file-plugin": {
"enabled": true,
"priority": 100
}
}
});
tokio::fs::write(
&config_path,
serde_json::to_string_pretty(&json_config).unwrap(),
)
.await
.unwrap();
let loader = ConfigLoader::new();
let config = loader.load(ConfigSource::File(config_path)).await.unwrap();
assert_eq!(config.system.max_parallel_plugins, 20);
assert!(config.system.debug);
assert!(config.plugins.contains_key("file-plugin"));
}
#[tokio::test]
async fn test_config_merging() {
let loader = ConfigLoader::new();
let sources = vec![
ConfigSource::Json(
r#"
{
"system": { "max_parallel_plugins": 4 },
"plugins": { "plugin-a": { "enabled": true } }
}
"#
.to_string(),
),
ConfigSource::Json(
r#"
{
"system": { "debug": true },
"plugins": { "plugin-b": { "enabled": false } }
}
"#
.to_string(),
),
];
let config = loader.load(ConfigSource::Multiple(sources)).await.unwrap();
assert_eq!(config.system.max_parallel_plugins, 4);
assert!(config.system.debug);
assert!(config.plugins.contains_key("plugin-a"));
assert!(config.plugins.contains_key("plugin-b"));
}
#[tokio::test]
async fn test_save_configuration() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("saved_config.json");
let mut config = PluginSystemConfig::default();
config.system.max_parallel_plugins = 42;
config.system.debug = true;
let loader = ConfigLoader::new();
loader.save(&config, &config_path).await.unwrap();
let loaded_config = loader.load(ConfigSource::File(config_path)).await.unwrap();
assert_eq!(loaded_config.system.max_parallel_plugins, 42);
assert!(loaded_config.system.debug);
}
#[tokio::test]
async fn test_load_options() {
let loader = ConfigLoader::with_options(LoadOptions {
validate: false,
use_defaults: false,
allow_missing: true,
expand_env_vars: false,
});
let non_existent_path = PathBuf::from("/non/existent/config.json");
let config = loader
.load(ConfigSource::File(non_existent_path))
.await
.unwrap();
assert_eq!(config.system.max_parallel_plugins, 1); }
}