Configuration Management Technical Specification
This technical specification provides the detailed implementation design for the Configuration Management Service (sindhan-config) Rust crate, showcasing the application of multiple software design patterns to create a robust, extensible, and type-safe configuration system.
Design Patterns Applied
The implementation leverages multiple software design patterns to achieve flexibility, maintainability, and extensibility:
- Builder Pattern: For configuration builders and complex object construction
- Factory Pattern: For parser creation and format detection
- Strategy Pattern: For merge strategies and validation approaches
- Observer Pattern: For configuration change notifications
- Chain of Responsibility Pattern: For validation pipeline and discovery chain
- Adapter Pattern: For external system integration
- Decorator Pattern: For configuration enhancement and transformation
- Singleton Pattern: For global configuration registry
- Template Method Pattern: For configuration loading workflows
- Command Pattern: For configuration operations and undo functionality
Core Type System and Traits
Base Configuration Traits
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use async_trait::async_trait;
/// Core trait defining configuration schema behavior with schema manager integration
/// **Pattern Applied: Template Method Pattern**
pub trait ConfigurationSchema: Send + Sync + 'static {
type ConfigType: for<'de> Deserialize<'de> + Serialize + Clone + Send + Sync + 'static;
fn schema_name() -> &'static str;
fn schema_version() -> &'static str;
fn schema_file_patterns() -> Vec<&'static str> {
vec![
&format!("schemas/{}.schema.toml", Self::schema_name()),
&format!("schemas/{}.schema.yaml", Self::schema_name()),
&format!("schemas/{}.schema.json", Self::schema_name()),
]
}
fn default_configuration() -> Self::ConfigType;
fn validation_rules() -> Vec<Box<dyn ValidationRule<Self::ConfigType>>>;
fn schema_dependencies() -> Vec<&'static str> { vec![] }
fn schema_inheritance() -> Option<&'static str> { None }
/// Template method defining the standard loading workflow
async fn load_configuration(discovery: &ConfigurationDiscovery) -> Result<Self::ConfigType> {
// Step 1: Discover configuration files
let discovered = discovery.discover_files(Self::schema_name()).await?;
// Step 2: Parse discovered files
let parsed = Self::parse_configurations(discovered).await?;
// Step 3: Merge configurations
let merged = Self::merge_configurations(parsed).await?;
// Step 4: Validate final configuration
Self::validate_configuration(&merged).await?;
Ok(merged)
}
async fn parse_configurations(files: Vec<DiscoveredFile>) -> Result<Vec<Self::ConfigType>>;
async fn merge_configurations(configs: Vec<Self::ConfigType>) -> Result<Self::ConfigType>;
async fn validate_configuration(config: &Self::ConfigType) -> Result<()>;
}
/// Trait for configuration validation rules
/// **Pattern Applied: Strategy Pattern**
pub trait ValidationRule<T>: Send + Sync {
fn name(&self) -> &'static str;
fn validate(&self, config: &T) -> Result<ValidationResult>;
fn severity(&self) -> ValidationSeverity;
}
/// Configuration change observer trait
/// **Pattern Applied: Observer Pattern**
#[async_trait]
pub trait ConfigurationObserver: Send + Sync {
async fn on_configuration_changed(&self, change: ConfigurationChange) -> Result<()>;
async fn on_validation_failed(&self, error: ValidationError) -> Result<()>;
async fn on_reload_completed(&self, success: bool) -> Result<()>;
}Configuration Discovery Engine
/// Configuration discovery engine implementing chain of responsibility
/// **Pattern Applied: Chain of Responsibility Pattern**
pub struct ConfigurationDiscovery {
discovery_chain: Vec<Box<dyn DiscoveryHandler>>,
format_factory: Arc<FormatParserFactory>,
schema_manager: Arc<SchemaManager>,
}
/// Schema Manager handles schema discovery, loading, and caching
/// **Pattern Applied: Singleton Pattern + Factory Pattern**
pub struct SchemaManager {
schema_cache: Arc<RwLock<HashMap<String, CachedSchema>>>,
schema_discovery: SchemaDiscoveryEngine,
schema_loader: SchemaLoader,
schema_validator: SchemaValidator,
}
struct CachedSchema {
metadata: SchemaMetadata,
definition: SchemaDefinition,
loaded_at: std::time::Instant,
dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaMetadata {
pub name: String,
pub version: semver::Version,
pub compatible_versions: Vec<semver::VersionReq>,
pub description: String,
pub author: String,
pub created: chrono::DateTime<chrono::Utc>,
pub last_modified: chrono::DateTime<chrono::Utc>,
pub tags: Vec<String>,
pub inheritance: Option<SchemaInheritance>,
pub dependencies: SchemaDependencies,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaInheritance {
pub parent_schema: String,
pub override_mode: OverrideMode,
pub excluded_fields: Vec<String>,
pub included_fields: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDependencies {
pub required_schemas: Vec<String>,
pub optional_schemas: Vec<String>,
pub version_constraints: HashMap<String, semver::VersionReq>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OverrideMode {
DeepMerge,
ShallowMerge,
SelectiveMerge,
Replace,
}
impl ConfigurationDiscovery {
/// **Pattern Applied: Builder Pattern**
pub fn builder() -> ConfigurationDiscoveryBuilder {
ConfigurationDiscoveryBuilder::new()
}
pub async fn discover_files(&self, module_name: &str) -> Result<Vec<DiscoveredFile>> {
// First, load the schema for this module
let schema = self.schema_manager.load_schema(module_name).await?;
// Use schema information to guide discovery
for handler in &self.discovery_chain {
if let Some(files) = handler.discover_with_schema(module_name, &schema).await? {
return Ok(files);
}
}
// Return default configuration if no files found
Ok(vec![DiscoveredFile::default_with_schema(module_name, schema)])
}
pub async fn discover_schema_files(&self, module_name: &str) -> Result<Vec<DiscoveredSchemaFile>> {
let mut discovered_schemas = Vec::new();
// Search in all schema locations
for handler in &self.discovery_chain {
if let Some(schema_files) = handler.discover_schemas(module_name).await? {
discovered_schemas.extend(schema_files);
}
}
Ok(discovered_schemas)
}
}
/// Builder for configuration discovery
/// **Pattern Applied: Builder Pattern**
pub struct ConfigurationDiscoveryBuilder {
discovery_handlers: Vec<Box<dyn DiscoveryHandler>>,
custom_search_paths: Vec<PathBuf>,
format_precedence: Vec<ConfigurationFormat>,
enable_environment_overrides: bool,
enable_hot_reload: bool,
}
impl ConfigurationDiscoveryBuilder {
pub fn new() -> Self {
Self {
discovery_handlers: Vec::new(),
custom_search_paths: Vec::new(),
format_precedence: vec![
ConfigurationFormat::Toml,
ConfigurationFormat::Yaml,
ConfigurationFormat::Json,
],
enable_environment_overrides: true,
enable_hot_reload: false,
}
}
pub fn build(self) -> Result<ConfigurationDiscovery> {
let mut discovery_handlers: Vec<Box<dyn DiscoveryHandler>> = vec![
Box::new(ProjectSchemaDiscoveryHandler::new()),
Box::new(UserSchemaDiscoveryHandler::new()),
Box::new(SystemSchemaDiscoveryHandler::new()),
Box::new(EmbeddedSchemaDiscoveryHandler::new()),
];
// Add custom handlers
discovery_handlers.extend(self.discovery_handlers);
let schema_manager = Arc::new(SchemaManager::new(
SchemaDiscoveryEngine::new(self.custom_search_paths.clone()),
SchemaLoader::new(),
SchemaValidator::new(),
));
Ok(ConfigurationDiscovery {
discovery_chain: discovery_handlers,
format_factory: Arc::new(FormatParserFactory::new()),
schema_manager,
})
}
pub fn add_search_path(mut self, path: PathBuf) -> Self {
self.custom_search_paths.push(path);
self
}
pub fn with_format_precedence(mut self, formats: Vec<ConfigurationFormat>) -> Self {
self.format_precedence = formats;
self
}
pub fn enable_hot_reload(mut self) -> Self {
self.enable_hot_reload = true;
self
}
pub fn with_schema_directory(mut self, schema_dir: PathBuf) -> Self {
self.custom_search_paths.push(schema_dir.join("schemas"));
self
}
pub fn with_embedded_schemas(mut self, embedded: bool) -> Self {
if embedded {
// Add embedded schema handler
}
self
}
pub fn disable_environment_overrides(mut self) -> Self {
self.enable_environment_overrides = false;
self
}
}
// Schema discovery handlers implementing specific search strategies
/// **Pattern Applied: Chain of Responsibility Pattern**
/// Project-level schema discovery handler
pub struct ProjectSchemaDiscoveryHandler {
search_paths: Vec<PathBuf>,
}
impl ProjectSchemaDiscoveryHandler {
pub fn new() -> Self {
Self {
search_paths: vec![
PathBuf::from("./schemas"),
PathBuf::from("./config/schemas"),
PathBuf::from("./.sindhan/schemas"),
],
}
}
}
#[async_trait]
impl SchemaDiscoveryHandler for ProjectSchemaDiscoveryHandler {
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>> {
for base_path in &self.search_paths {
let schema_patterns = vec![
format!("{}.schema.toml", module_name),
format!("{}.schema.yaml", module_name),
format!("{}.schema.yml", module_name),
format!("{}.schema.json", module_name),
];
for pattern in schema_patterns {
let schema_path = base_path.join(&pattern);
if schema_path.exists() {
return Ok(Some(vec![DiscoveredSchemaFile {
path: schema_path,
source: SchemaSource::Project,
format: SchemaFormat::from_extension(&pattern)?,
priority: 1,
}]));
}
}
}
Ok(None)
}
fn priority(&self) -> u8 { 1 }
fn name(&self) -> &'static str { "Project Schema Discovery" }
}
/// User-level schema discovery handler
pub struct UserSchemaDiscoveryHandler {
user_schema_dir: PathBuf,
}
impl UserSchemaDiscoveryHandler {
pub fn new() -> Self {
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
Self {
user_schema_dir: home_dir.join(".sindhan").join("schemas"),
}
}
}
#[async_trait]
impl SchemaDiscoveryHandler for UserSchemaDiscoveryHandler {
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>> {
let schema_patterns = vec![
format!("{}.schema.toml", module_name),
format!("{}.schema.yaml", module_name),
format!("{}.schema.yml", module_name),
format!("{}.schema.json", module_name),
];
for pattern in schema_patterns {
let schema_path = self.user_schema_dir.join(&pattern);
if schema_path.exists() {
return Ok(Some(vec![DiscoveredSchemaFile {
path: schema_path,
source: SchemaSource::User,
format: SchemaFormat::from_extension(&pattern)?,
priority: 2,
}]));
}
}
Ok(None)
}
fn priority(&self) -> u8 { 2 }
fn name(&self) -> &'static str { "User Schema Discovery" }
}
/// System-level schema discovery handler
pub struct SystemSchemaDiscoveryHandler {
system_schema_dir: Option<PathBuf>,
}
impl SystemSchemaDiscoveryHandler {
pub fn new() -> Self {
let system_dir = std::env::var("SINDHAN_SCHEMA_DIR")
.ok()
.map(|dir| PathBuf::from(dir).join("schemas"));
Self {
system_schema_dir: system_dir,
}
}
}
#[async_trait]
impl SchemaDiscoveryHandler for SystemSchemaDiscoveryHandler {
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>> {
if let Some(ref schema_dir) = self.system_schema_dir {
let schema_patterns = vec![
format!("{}.schema.toml", module_name),
format!("{}.schema.yaml", module_name),
format!("{}.schema.yml", module_name),
format!("{}.schema.json", module_name),
];
for pattern in schema_patterns {
let schema_path = schema_dir.join(&pattern);
if schema_path.exists() {
return Ok(Some(vec![DiscoveredSchemaFile {
path: schema_path,
source: SchemaSource::System,
format: SchemaFormat::from_extension(&pattern)?,
priority: 3,
}]));
}
}
}
Ok(None)
}
fn priority(&self) -> u8 { 3 }
fn name(&self) -> &'static str { "System Schema Discovery" }
}
/// Embedded schema discovery handler for built-in schemas
pub struct EmbeddedSchemaDiscoveryHandler {
embedded_registry: Arc<EmbeddedSchemaRegistry>,
}
impl EmbeddedSchemaDiscoveryHandler {
pub fn new() -> Self {
Self {
embedded_registry: Arc::new(EmbeddedSchemaRegistry::default()),
}
}
}
#[async_trait]
impl SchemaDiscoveryHandler for EmbeddedSchemaDiscoveryHandler {
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>> {
if let Some(embedded_schema) = self.embedded_registry.get_schema(module_name) {
return Ok(Some(vec![DiscoveredSchemaFile {
path: PathBuf::from(format!("embedded://{}.schema.toml", module_name)),
source: SchemaSource::Embedded,
format: SchemaFormat::Toml,
priority: 4,
}]));
}
Ok(None)
}
fn priority(&self) -> u8 { 4 }
fn name(&self) -> &'static str { "Embedded Schema Discovery" }
}
/// Registry for embedded schemas compiled into the binary
pub struct EmbeddedSchemaRegistry {
schemas: HashMap<String, &'static str>,
}
impl Default for EmbeddedSchemaRegistry {
fn default() -> Self {
let mut schemas = HashMap::new();
// Built-in schemas for core modules
schemas.insert("global".to_string(), include_str!("../schemas/global.schema.toml"));
schemas.insert("agent-identity".to_string(), include_str!("../schemas/agent-identity.schema.toml"));
schemas.insert("memory-systems".to_string(), include_str!("../schemas/memory-systems.schema.toml"));
schemas.insert("context-management".to_string(), include_str!("../schemas/context-management.schema.toml"));
schemas.insert("environment-awareness".to_string(), include_str!("../schemas/environment-awareness.schema.toml"));
schemas.insert("observability".to_string(), include_str!("../schemas/observability.schema.toml"));
schemas.insert("tools-mcp".to_string(), include_str!("../schemas/tools-mcp.schema.toml"));
schemas.insert("agent-interface".to_string(), include_str!("../schemas/agent-interface.schema.toml"));
Self { schemas }
}
}
impl EmbeddedSchemaRegistry {
pub fn get_schema(&self, module_name: &str) -> Option<&'static str> {
self.schemas.get(module_name).copied()
}
pub fn register_schema(&mut self, module_name: String, schema_content: &'static str) {
self.schemas.insert(module_name, schema_content);
}
}
/// Schema Manager implementation
impl SchemaManager {
pub fn new(
discovery_engine: SchemaDiscoveryEngine,
loader: SchemaLoader,
validator: SchemaValidator,
) -> Self {
Self {
schema_cache: Arc::new(RwLock::new(HashMap::new())),
schema_discovery: discovery_engine,
schema_loader: loader,
schema_validator: validator,
}
}
/// Load a schema for the specified module
pub async fn load_schema(&self, module_name: &str) -> Result<SchemaDefinition> {
let cache_key = format!("{}:latest", module_name);
// Check cache first
{
let cache = self.schema_cache.read().await;
if let Some(cached) = cache.get(&cache_key) {
// Check if cache is still valid (e.g., not expired)
if cached.loaded_at.elapsed() < std::time::Duration::from_secs(300) {
return Ok(cached.definition.clone());
}
}
}
// Discover schema files
let discovered_files = self.schema_discovery.discover_schema_files(module_name).await?;
if discovered_files.is_empty() {
return Err(anyhow::anyhow!("No schema found for module: {}", module_name));
}
// Load the highest priority schema
let primary_schema_file = discovered_files
.iter()
.min_by_key(|file| file.priority)
.unwrap();
let schema_definition = self.schema_loader
.load_schema_file(&primary_schema_file.path)
.await?;
// Validate schema
self.schema_validator.validate_schema(&schema_definition).await?;
// Process inheritance if specified
let final_schema = if let Some(inheritance) = &schema_definition.metadata.inheritance {
self.process_schema_inheritance(&schema_definition, inheritance).await?
} else {
schema_definition
};
// Cache the loaded schema
{
let mut cache = self.schema_cache.write().await;
cache.insert(cache_key, CachedSchema {
metadata: final_schema.metadata.clone(),
definition: final_schema.clone(),
loaded_at: std::time::Instant::now(),
dependencies: final_schema.metadata.dependencies.required_schemas.clone(),
});
}
Ok(final_schema)
}
/// Process schema inheritance by loading and merging parent schemas
async fn process_schema_inheritance(
&self,
child_schema: &SchemaDefinition,
inheritance: &SchemaInheritance,
) -> Result<SchemaDefinition> {
// Load parent schema
let parent_schema = self.load_schema(&inheritance.parent_schema).await?;
// Merge schemas based on override mode
match inheritance.override_mode {
OverrideMode::DeepMerge => {
self.deep_merge_schemas(&parent_schema, child_schema, inheritance).await
},
OverrideMode::ShallowMerge => {
self.shallow_merge_schemas(&parent_schema, child_schema, inheritance).await
},
OverrideMode::SelectiveMerge => {
self.selective_merge_schemas(&parent_schema, child_schema, inheritance).await
},
OverrideMode::Replace => {
Ok(child_schema.clone())
},
}
}
async fn deep_merge_schemas(
&self,
parent: &SchemaDefinition,
child: &SchemaDefinition,
inheritance: &SchemaInheritance,
) -> Result<SchemaDefinition> {
// Implementation for deep merging schemas
// This would recursively merge all schema sections
todo!("Implement deep schema merging")
}
async fn shallow_merge_schemas(
&self,
parent: &SchemaDefinition,
child: &SchemaDefinition,
inheritance: &SchemaInheritance,
) -> Result<SchemaDefinition> {
// Implementation for shallow merging schemas
// This would merge only top-level sections
todo!("Implement shallow schema merging")
}
async fn selective_merge_schemas(
&self,
parent: &SchemaDefinition,
child: &SchemaDefinition,
inheritance: &SchemaInheritance,
) -> Result<SchemaDefinition> {
// Implementation for selective merging schemas
// This would merge only specified fields
todo!("Implement selective schema merging")
}
}
/// Schema-aware discovery handler trait
#[async_trait]
pub trait SchemaDiscoveryHandler: Send + Sync {
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>>;
fn priority(&self) -> u8;
fn name(&self) -> &'static str;
}
/// Discovered schema file information
#[derive(Debug, Clone)]
pub struct DiscoveredSchemaFile {
pub path: PathBuf,
pub source: SchemaSource,
pub format: SchemaFormat,
pub priority: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SchemaSource {
Project,
User,
System,
Embedded,
Remote,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SchemaFormat {
Toml,
Yaml,
Json,
}
impl SchemaFormat {
fn from_extension(filename: &str) -> Result<Self> {
match filename.split('.').last() {
Some("toml") => Ok(SchemaFormat::Toml),
Some("yaml") | Some("yml") => Ok(SchemaFormat::Yaml),
Some("json") => Ok(SchemaFormat::Json),
_ => Err(anyhow::anyhow!("Unsupported schema format")),
}
}
}
/// Complete schema definition structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDefinition {
pub metadata: SchemaMetadata,
pub config_structure: ConfigStructure,
pub validation_rules: ValidationRules,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigStructure {
pub fields: HashMap<String, FieldDefinition>,
pub sections: HashMap<String, SectionDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub field_type: FieldType,
pub required: bool,
pub default_value: Option<serde_json::Value>,
pub validation: Option<FieldValidation>,
pub description: String,
pub sensitive: bool,
pub encrypted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FieldType {
String,
Integer,
Float,
Boolean,
Array(Box<FieldType>),
Object(ConfigStructure),
Enum(Vec<String>),
Duration,
Url,
Path,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldValidation {
pub pattern: Option<String>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
pub custom_validator: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SectionDefinition {
pub required: bool,
pub fields: HashMap<String, FieldDefinition>,
pub subsections: HashMap<String, SectionDefinition>,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRules {
pub field_rules: HashMap<String, Vec<ValidationRule>>,
pub section_rules: HashMap<String, Vec<ValidationRule>>,
pub global_rules: Vec<ValidationRule>,
}
/// Discovery handler trait for chain of responsibility
/// **Pattern Applied: Chain of Responsibility Pattern**
#[async_trait]
pub trait DiscoveryHandler: Send + Sync {
async fn discover(&self, module_name: &str) -> Result<Option<Vec<DiscoveredFile>>>;
async fn discover_with_schema(
&self,
module_name: &str,
schema: &SchemaDefinition
) -> Result<Option<Vec<DiscoveredFile>>> {
// Default implementation falls back to standard discovery
self.discover(module_name).await
}
async fn discover_schemas(&self, module_name: &str) -> Result<Option<Vec<DiscoveredSchemaFile>>> {
// Default implementation returns None for non-schema handlers
Ok(None)
}
fn priority(&self) -> u8;
fn name(&self) -> &'static str;
}
/// Current directory discovery handler
pub struct CurrentDirectoryHandler {
format_precedence: Vec<ConfigurationFormat>,
}
#[async_trait]
impl DiscoveryHandler for CurrentDirectoryHandler {
async fn discover(&self, module_name: &str) -> Result<Option<Vec<DiscoveredFile>>> {
let current_dir = std::env::current_dir()
.context("Failed to get current directory")?;
for format in &self.format_precedence {
let filename = format!("{}.{}", module_name, format.extension());
let config_path = current_dir.join(&filename);
if config_path.exists() {
return Ok(Some(vec![DiscoveredFile {
path: config_path,
format: *format,
source: DiscoverySource::CurrentDirectory,
priority: self.priority(),
}]));
}
}
Ok(None)
}
fn priority(&self) -> u8 { 1 }
fn name(&self) -> &'static str { "CurrentDirectory" }
}
/// Home directory discovery handler
pub struct HomeDirectoryHandler {
format_precedence: Vec<ConfigurationFormat>,
}
#[async_trait]
impl DiscoveryHandler for HomeDirectoryHandler {
async fn discover(&self, module_name: &str) -> Result<Option<Vec<DiscoveredFile>>> {
let home_dir = dirs::home_dir()
.context("Failed to get home directory")?;
let sindhan_dir = home_dir.join(".sindhan");
if !sindhan_dir.exists() {
return Ok(None);
}
for format in &self.format_precedence {
let filename = format!("{}.{}", module_name, format.extension());
let config_path = sindhan_dir.join(&filename);
if config_path.exists() {
return Ok(Some(vec![DiscoveredFile {
path: config_path,
format: *format,
source: DiscoverySource::HomeDirectory,
priority: self.priority(),
}]));
}
}
Ok(None)
}
fn priority(&self) -> u8 { 2 }
fn name(&self) -> &'static str { "HomeDirectory" }
}
/// Environment directory discovery handler
pub struct EnvironmentDirectoryHandler {
format_precedence: Vec<ConfigurationFormat>,
}
#[async_trait]
impl DiscoveryHandler for EnvironmentDirectoryHandler {
async fn discover(&self, module_name: &str) -> Result<Option<Vec<DiscoveredFile>>> {
let env_dir = match std::env::var("SINDHAN_CONFIG_DIR") {
Ok(dir) => PathBuf::from(dir),
Err(_) => return Ok(None),
};
if !env_dir.exists() {
return Ok(None);
}
for format in &self.format_precedence {
let filename = format!("{}.{}", module_name, format.extension());
let config_path = env_dir.join(&filename);
if config_path.exists() {
return Ok(Some(vec![DiscoveredFile {
path: config_path,
format: *format,
source: DiscoverySource::EnvironmentDirectory,
priority: self.priority(),
}]));
}
}
Ok(None)
}
fn priority(&self) -> u8 { 3 }
fn name(&self) -> &'static str { "EnvironmentDirectory" }
}Format Parser Factory
/// Factory for creating format-specific parsers
/// **Pattern Applied: Factory Pattern**
pub struct FormatParserFactory {
parsers: HashMap<ConfigurationFormat, Box<dyn ConfigurationParser>>,
}
impl FormatParserFactory {
pub fn new() -> Self {
let mut parsers: HashMap<ConfigurationFormat, Box<dyn ConfigurationParser>> = HashMap::new();
parsers.insert(ConfigurationFormat::Toml, Box::new(TomlParser::new()));
parsers.insert(ConfigurationFormat::Yaml, Box::new(YamlParser::new()));
parsers.insert(ConfigurationFormat::Json, Box::new(JsonParser::new()));
Self { parsers }
}
pub fn create_parser(&self, format: ConfigurationFormat) -> Result<&dyn ConfigurationParser> {
self.parsers.get(&format)
.map(|p| p.as_ref())
.context(format!("No parser available for format: {:?}", format))
}
/// Register a custom parser
pub fn register_parser(&mut self, format: ConfigurationFormat, parser: Box<dyn ConfigurationParser>) {
self.parsers.insert(format, parser);
}
}
/// Configuration parser trait
/// **Pattern Applied: Strategy Pattern**
#[async_trait]
pub trait ConfigurationParser: Send + Sync {
async fn parse<T>(&self, content: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>;
fn format(&self) -> ConfigurationFormat;
fn can_parse(&self, content: &str) -> bool;
}
/// TOML parser implementation
pub struct TomlParser;
impl TomlParser {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl ConfigurationParser for TomlParser {
async fn parse<T>(&self, content: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>
{
toml::from_str(content)
.context("Failed to parse TOML configuration")
}
fn format(&self) -> ConfigurationFormat {
ConfigurationFormat::Toml
}
fn can_parse(&self, content: &str) -> bool {
// Simple heuristic - check for TOML-specific syntax
content.contains('[') && content.contains('=') && !content.contains('{')
}
}
/// YAML parser implementation
pub struct YamlParser;
impl YamlParser {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl ConfigurationParser for YamlParser {
async fn parse<T>(&self, content: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>
{
serde_yaml::from_str(content)
.context("Failed to parse YAML configuration")
}
fn format(&self) -> ConfigurationFormat {
ConfigurationFormat::Yaml
}
fn can_parse(&self, content: &str) -> bool {
// Simple heuristic - check for YAML-specific syntax
content.contains(':') && (content.contains("---") || content.contains(" "))
}
}
/// JSON parser implementation
pub struct JsonParser;
impl JsonParser {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl ConfigurationParser for JsonParser {
async fn parse<T>(&self, content: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>
{
serde_json::from_str(content)
.context("Failed to parse JSON configuration")
}
fn format(&self) -> ConfigurationFormat {
ConfigurationFormat::Json
}
fn can_parse(&self, content: &str) -> bool {
// Simple heuristic - check for JSON-specific syntax
content.trim().starts_with('{') && content.trim().ends_with('}')
}
}Configuration Merge Strategies
/// Configuration merge strategy trait
/// **Pattern Applied: Strategy Pattern**
pub trait MergeStrategy<T>: Send + Sync {
fn merge(&self, base: T, override_config: T) -> Result<T>;
fn strategy_name(&self) -> &'static str;
fn supports_type(&self) -> bool;
}
/// Deep merge strategy for complex nested configurations
pub struct DeepMergeStrategy;
impl<T> MergeStrategy<T> for DeepMergeStrategy
where
T: Serialize + for<'de> Deserialize<'de> + Clone
{
fn merge(&self, base: T, override_config: T) -> Result<T> {
// Convert to JSON values for deep merging
let mut base_value: serde_json::Value = serde_json::to_value(base)?;
let override_value: serde_json::Value = serde_json::to_value(override_config)?;
self.deep_merge_values(&mut base_value, override_value);
// Convert back to the target type
serde_json::from_value(base_value)
.context("Failed to deserialize merged configuration")
}
fn strategy_name(&self) -> &'static str {
"DeepMerge"
}
fn supports_type(&self) -> bool {
true
}
}
impl DeepMergeStrategy {
fn deep_merge_values(&self, base: &mut serde_json::Value, override_val: serde_json::Value) {
match (base, override_val) {
(serde_json::Value::Object(base_map), serde_json::Value::Object(override_map)) => {
for (key, value) in override_map {
match base_map.entry(key) {
serde_json::map::Entry::Occupied(mut entry) => {
self.deep_merge_values(entry.get_mut(), value);
}
serde_json::map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
}
(base_val, override_val) => {
*base_val = override_val;
}
}
}
}
/// Shallow merge strategy for simple configurations
pub struct ShallowMergeStrategy;
impl<T> MergeStrategy<T> for ShallowMergeStrategy
where
T: Serialize + for<'de> Deserialize<'de> + Clone
{
fn merge(&self, _base: T, override_config: T) -> Result<T> {
// Shallow merge simply returns the override configuration
Ok(override_config)
}
fn strategy_name(&self) -> &'static str {
"ShallowMerge"
}
fn supports_type(&self) -> bool {
true
}
}
/// Array append strategy for configurations with array values
pub struct ArrayAppendStrategy;
impl<T> MergeStrategy<T> for ArrayAppendStrategy
where
T: Serialize + for<'de> Deserialize<'de> + Clone
{
fn merge(&self, base: T, override_config: T) -> Result<T> {
let mut base_value: serde_json::Value = serde_json::to_value(base)?;
let override_value: serde_json::Value = serde_json::to_value(override_config)?;
self.append_arrays(&mut base_value, override_value);
serde_json::from_value(base_value)
.context("Failed to deserialize merged configuration")
}
fn strategy_name(&self) -> &'static str {
"ArrayAppend"
}
fn supports_type(&self) -> bool {
true
}
}
impl ArrayAppendStrategy {
fn append_arrays(&self, base: &mut serde_json::Value, override_val: serde_json::Value) {
match (base, override_val) {
(serde_json::Value::Array(base_array), serde_json::Value::Array(override_array)) => {
base_array.extend(override_array);
}
(serde_json::Value::Object(base_map), serde_json::Value::Object(override_map)) => {
for (key, value) in override_map {
match base_map.entry(key) {
serde_json::map::Entry::Occupied(mut entry) => {
self.append_arrays(entry.get_mut(), value);
}
serde_json::map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
}
(base_val, override_val) => {
*base_val = override_val;
}
}
}
}Configuration Manager
/// Main configuration manager implementing multiple patterns
/// **Pattern Applied: Singleton Pattern, Observer Pattern**
pub struct ConfigurationManager<T: ConfigurationSchema> {
config: Arc<RwLock<T::ConfigType>>,
discovery: ConfigurationDiscovery,
observers: Arc<RwLock<Vec<Arc<dyn ConfigurationObserver>>>>,
merge_strategy: Box<dyn MergeStrategy<T::ConfigType>>,
validator: ValidationPipeline<T::ConfigType>,
hot_reload_enabled: bool,
_file_watcher: Option<tokio::task::JoinHandle<()>>,
}
impl<T: ConfigurationSchema> ConfigurationManager<T> {
/// **Pattern Applied: Builder Pattern**
pub fn builder() -> ConfigurationManagerBuilder<T> {
ConfigurationManagerBuilder::new()
}
pub async fn load_configuration(&self) -> Result<()> {
let config = T::load_configuration(&self.discovery).await?;
// Validate the loaded configuration
let validation_result = self.validator.validate(&config).await?;
if !validation_result.is_valid() {
return Err(anyhow::anyhow!("Configuration validation failed: {:?}", validation_result.errors()));
}
// Update the configuration
let mut config_guard = self.config.write().await;
*config_guard = config.clone();
drop(config_guard);
// Notify observers
self.notify_observers(ConfigurationChange::Loaded { config }).await?;
Ok(())
}
pub async fn get_configuration(&self) -> T::ConfigType {
self.config.read().await.clone()
}
pub async fn update_configuration(&self, new_config: T::ConfigType) -> Result<()> {
// Validate the new configuration
let validation_result = self.validator.validate(&new_config).await?;
if !validation_result.is_valid() {
return Err(anyhow::anyhow!("Configuration validation failed: {:?}", validation_result.errors()));
}
let old_config = {
let mut config_guard = self.config.write().await;
let old = config_guard.clone();
*config_guard = new_config.clone();
old
};
// Notify observers
self.notify_observers(ConfigurationChange::Updated {
old_config,
new_config
}).await?;
Ok(())
}
/// **Pattern Applied: Observer Pattern**
pub async fn add_observer(&self, observer: Arc<dyn ConfigurationObserver>) {
let mut observers = self.observers.write().await;
observers.push(observer);
}
async fn notify_observers(&self, change: ConfigurationChange) -> Result<()> {
let observers = self.observers.read().await;
for observer in observers.iter() {
if let Err(e) = observer.on_configuration_changed(change.clone()).await {
eprintln!("Observer notification failed: {}", e);
}
}
Ok(())
}
async fn start_file_watcher(&self) -> Result<tokio::task::JoinHandle<()>> {
// File watching implementation would go here
// This is a placeholder for the actual file watching logic
Ok(tokio::spawn(async {
// File watching loop
}))
}
}
/// Builder for configuration manager
/// **Pattern Applied: Builder Pattern**
pub struct ConfigurationManagerBuilder<T: ConfigurationSchema> {
discovery_builder: Option<ConfigurationDiscoveryBuilder>,
merge_strategy: Option<Box<dyn MergeStrategy<T::ConfigType>>>,
validation_rules: Vec<Box<dyn ValidationRule<T::ConfigType>>>,
hot_reload_enabled: bool,
initial_observers: Vec<Arc<dyn ConfigurationObserver>>,
}
impl<T: ConfigurationSchema> ConfigurationManagerBuilder<T> {
pub fn new() -> Self {
Self {
discovery_builder: None,
merge_strategy: None,
validation_rules: T::validation_rules(),
hot_reload_enabled: false,
initial_observers: Vec::new(),
}
}
pub fn with_discovery(mut self, discovery_builder: ConfigurationDiscoveryBuilder) -> Self {
self.discovery_builder = Some(discovery_builder);
self
}
pub fn with_merge_strategy(mut self, strategy: Box<dyn MergeStrategy<T::ConfigType>>) -> Self {
self.merge_strategy = Some(strategy);
self
}
pub fn add_validation_rule(mut self, rule: Box<dyn ValidationRule<T::ConfigType>>) -> Self {
self.validation_rules.push(rule);
self
}
pub fn enable_hot_reload(mut self) -> Self {
self.hot_reload_enabled = true;
self
}
pub fn add_observer(mut self, observer: Arc<dyn ConfigurationObserver>) -> Self {
self.initial_observers.push(observer);
self
}
pub async fn build(self) -> Result<ConfigurationManager<T>> {
let discovery = self.discovery_builder
.unwrap_or_else(|| ConfigurationDiscovery::builder())
.build()?;
let merge_strategy = self.merge_strategy
.unwrap_or_else(|| Box::new(DeepMergeStrategy));
let validator = ValidationPipeline::new(self.validation_rules);
let config = Arc::new(RwLock::new(T::default_configuration()));
let observers = Arc::new(RwLock::new(self.initial_observers));
let mut manager = ConfigurationManager {
config,
discovery,
observers,
merge_strategy,
validator,
hot_reload_enabled: self.hot_reload_enabled,
_file_watcher: None,
};
if manager.hot_reload_enabled {
let file_watcher = manager.start_file_watcher().await?;
manager._file_watcher = Some(file_watcher);
}
Ok(manager)
}
}Validation Pipeline
/// Validation pipeline implementing chain of responsibility
/// **Pattern Applied: Chain of Responsibility Pattern**
pub struct ValidationPipeline<T> {
validators: Vec<Box<dyn ValidationRule<T>>>,
}
impl<T> ValidationPipeline<T> {
pub fn new(validators: Vec<Box<dyn ValidationRule<T>>>) -> Self {
Self { validators }
}
pub async fn validate(&self, config: &T) -> Result<ValidationResult> {
let mut results = Vec::new();
let mut has_errors = false;
for validator in &self.validators {
match validator.validate(config) {
Ok(result) => {
if result.severity == ValidationSeverity::Error {
has_errors = true;
}
results.push(result);
}
Err(e) => {
has_errors = true;
results.push(ValidationResult {
rule_name: validator.name().to_string(),
message: format!("Validation rule failed: {}", e),
severity: ValidationSeverity::Error,
field_path: None,
});
}
}
}
Ok(ValidationResult::aggregate(results, !has_errors))
}
pub fn add_validator(&mut self, validator: Box<dyn ValidationRule<T>>) {
self.validators.push(validator);
}
}
/// Example validation rules
pub struct RequiredFieldValidator {
field_name: String,
}
impl RequiredFieldValidator {
pub fn new(field_name: String) -> Self {
Self { field_name }
}
}
impl<T> ValidationRule<T> for RequiredFieldValidator
where
T: Serialize
{
fn name(&self) -> &'static str {
"RequiredField"
}
fn validate(&self, config: &T) -> Result<ValidationResult> {
let value = serde_json::to_value(config)?;
if let Some(obj) = value.as_object() {
if obj.contains_key(&self.field_name) {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Required field '{}' is present", self.field_name),
severity: ValidationSeverity::Info,
field_path: Some(self.field_name.clone()),
})
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Required field '{}' is missing", self.field_name),
severity: ValidationSeverity::Error,
field_path: Some(self.field_name.clone()),
})
}
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: "Configuration is not an object".to_string(),
severity: ValidationSeverity::Error,
field_path: None,
})
}
}
fn severity(&self) -> ValidationSeverity {
ValidationSeverity::Error
}
}
/// Range validation rule
pub struct RangeValidator {
field_name: String,
min_value: f64,
max_value: f64,
}
impl RangeValidator {
pub fn new(field_name: String, min_value: f64, max_value: f64) -> Self {
Self { field_name, min_value, max_value }
}
}
impl<T> ValidationRule<T> for RangeValidator
where
T: Serialize
{
fn name(&self) -> &'static str {
"Range"
}
fn validate(&self, config: &T) -> Result<ValidationResult> {
let value = serde_json::to_value(config)?;
if let Some(obj) = value.as_object() {
if let Some(field_value) = obj.get(&self.field_name) {
if let Some(num) = field_value.as_f64() {
if num >= self.min_value && num <= self.max_value {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Field '{}' is within range [{}, {}]", self.field_name, self.min_value, self.max_value),
severity: ValidationSeverity::Info,
field_path: Some(self.field_name.clone()),
})
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Field '{}' value {} is outside range [{}, {}]", self.field_name, num, self.min_value, self.max_value),
severity: ValidationSeverity::Error,
field_path: Some(self.field_name.clone()),
})
}
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Field '{}' is not a number", self.field_name),
severity: ValidationSeverity::Error,
field_path: Some(self.field_name.clone()),
})
}
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: format!("Field '{}' not found for range validation", self.field_name),
severity: ValidationSeverity::Warning,
field_path: Some(self.field_name.clone()),
})
}
} else {
Ok(ValidationResult {
rule_name: self.name().to_string(),
message: "Configuration is not an object".to_string(),
severity: ValidationSeverity::Error,
field_path: None,
})
}
}
fn severity(&self) -> ValidationSeverity {
ValidationSeverity::Error
}
}Configuration Decorators and Adapters
/// Configuration decorator for adding encryption support
/// **Pattern Applied: Decorator Pattern**
pub struct EncryptedConfigurationDecorator<T: ConfigurationSchema> {
inner: Arc<ConfigurationManager<T>>,
encryption_key: Vec<u8>,
encrypted_fields: Vec<String>,
}
impl<T: ConfigurationSchema> EncryptedConfigurationDecorator<T> {
pub fn new(
inner: Arc<ConfigurationManager<T>>,
encryption_key: Vec<u8>,
encrypted_fields: Vec<String>
) -> Self {
Self {
inner,
encryption_key,
encrypted_fields,
}
}
pub async fn get_configuration(&self) -> Result<T::ConfigType> {
let mut config = self.inner.get_configuration().await;
// Decrypt encrypted fields
self.decrypt_fields(&mut config)?;
Ok(config)
}
pub async fn update_configuration(&self, mut config: T::ConfigType) -> Result<()> {
// Encrypt sensitive fields before storing
self.encrypt_fields(&mut config)?;
self.inner.update_configuration(config).await
}
fn encrypt_fields(&self, config: &mut T::ConfigType) -> Result<()> {
let mut value = serde_json::to_value(&*config)?;
for field_path in &self.encrypted_fields {
if let Some(field_value) = self.get_field_mut(&mut value, field_path) {
if let Some(string_value) = field_value.as_str() {
let encrypted = self.encrypt_string(string_value)?;
*field_value = serde_json::Value::String(encrypted);
}
}
}
*config = serde_json::from_value(value)?;
Ok(())
}
fn decrypt_fields(&self, config: &mut T::ConfigType) -> Result<()> {
let mut value = serde_json::to_value(&*config)?;
for field_path in &self.encrypted_fields {
if let Some(field_value) = self.get_field_mut(&mut value, field_path) {
if let Some(string_value) = field_value.as_str() {
let decrypted = self.decrypt_string(string_value)?;
*field_value = serde_json::Value::String(decrypted);
}
}
}
*config = serde_json::from_value(value)?;
Ok(())
}
fn get_field_mut<'a>(&self, value: &'a mut serde_json::Value, path: &str) -> Option<&'a mut serde_json::Value> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = value;
for part in parts {
current = current.get_mut(part)?;
}
Some(current)
}
fn encrypt_string(&self, input: &str) -> Result<String> {
// Placeholder encryption - in real implementation, use proper encryption
let encrypted = base64::encode(format!("ENCRYPTED:{}", input));
Ok(encrypted)
}
fn decrypt_string(&self, input: &str) -> Result<String> {
// Placeholder decryption - in real implementation, use proper decryption
let decoded = base64::decode(input)?;
let decoded_str = String::from_utf8(decoded)?;
if let Some(stripped) = decoded_str.strip_prefix("ENCRYPTED:") {
Ok(stripped.to_string())
} else {
Ok(input.to_string()) // Return as-is if not encrypted
}
}
}
/// Adapter for external configuration sources
/// **Pattern Applied: Adapter Pattern**
pub struct ExternalConfigurationAdapter {
source_url: String,
client: reqwest::Client,
auth_token: Option<String>,
}
impl ExternalConfigurationAdapter {
pub fn new(source_url: String, auth_token: Option<String>) -> Self {
Self {
source_url,
client: reqwest::Client::new(),
auth_token,
}
}
pub async fn fetch_configuration<T>(&self) -> Result<T>
where
T: for<'de> Deserialize<'de>
{
let mut request = self.client.get(&self.source_url);
if let Some(token) = &self.auth_token {
request = request.bearer_auth(token);
}
let response = request.send().await?;
let config: T = response.json().await?;
Ok(config)
}
}
/// External configuration source adapter implementing the discovery handler interface
/// **Pattern Applied: Adapter Pattern**
pub struct ExternalSourceAdapter {
adapter: ExternalConfigurationAdapter,
cache_duration: std::time::Duration,
last_fetch: Arc<RwLock<Option<std::time::Instant>>>,
cached_config: Arc<RwLock<Option<serde_json::Value>>>,
}
#[async_trait]
impl DiscoveryHandler for ExternalSourceAdapter {
async fn discover(&self, module_name: &str) -> Result<Option<Vec<DiscoveredFile>>> {
// Check cache validity
let should_fetch = {
let last_fetch = self.last_fetch.read().await;
match *last_fetch {
Some(time) => time.elapsed() > self.cache_duration,
None => true,
}
};
if should_fetch {
// Fetch fresh configuration
let config: serde_json::Value = self.adapter.fetch_configuration().await?;
// Update cache
{
let mut cached = self.cached_config.write().await;
*cached = Some(config);
}
{
let mut last_fetch = self.last_fetch.write().await;
*last_fetch = Some(std::time::Instant::now());
}
}
// Return cached configuration as a virtual file
let cached_config = self.cached_config.read().await;
if let Some(config) = cached_config.as_ref() {
let content = serde_json::to_string_pretty(config)?;
Ok(Some(vec![DiscoveredFile {
path: PathBuf::from(format!("external://{}", module_name)),
format: ConfigurationFormat::Json,
source: DiscoverySource::External,
priority: 10, // Lower priority than local files
}]))
} else {
Ok(None)
}
}
fn priority(&self) -> u8 { 10 }
fn name(&self) -> &'static str { "ExternalSource" }
}Configuration Commands and Undo Support
/// Command pattern for configuration operations
/// **Pattern Applied: Command Pattern**
pub trait ConfigurationCommand: Send + Sync {
fn execute(&self) -> Result<ConfigurationCommandResult>;
fn undo(&self) -> Result<ConfigurationCommandResult>;
fn description(&self) -> String;
fn command_id(&self) -> String;
}
/// Update configuration command
pub struct UpdateConfigurationCommand<T: ConfigurationSchema> {
manager: Arc<ConfigurationManager<T>>,
new_config: T::ConfigType,
old_config: Option<T::ConfigType>,
command_id: String,
}
impl<T: ConfigurationSchema> UpdateConfigurationCommand<T> {
pub fn new(manager: Arc<ConfigurationManager<T>>, new_config: T::ConfigType) -> Self {
Self {
manager,
new_config,
old_config: None,
command_id: uuid::Uuid::new_v4().to_string(),
}
}
}
impl<T: ConfigurationSchema> ConfigurationCommand for UpdateConfigurationCommand<T> {
fn execute(&self) -> Result<ConfigurationCommandResult> {
// This would need to be made async in a real implementation
// For now, this is a synchronous placeholder
Ok(ConfigurationCommandResult {
command_id: self.command_id.clone(),
success: true,
message: "Configuration updated successfully".to_string(),
timestamp: chrono::Utc::now(),
})
}
fn undo(&self) -> Result<ConfigurationCommandResult> {
if let Some(old_config) = &self.old_config {
// Restore previous configuration
Ok(ConfigurationCommandResult {
command_id: self.command_id.clone(),
success: true,
message: "Configuration restored successfully".to_string(),
timestamp: chrono::Utc::now(),
})
} else {
Err(anyhow::anyhow!("Cannot undo: no previous configuration available"))
}
}
fn description(&self) -> String {
"Update configuration".to_string()
}
fn command_id(&self) -> String {
self.command_id.clone()
}
}
/// Configuration command invoker
/// **Pattern Applied: Command Pattern**
pub struct ConfigurationCommandInvoker {
command_history: Vec<Box<dyn ConfigurationCommand>>,
undo_stack: Vec<Box<dyn ConfigurationCommand>>,
}
impl ConfigurationCommandInvoker {
pub fn new() -> Self {
Self {
command_history: Vec::new(),
undo_stack: Vec::new(),
}
}
pub fn execute_command(&mut self, command: Box<dyn ConfigurationCommand>) -> Result<ConfigurationCommandResult> {
let result = command.execute()?;
if result.success {
self.command_history.push(command);
self.undo_stack.clear(); // Clear redo stack when new command is executed
}
Ok(result)
}
pub fn undo_last_command(&mut self) -> Result<ConfigurationCommandResult> {
if let Some(command) = self.command_history.pop() {
let result = command.undo()?;
if result.success {
self.undo_stack.push(command);
}
Ok(result)
} else {
Err(anyhow::anyhow!("No commands to undo"))
}
}
pub fn redo_last_command(&mut self) -> Result<ConfigurationCommandResult> {
if let Some(command) = self.undo_stack.pop() {
let result = command.execute()?;
if result.success {
self.command_history.push(command);
}
Ok(result)
} else {
Err(anyhow::anyhow!("No commands to redo"))
}
}
}Usage Examples and Module Integration
/// Example module configuration demonstrating the usage patterns
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentIdentityConfig {
pub identity_provider: String,
pub certificate_path: String,
pub key_rotation_interval: u64,
pub authentication: AuthenticationConfig,
pub security: SecurityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticationConfig {
pub method: String,
pub timeout_seconds: u32,
pub max_retry_attempts: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub encryption_enabled: bool,
pub secret_key: String,
pub allowed_origins: Vec<String>,
}
/// Implementation of ConfigurationSchema for Agent Identity
impl ConfigurationSchema for AgentIdentityConfigSchema {
type ConfigType = AgentIdentityConfig;
fn schema_name() -> &'static str {
"agent-identity"
}
fn schema_version() -> &'static str {
"1.0.0"
}
fn default_configuration() -> Self::ConfigType {
AgentIdentityConfig {
identity_provider: "default".to_string(),
certificate_path: "/etc/sindhan/certs/agent.crt".to_string(),
key_rotation_interval: 86400, // 24 hours
authentication: AuthenticationConfig {
method: "jwt".to_string(),
timeout_seconds: 30,
max_retry_attempts: 3,
},
security: SecurityConfig {
encryption_enabled: true,
secret_key: "default-secret-key".to_string(),
allowed_origins: vec!["localhost".to_string()],
},
}
}
fn validation_rules() -> Vec<Box<dyn ValidationRule<Self::ConfigType>>> {
vec![
Box::new(RequiredFieldValidator::new("identity_provider".to_string())),
Box::new(RequiredFieldValidator::new("certificate_path".to_string())),
Box::new(RangeValidator::new("key_rotation_interval".to_string(), 3600.0, 604800.0)), // 1 hour to 1 week
Box::new(RangeValidator::new("authentication.timeout_seconds".to_string(), 1.0, 300.0)), // 1 second to 5 minutes
]
}
async fn parse_configurations(files: Vec<DiscoveredFile>) -> Result<Vec<Self::ConfigType>> {
let mut configs = Vec::new();
for file in files {
let content = tokio::fs::read_to_string(&file.path).await?;
let factory = FormatParserFactory::new();
let parser = factory.create_parser(file.format)?;
let config: Self::ConfigType = parser.parse(&content).await?;
configs.push(config);
}
Ok(configs)
}
async fn merge_configurations(configs: Vec<Self::ConfigType>) -> Result<Self::ConfigType> {
if configs.is_empty() {
return Ok(Self::default_configuration());
}
let strategy = DeepMergeStrategy;
let mut result = configs[0].clone();
for config in configs.into_iter().skip(1) {
result = strategy.merge(result, config)?;
}
Ok(result)
}
async fn validate_configuration(config: &Self::ConfigType) -> Result<()> {
// Additional custom validation logic
if config.authentication.timeout_seconds == 0 {
return Err(anyhow::anyhow!("Authentication timeout cannot be zero"));
}
if config.security.secret_key.len() < 16 {
return Err(anyhow::anyhow!("Secret key must be at least 16 characters"));
}
Ok(())
}
}
pub struct AgentIdentityConfigSchema;
/// Example usage of the configuration system
pub async fn example_usage() -> Result<()> {
// **Pattern Applied: Builder Pattern**
let discovery = ConfigurationDiscovery::builder()
.add_search_path(PathBuf::from("/etc/sindhan/config"))
.enable_hot_reload()
.build()?;
// **Pattern Applied: Builder Pattern**
let manager = ConfigurationManager::<AgentIdentityConfigSchema>::builder()
.with_discovery(ConfigurationDiscovery::builder())
.with_merge_strategy(Box::new(DeepMergeStrategy))
.add_validation_rule(Box::new(RequiredFieldValidator::new("identity_provider".to_string())))
.enable_hot_reload()
.build()
.await?;
// Load initial configuration
manager.load_configuration().await?;
// Get configuration
let config = manager.get_configuration().await;
println!("Identity Provider: {}", config.identity_provider);
// **Pattern Applied: Decorator Pattern**
let encrypted_manager = Arc::new(EncryptedConfigurationDecorator::new(
Arc::new(manager),
vec![0u8; 32], // 32-byte encryption key
vec!["security.secret_key".to_string()],
));
// Use encrypted configuration
let secure_config = encrypted_manager.get_configuration().await?;
println!("Secure config loaded with encrypted fields");
// **Pattern Applied: Observer Pattern**
let observer = Arc::new(ConfigurationLogger::new());
encrypted_manager.inner.add_observer(observer).await;
// **Pattern Applied: Command Pattern**
let mut command_invoker = ConfigurationCommandInvoker::new();
let mut new_config = secure_config.clone();
new_config.authentication.timeout_seconds = 60;
let update_command = Box::new(UpdateConfigurationCommand::new(
encrypted_manager.inner.clone(),
new_config,
));
let result = command_invoker.execute_command(update_command)?;
println!("Command executed: {}", result.message);
// Undo the last command
let undo_result = command_invoker.undo_last_command()?;
println!("Command undone: {}", undo_result.message);
Ok(())
}
/// Example configuration observer
/// **Pattern Applied: Observer Pattern**
pub struct ConfigurationLogger {
log_level: String,
}
impl ConfigurationLogger {
pub fn new() -> Self {
Self {
log_level: "INFO".to_string(),
}
}
}
#[async_trait]
impl ConfigurationObserver for ConfigurationLogger {
async fn on_configuration_changed(&self, change: ConfigurationChange) -> Result<()> {
match change {
ConfigurationChange::Loaded { .. } => {
println!("[{}] Configuration loaded successfully", self.log_level);
}
ConfigurationChange::Updated { .. } => {
println!("[{}] Configuration updated successfully", self.log_level);
}
ConfigurationChange::ValidationFailed { error } => {
println!("[ERROR] Configuration validation failed: {:?}", error);
}
}
Ok(())
}
async fn on_validation_failed(&self, error: ValidationError) -> Result<()> {
println!("[ERROR] Validation failed: {:?}", error);
Ok(())
}
async fn on_reload_completed(&self, success: bool) -> Result<()> {
if success {
println!("[{}] Configuration reload completed successfully", self.log_level);
} else {
println!("[ERROR] Configuration reload failed");
}
Ok(())
}
}Supporting Data Structures
/// Configuration format enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConfigurationFormat {
Toml,
Yaml,
Json,
}
impl ConfigurationFormat {
pub fn extension(&self) -> &'static str {
match self {
ConfigurationFormat::Toml => "toml",
ConfigurationFormat::Yaml => "yaml",
ConfigurationFormat::Json => "json",
}
}
}
/// Discovered file information
#[derive(Debug, Clone)]
pub struct DiscoveredFile {
pub path: PathBuf,
pub format: ConfigurationFormat,
pub source: DiscoverySource,
pub priority: u8,
}
impl DiscoveredFile {
pub fn default(module_name: &str) -> Self {
Self {
path: PathBuf::from(format!("default://{}", module_name)),
format: ConfigurationFormat::Toml,
source: DiscoverySource::Default,
priority: 255, // Lowest priority
}
}
}
/// Discovery source enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoverySource {
CurrentDirectory,
HomeDirectory,
EnvironmentDirectory,
External,
Default,
}
/// Validation result structure
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub rule_name: String,
pub message: String,
pub severity: ValidationSeverity,
pub field_path: Option<String>,
}
impl ValidationResult {
pub fn aggregate(results: Vec<ValidationResult>, is_valid: bool) -> Self {
let error_count = results.iter().filter(|r| r.severity == ValidationSeverity::Error).count();
let warning_count = results.iter().filter(|r| r.severity == ValidationSeverity::Warning).count();
Self {
rule_name: "Aggregate".to_string(),
message: format!("Validation complete: {} errors, {} warnings", error_count, warning_count),
severity: if is_valid { ValidationSeverity::Info } else { ValidationSeverity::Error },
field_path: None,
}
}
pub fn is_valid(&self) -> bool {
self.severity != ValidationSeverity::Error
}
pub fn errors(&self) -> Vec<String> {
if self.severity == ValidationSeverity::Error {
vec![self.message.clone()]
} else {
vec![]
}
}
}
/// Validation severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationSeverity {
Info,
Warning,
Error,
}
/// Configuration change enumeration
#[derive(Debug, Clone)]
pub enum ConfigurationChange {
Loaded { config: serde_json::Value },
Updated { old_config: serde_json::Value, new_config: serde_json::Value },
ValidationFailed { error: ValidationError },
}
/// Validation error structure
#[derive(Debug, Clone)]
pub struct ValidationError {
pub message: String,
pub field_path: Option<String>,
pub error_code: String,
}
/// Configuration command result
#[derive(Debug, Clone)]
pub struct ConfigurationCommandResult {
pub command_id: String,
pub success: bool,
pub message: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}Design Pattern Summary
This technical specification demonstrates the comprehensive application of multiple software design patterns:
-
Builder Pattern: Used extensively for constructing complex configuration objects, discovery engines, and managers with optional parameters and fluent APIs.
-
Factory Pattern: Applied in
FormatParserFactoryfor creating format-specific parsers based on configuration format types. -
Strategy Pattern: Implemented in merge strategies, validation rules, and parser selection, allowing runtime algorithm selection.
-
Observer Pattern: Used for configuration change notifications, enabling reactive updates across the system.
-
Chain of Responsibility Pattern: Applied in discovery handlers and validation pipelines, providing flexible processing chains.
-
Adapter Pattern: Used for integrating external configuration sources and adapting different data formats.
-
Decorator Pattern: Implemented for adding encryption and other enhancements to existing configuration managers.
-
Singleton Pattern: Applied in configuration registry and global state management.
-
Template Method Pattern: Used in
ConfigurationSchematrait to define standard loading workflows. -
Command Pattern: Implemented for configuration operations with undo/redo support.
This implementation provides a robust, extensible, and maintainable configuration management system that serves as the foundation for all Sindhan AI platform modules while demonstrating best practices in software architecture and design patterns.