use crate::config::hierarchy::{ConfigLevel, HierarchicalConfig, PartialConfig};
use crate::config::locking::HierarchicalLockManager;
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::fs;
use tracing::info;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedConfig {
pub version: String,
pub exported_from: ConfigLevel,
pub exported_by: String,
pub exported_at: String,
pub description: Option<String>,
pub config: PartialConfig,
pub locks: HashMap<String, LockExport>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockExport {
pub value: String,
pub reason: String,
}
impl SharedConfig {
pub async fn from_level(level: ConfigLevel) -> Result<Self> {
let hier_config = HierarchicalConfig::load().await?;
let lock_manager = HierarchicalLockManager::load().await?;
let partial = match level {
ConfigLevel::System => hier_config.system.clone(),
ConfigLevel::User => hier_config.user.clone(),
ConfigLevel::Project => hier_config.project.clone(),
};
let config = partial.unwrap_or_default();
let locks = match level {
ConfigLevel::System => lock_manager.get_locks_at_level(ConfigLevel::System).await?,
ConfigLevel::User => lock_manager.get_locks_at_level(ConfigLevel::User).await?,
ConfigLevel::Project => {
lock_manager
.get_locks_at_level(ConfigLevel::Project)
.await?
}
};
let lock_exports: HashMap<String, LockExport> = locks
.into_iter()
.map(|(key, entry)| {
(
key,
LockExport {
value: entry.value,
reason: entry.reason,
},
)
})
.collect();
Ok(Self {
version: env!("CARGO_PKG_VERSION").to_string(),
exported_from: level,
exported_by: whoami::username(),
exported_at: chrono::Utc::now().to_rfc3339(),
description: None,
config,
locks: lock_exports,
})
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn to_toml(&self) -> Result<String> {
toml::to_string_pretty(self)
.map_err(|e| Error::config(format!("Failed to serialize shared config: {}", e)))
}
pub fn from_toml(toml_str: &str) -> Result<Self> {
toml::from_str(toml_str)
.map_err(|e| Error::config(format!("Failed to parse shared config: {}", e)))
}
pub async fn export_to_file(&self, path: &PathBuf) -> Result<()> {
let contents = self.to_toml()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
Error::config(format!("Failed to create directory for export: {}", e))
})?;
}
fs::write(path, contents)
.await
.map_err(|e| Error::config(format!("Failed to write shared config to file: {}", e)))?;
info!("Exported configuration to {}", path.display());
Ok(())
}
pub async fn import_from_file(path: &PathBuf) -> Result<Self> {
if !path.exists() {
return Err(Error::config(format!(
"Shared config file not found: {}",
path.display()
)));
}
let contents = fs::read_to_string(path)
.await
.map_err(|e| Error::config(format!("Failed to read shared config file: {}", e)))?;
Self::from_toml(&contents)
}
pub fn summary(&self) -> String {
let mut summary = format!("Shared Configuration\n");
summary.push_str(&format!(" Version: {}\n", self.version));
summary.push_str(&format!(
" Exported from: {} level\n",
self.exported_from.display_name()
));
summary.push_str(&format!(" Exported by: {}\n", self.exported_by));
summary.push_str(&format!(" Exported at: {}\n", self.exported_at));
if let Some(ref desc) = self.description {
summary.push_str(&format!(" Description: {}\n", desc));
}
summary.push_str(&format!(
"\n Configuration entries: {}\n",
self.count_config_entries()
));
summary.push_str(&format!(" Locked keys: {}\n", self.locks.len()));
summary
}
fn count_config_entries(&self) -> usize {
let mut count = 0;
if self.config.initialized.is_some() {
count += 1;
}
if self.config.version.is_some() {
count += 1;
}
if self.config.update_channel.is_some() {
count += 1;
}
if self.config.auto_update.is_some() {
count += 1;
}
if self.config.clippy_rules.is_some() {
count += 1;
}
if self.config.max_file_lines.is_some() {
count += 1;
}
if self.config.max_function_lines.is_some() {
count += 1;
}
if self.config.required_edition.is_some() {
count += 1;
}
if self.config.required_rust_version.is_some() {
count += 1;
}
if self.config.ban_underscore_bandaid.is_some() {
count += 1;
}
if self.config.require_documentation.is_some() {
count += 1;
}
if self.config.custom_rules.is_some() {
count += 1;
}
count
}
}
#[derive(Debug, Clone)]
pub struct ImportOptions {
pub target_level: ConfigLevel,
pub overwrite: bool,
pub import_locks: bool,
pub require_justification: bool,
}
impl Default for ImportOptions {
fn default() -> Self {
Self {
target_level: ConfigLevel::Project,
overwrite: true,
import_locks: true,
require_justification: true,
}
}
}
pub async fn import_shared_config(
shared: &SharedConfig,
options: ImportOptions,
) -> Result<ImportReport> {
let mut report = ImportReport::default();
let hier_config = HierarchicalConfig::load().await?;
let mut lock_manager = HierarchicalLockManager::load().await?;
let existing_partial = match options.target_level {
ConfigLevel::System => hier_config.system.clone().unwrap_or_default(),
ConfigLevel::User => hier_config.user.clone().unwrap_or_default(),
ConfigLevel::Project => hier_config.project.clone().unwrap_or_default(),
};
let merged_partial = if options.overwrite {
existing_partial.merge(shared.config.clone())
} else {
shared.config.clone().merge(existing_partial)
};
let mut conflicts = Vec::new();
if options.require_justification {
let locks = lock_manager.get_effective_locks();
for key in shared.config.list_keys() {
if let Some((level, entry)) = locks.get(&key) {
if *level > options.target_level {
conflicts.push(LockConflict {
key: key.clone(),
locked_at: *level,
current_value: entry.value.clone(),
attempted_value: shared.config.get_value(&key).unwrap_or_default(),
});
}
}
}
}
if !conflicts.is_empty() {
report.conflicts = conflicts;
return Ok(report);
}
HierarchicalConfig::save_partial_at_level(&merged_partial, options.target_level).await?;
report.config_imported = true;
report.config_keys_updated = merged_partial.count_set_fields();
if options.import_locks && !shared.locks.is_empty() {
for (key, lock_export) in &shared.locks {
if lock_manager
.is_locked_at_level(key, options.target_level)
.is_some()
{
report.locks_skipped.push(key.clone());
continue;
}
let entry = crate::config::locking::LockEntry::new(
&lock_export.value,
format!("Imported from shared config: {}", lock_export.reason),
options.target_level,
);
lock_manager.lock_with_entry(key, entry).await?;
report.locks_imported.push(key.clone());
}
}
info!(
"Imported {} config keys and {} locks to {} level",
report.config_keys_updated,
report.locks_imported.len(),
options.target_level.display_name()
);
Ok(report)
}
#[derive(Debug, Clone, Default)]
pub struct ImportReport {
pub config_imported: bool,
pub config_keys_updated: usize,
pub locks_imported: Vec<String>,
pub locks_skipped: Vec<String>,
pub conflicts: Vec<LockConflict>,
}
#[derive(Debug, Clone)]
pub struct LockConflict {
pub key: String,
pub locked_at: ConfigLevel,
pub current_value: String,
pub attempted_value: String,
}
pub trait PartialConfigExt {
fn list_keys(&self) -> Vec<String>;
fn get_value(&self, key: &str) -> Option<String>;
fn count_set_fields(&self) -> usize;
}
impl PartialConfigExt for PartialConfig {
fn list_keys(&self) -> Vec<String> {
let mut keys = Vec::new();
if self.initialized.is_some() {
keys.push("initialized".to_string());
}
if self.version.is_some() {
keys.push("version".to_string());
}
if self.update_channel.is_some() {
keys.push("update_channel".to_string());
}
if self.auto_update.is_some() {
keys.push("auto_update".to_string());
}
if self.clippy_rules.is_some() {
keys.push("clippy_rules".to_string());
}
if self.max_file_lines.is_some() {
keys.push("max_file_lines".to_string());
}
if self.max_function_lines.is_some() {
keys.push("max_function_lines".to_string());
}
if self.required_edition.is_some() {
keys.push("required_edition".to_string());
}
if self.required_rust_version.is_some() {
keys.push("required_rust_version".to_string());
}
if self.ban_underscore_bandaid.is_some() {
keys.push("ban_underscore_bandaid".to_string());
}
if self.require_documentation.is_some() {
keys.push("require_documentation".to_string());
}
if self.custom_rules.is_some() {
keys.push("custom_rules".to_string());
}
keys
}
fn get_value(&self, key: &str) -> Option<String> {
match key {
"initialized" => self.initialized.map(|v| v.to_string()),
"version" => self.version.clone(),
"update_channel" => self.update_channel.clone(),
"auto_update" => self.auto_update.map(|v| v.to_string()),
"clippy_rules" => self.clippy_rules.as_ref().map(|v| format!("{:?}", v)),
"max_file_lines" => self.max_file_lines.map(|v| v.to_string()),
"max_function_lines" => self.max_function_lines.map(|v| v.to_string()),
"required_edition" => self.required_edition.clone(),
"required_rust_version" => self.required_rust_version.clone(),
"ban_underscore_bandaid" => self.ban_underscore_bandaid.map(|v| v.to_string()),
"require_documentation" => self.require_documentation.map(|v| v.to_string()),
"custom_rules" => self.custom_rules.as_ref().map(|v| format!("{:?}", v)),
_ => None,
}
}
fn count_set_fields(&self) -> usize {
self.list_keys().len()
}
}
pub trait HierarchicalLockManagerExt {
#[allow(async_fn_in_trait)]
async fn get_locks_at_level(
&self,
level: ConfigLevel,
) -> Result<HashMap<String, crate::config::locking::LockEntry>>;
#[allow(async_fn_in_trait)]
async fn lock_with_entry(
&mut self,
key: &str,
entry: crate::config::locking::LockEntry,
) -> Result<()>;
}
impl HierarchicalLockManagerExt for HierarchicalLockManager {
async fn get_locks_at_level(
&self,
level: ConfigLevel,
) -> Result<HashMap<String, crate::config::locking::LockEntry>> {
use crate::config::locking::LockedConfig;
if let Some(locked) = LockedConfig::load_from_level(level).await? {
Ok(locked.locks)
} else {
Ok(HashMap::new())
}
}
async fn lock_with_entry(
&mut self,
key: &str,
entry: crate::config::locking::LockEntry,
) -> Result<()> {
use crate::config::locking::LockedConfig;
let level = entry.level;
let mut locks = LockedConfig::load_from_level(level)
.await?
.unwrap_or_default();
locks.lock(key.to_string(), entry);
locks.save_to_level(level).await?;
Ok(())
}
}
pub trait HierarchicalConfigExt {
#[allow(async_fn_in_trait)]
async fn save_partial_at_level(partial: &PartialConfig, level: ConfigLevel) -> Result<()>;
}
impl HierarchicalConfigExt for HierarchicalConfig {
async fn save_partial_at_level(partial: &PartialConfig, level: ConfigLevel) -> Result<()> {
use tokio::fs;
let path = level.path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
let full_config = partial.clone().to_full_config();
let contents = toml::to_string_pretty(&full_config)
.map_err(|e| Error::config(format!("Failed to serialize config: {}", e)))?;
fs::write(&path, contents).await.map_err(|e| {
Error::config(format!(
"Failed to write {} config: {}",
level.display_name(),
e
))
})?;
info!(
"Saved {} configuration to {}",
level.display_name(),
path.display()
);
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_shared_config_serialization() {
let config = PartialConfig {
required_edition: Some("2024".to_string()),
max_file_lines: Some(300),
..Default::default()
};
let shared = SharedConfig {
version: "1.0.0".to_string(),
exported_from: ConfigLevel::Project,
exported_by: "test_user".to_string(),
exported_at: "2024-01-01T00:00:00Z".to_string(),
description: Some("Test config".to_string()),
config,
locks: HashMap::new(),
};
let toml = shared.to_toml().unwrap();
assert!(toml.contains("version"));
assert!(toml.contains("2024"));
}
#[test]
fn test_partial_config_ext() {
let partial = PartialConfig {
required_edition: Some("2024".to_string()),
max_file_lines: Some(300),
..Default::default()
};
let keys = partial.list_keys();
assert!(keys.contains(&"required_edition".to_string()));
assert!(keys.contains(&"max_file_lines".to_string()));
assert_eq!(partial.count_set_fields(), 2);
}
}