use crate::services::service_base::ServiceMetrics;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmatConfig {
pub system: SystemConfig,
pub quality: QualityConfig,
pub analysis: AnalysisConfig,
pub performance: PerformanceConfig,
pub mcp: McpConfig,
pub roadmap: RoadmapConfig,
pub telemetry: TelemetryConfig,
pub custom: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemConfig {
pub project_name: String,
pub project_path: PathBuf,
pub output_dir: PathBuf,
pub max_concurrent_operations: usize,
pub verbose: bool,
pub debug: bool,
pub default_toolchain: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityConfig {
pub max_complexity: u32,
pub max_cognitive_complexity: u32,
pub min_coverage: f64,
pub allow_satd: bool,
pub require_docs: bool,
pub lint_compliance: bool,
pub fail_on_violation: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisConfig {
pub include_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
pub max_file_size: usize,
pub max_line_length: usize,
pub skip_vendor: bool,
pub parallel: bool,
pub thread_count: usize,
pub timeout_seconds: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub enable_regression_tests: bool,
pub enable_memory_tests: bool,
pub enable_throughput_tests: bool,
pub test_iterations: usize,
pub timeout_ms: u64,
pub target_startup_latency_ms: u64,
pub target_throughput_loc_per_sec: u64,
pub target_memory_mb: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub server_name: String,
pub server_version: String,
pub enable_compression: bool,
pub request_timeout_seconds: u64,
pub max_request_size: usize,
pub log_requests: bool,
pub enabled_tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoadmapConfig {
pub roadmap_path: PathBuf,
pub auto_generate_todos: bool,
pub enforce_quality_gates: bool,
pub require_task_ids: bool,
pub task_id_pattern: String,
pub velocity_tracking: bool,
pub burndown_charts: bool,
pub git: GitConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitConfig {
pub create_branches: bool,
pub branch_pattern: String,
pub commit_pattern: String,
pub require_quality_check: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
pub enabled: bool,
pub collection_interval_seconds: u64,
pub max_data_age_days: u32,
pub enable_aggregation: bool,
pub enable_export: bool,
pub export_format: String,
}
pub struct ConfigurationService {
config: Arc<RwLock<PmatConfig>>,
config_path: PathBuf,
metrics: Arc<RwLock<ServiceMetrics>>,
watchers: Arc<RwLock<Vec<Box<dyn ConfigWatcher + Send + Sync>>>>,
}
pub trait ConfigWatcher {
fn on_config_changed(&self, config: &PmatConfig) -> Result<()>;
}
impl ConfigurationService {
#[must_use]
pub fn new(config_path: Option<PathBuf>) -> Self {
let default_path = config_path.unwrap_or_else(|| {
std::env::current_dir()
.unwrap_or_default()
.join("pmat.toml")
});
let default_config = Self::default_config();
Self {
config: Arc::new(RwLock::new(default_config)),
config_path: default_path,
metrics: Arc::new(RwLock::new(ServiceMetrics::default())),
watchers: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn load(&self) -> Result<()> {
if self.config_path.exists() {
let content = tokio::fs::read_to_string(&self.config_path).await?;
let config: PmatConfig = toml::from_str(&content)?;
{
let mut config_lock = self
.config
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire config write lock"))?;
*config_lock = config.clone();
}
self.notify_watchers(&config)?;
{
let mut metrics = self
.metrics
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?;
metrics.record_request(std::time::Duration::from_millis(1), true);
}
}
Ok(())
}
pub async fn save(&self) -> Result<()> {
let config = {
self.config
.read()
.map_err(|_| anyhow::anyhow!("Failed to acquire config read lock"))?
.clone()
};
let content = toml::to_string_pretty(&config)?;
tokio::fs::write(&self.config_path, content).await?;
{
let mut metrics = self
.metrics
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?;
metrics.record_request(std::time::Duration::from_millis(1), true);
}
Ok(())
}
pub fn get_config(&self) -> Result<PmatConfig> {
Ok(self
.config
.read()
.map_err(|_| anyhow::anyhow!("Failed to acquire config read lock"))?
.clone())
}
pub async fn update_config<F>(&self, updater: F) -> Result<()>
where
F: FnOnce(&mut PmatConfig) -> Result<()>,
{
let config_clone = {
let mut config = self
.config
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire config write lock"))?;
updater(&mut config)?;
config.clone()
};
self.save().await?;
self.notify_watchers(&config_clone)?;
Ok(())
}
pub fn add_watcher(&self, watcher: Box<dyn ConfigWatcher + Send + Sync>) -> Result<()> {
let mut watchers = self
.watchers
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire watchers lock"))?;
watchers.push(watcher);
Ok(())
}
pub fn get_quality_config(&self) -> Result<QualityConfig> {
Ok(self.get_config()?.quality)
}
pub fn get_analysis_config(&self) -> Result<AnalysisConfig> {
Ok(self.get_config()?.analysis)
}
pub fn get_performance_config(&self) -> Result<PerformanceConfig> {
Ok(self.get_config()?.performance)
}
pub fn get_mcp_config(&self) -> Result<McpConfig> {
Ok(self.get_config()?.mcp)
}
pub fn get_roadmap_config(&self) -> Result<RoadmapConfig> {
Ok(self.get_config()?.roadmap)
}
pub fn get_telemetry_config(&self) -> Result<TelemetryConfig> {
Ok(self.get_config()?.telemetry)
}
fn notify_watchers(&self, config: &PmatConfig) -> Result<()> {
let watchers = self
.watchers
.read()
.map_err(|_| anyhow::anyhow!("Failed to acquire watchers lock"))?;
for watcher in watchers.iter() {
if let Err(e) = watcher.on_config_changed(config) {
tracing::warn!("Configuration watcher failed: {}", e);
}
}
Ok(())
}
#[must_use]
pub fn default_config() -> PmatConfig {
PmatConfig {
system: SystemConfig {
project_name: "pmat".to_string(),
project_path: std::env::current_dir().unwrap_or_default(),
output_dir: PathBuf::from("target/pmat"),
max_concurrent_operations: num_cpus::get(),
verbose: false,
debug: false,
default_toolchain: "rust".to_string(),
},
quality: QualityConfig {
max_complexity: 30,
max_cognitive_complexity: 25,
min_coverage: 80.0,
allow_satd: false,
require_docs: true,
lint_compliance: true,
fail_on_violation: true,
},
analysis: AnalysisConfig {
include_patterns: vec!["**/*.rs".to_string(), "**/*.ts".to_string()],
exclude_patterns: vec![
"**/target/**".to_string(),
"**/node_modules/**".to_string(),
],
max_file_size: 1024 * 1024, max_line_length: 100,
skip_vendor: true,
parallel: true,
thread_count: 0, timeout_seconds: 300, },
performance: PerformanceConfig {
enable_regression_tests: true,
enable_memory_tests: true,
enable_throughput_tests: true,
test_iterations: 10,
timeout_ms: 30000,
target_startup_latency_ms: 127,
target_throughput_loc_per_sec: 487000,
target_memory_mb: 47,
},
mcp: McpConfig {
server_name: "pmat-mcp-server".to_string(),
server_version: env!("CARGO_PKG_VERSION").to_string(),
enable_compression: true,
request_timeout_seconds: 30,
max_request_size: 10 * 1024 * 1024, log_requests: false,
enabled_tools: vec![
"analyze_complexity".to_string(),
"analyze_dead_code".to_string(),
"quality_gate".to_string(),
"refactor_start".to_string(),
],
},
roadmap: RoadmapConfig {
roadmap_path: PathBuf::from("docs/execution/roadmap.md"),
auto_generate_todos: true,
enforce_quality_gates: true,
require_task_ids: true,
task_id_pattern: "PMAT-[0-9]{4}".to_string(),
velocity_tracking: true,
burndown_charts: true,
git: GitConfig {
create_branches: true,
branch_pattern: "feature/{task_id}".to_string(),
commit_pattern: "{task_id}: {message}".to_string(),
require_quality_check: true,
},
},
telemetry: TelemetryConfig {
enabled: true,
collection_interval_seconds: 60,
max_data_age_days: 30,
enable_aggregation: true,
enable_export: false,
export_format: "json".to_string(),
},
custom: HashMap::new(),
}
}
}
impl ConfigurationService {
pub async fn start(&self) -> Result<()> {
self.load().await?;
{
let mut metrics = self
.metrics
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?;
metrics.record_request(Duration::from_millis(10), true);
}
tracing::info!(
"Configuration service started with config at: {:?}",
self.config_path
);
Ok(())
}
pub async fn stop(&self) -> Result<()> {
self.save().await?;
{
let mut metrics = self
.metrics
.write()
.map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?;
metrics.record_request(Duration::from_millis(5), true);
}
tracing::info!("Configuration service stopped");
Ok(())
}
pub async fn status(&self) -> Result<String> {
let config_exists = self.config_path.exists();
let _config = self.get_config()?;
Ok(format!(
"Configuration service: {} (file: {}, sections: {})",
if config_exists { "loaded" } else { "default" },
self.config_path.display(),
7 ))
}
pub async fn get_metrics(&self) -> Result<ServiceMetrics> {
Ok(self
.metrics
.read()
.map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?
.clone())
}
pub async fn health_check(&self) -> Result<bool> {
self.get_config().map(|_| true)
}
}
lazy_static::lazy_static! {
static ref CONFIGURATION: Arc<ConfigurationService> = Arc::new(ConfigurationService::new(None));
}
#[must_use]
pub fn configuration() -> Arc<ConfigurationService> {
CONFIGURATION.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_configuration_service_creation() {
let config_service = ConfigurationService::new(None);
let config = config_service.get_config().unwrap();
assert_eq!(config.system.project_name, "pmat");
assert_eq!(config.quality.max_complexity, 20);
assert!(!config.quality.allow_satd);
}
#[tokio::test]
async fn test_configuration_save_load() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("test_config.toml");
let config_service = ConfigurationService::new(Some(config_path.clone()));
config_service
.update_config(|config| {
config.system.project_name = "test_project".to_string();
config.quality.max_complexity = 25;
Ok(())
})
.await
.unwrap();
let new_service = ConfigurationService::new(Some(config_path));
new_service.load().await.unwrap();
let loaded_config = new_service.get_config().unwrap();
assert_eq!(loaded_config.system.project_name, "test_project");
assert_eq!(loaded_config.quality.max_complexity, 25);
}
#[tokio::test]
async fn test_configuration_sections() {
let config_service = ConfigurationService::new(None);
let quality_config = config_service.get_quality_config().unwrap();
assert_eq!(quality_config.max_complexity, 20);
let analysis_config = config_service.get_analysis_config().unwrap();
assert!(analysis_config.parallel);
let performance_config = config_service.get_performance_config().unwrap();
assert_eq!(performance_config.test_iterations, 10);
let mcp_config = config_service.get_mcp_config().unwrap();
assert_eq!(mcp_config.server_name, "pmat-mcp-server");
let roadmap_config = config_service.get_roadmap_config().unwrap();
assert!(roadmap_config.auto_generate_todos);
let telemetry_config = config_service.get_telemetry_config().unwrap();
assert!(telemetry_config.enabled);
}
#[tokio::test]
async fn test_service_lifecycle() {
let config_service = ConfigurationService::new(None);
assert!(config_service.start().await.is_ok());
assert!(config_service.health_check().await.unwrap());
let status = config_service.status().await.unwrap();
assert!(status.contains("Configuration service"));
let metrics = config_service.get_metrics().await.unwrap();
assert_eq!(metrics.request_count, 1);
assert!(config_service.stop().await.is_ok());
}
#[tokio::test]
async fn test_global_configuration_access() {
let config_service = configuration();
let config = config_service.get_config().unwrap();
assert_eq!(config.system.project_name, "pmat");
assert!(config.quality.fail_on_violation);
}
#[test]
fn test_configuration_serialization() {
let config = ConfigurationService::default_config();
let serialized = toml::to_string(&config).unwrap();
assert!(serialized.contains("[system]"));
assert!(serialized.contains("[quality]"));
assert!(serialized.contains("[analysis]"));
assert!(serialized.contains("[performance]"));
assert!(serialized.contains("[mcp]"));
assert!(serialized.contains("[roadmap]"));
assert!(serialized.contains("[telemetry]"));
let deserialized: PmatConfig = toml::from_str(&serialized).unwrap();
assert_eq!(deserialized.system.project_name, config.system.project_name);
assert_eq!(
deserialized.quality.max_complexity,
config.quality.max_complexity
);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}