1use crate::config::hierarchy::{ConfigLevel, HierarchicalConfig, PartialConfig};
10use crate::config::locking::HierarchicalLockManager;
11use crate::{Error, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::PathBuf;
15use tokio::fs;
16use tracing::info;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct SharedConfig {
25 pub version: String,
27 pub exported_from: ConfigLevel,
29 pub exported_by: String,
31 pub exported_at: String,
33 pub description: Option<String>,
35 pub config: PartialConfig,
37 pub locks: HashMap<String, LockExport>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct LockExport {
44 pub value: String,
46 pub reason: String,
48}
49
50impl SharedConfig {
51 pub async fn from_level(level: ConfigLevel) -> Result<Self> {
57 let hier_config = HierarchicalConfig::load().await?;
58 let lock_manager = HierarchicalLockManager::load().await?;
59
60 let partial = match level {
62 ConfigLevel::System => hier_config.system.clone(),
63 ConfigLevel::User => hier_config.user.clone(),
64 ConfigLevel::Project => hier_config.project.clone(),
65 };
66
67 let config = partial.unwrap_or_default();
69
70 let locks = match level {
72 ConfigLevel::System => lock_manager.get_locks_at_level(ConfigLevel::System).await?,
73 ConfigLevel::User => lock_manager.get_locks_at_level(ConfigLevel::User).await?,
74 ConfigLevel::Project => {
75 lock_manager
76 .get_locks_at_level(ConfigLevel::Project)
77 .await?
78 }
79 };
80
81 let lock_exports: HashMap<String, LockExport> = locks
83 .into_iter()
84 .map(|(key, entry)| {
85 (
86 key,
87 LockExport {
88 value: entry.value,
89 reason: entry.reason,
90 },
91 )
92 })
93 .collect();
94
95 Ok(Self {
96 version: env!("CARGO_PKG_VERSION").to_string(),
97 exported_from: level,
98 exported_by: whoami::username(),
99 exported_at: chrono::Utc::now().to_rfc3339(),
100 description: None,
101 config,
102 locks: lock_exports,
103 })
104 }
105
106 pub fn with_description(mut self, description: impl Into<String>) -> Self {
108 self.description = Some(description.into());
109 self
110 }
111
112 pub fn to_toml(&self) -> Result<String> {
118 toml::to_string_pretty(self)
119 .map_err(|e| Error::config(format!("Failed to serialize shared config: {}", e)))
120 }
121
122 pub fn from_toml(toml_str: &str) -> Result<Self> {
128 toml::from_str(toml_str)
129 .map_err(|e| Error::config(format!("Failed to parse shared config: {}", e)))
130 }
131
132 pub async fn export_to_file(&self, path: &PathBuf) -> Result<()> {
138 let contents = self.to_toml()?;
139
140 if let Some(parent) = path.parent() {
142 fs::create_dir_all(parent).await.map_err(|e| {
143 Error::config(format!("Failed to create directory for export: {}", e))
144 })?;
145 }
146
147 fs::write(path, contents)
148 .await
149 .map_err(|e| Error::config(format!("Failed to write shared config to file: {}", e)))?;
150
151 info!("Exported configuration to {}", path.display());
152 Ok(())
153 }
154
155 pub async fn import_from_file(path: &PathBuf) -> Result<Self> {
161 if !path.exists() {
162 return Err(Error::config(format!(
163 "Shared config file not found: {}",
164 path.display()
165 )));
166 }
167
168 let contents = fs::read_to_string(path)
169 .await
170 .map_err(|e| Error::config(format!("Failed to read shared config file: {}", e)))?;
171
172 Self::from_toml(&contents)
173 }
174
175 pub fn summary(&self) -> String {
177 let mut summary = format!("Shared Configuration\n");
178 summary.push_str(&format!(" Version: {}\n", self.version));
179 summary.push_str(&format!(
180 " Exported from: {} level\n",
181 self.exported_from.display_name()
182 ));
183 summary.push_str(&format!(" Exported by: {}\n", self.exported_by));
184 summary.push_str(&format!(" Exported at: {}\n", self.exported_at));
185 if let Some(ref desc) = self.description {
186 summary.push_str(&format!(" Description: {}\n", desc));
187 }
188 summary.push_str(&format!(
189 "\n Configuration entries: {}\n",
190 self.count_config_entries()
191 ));
192 summary.push_str(&format!(" Locked keys: {}\n", self.locks.len()));
193 summary
194 }
195
196 fn count_config_entries(&self) -> usize {
198 let mut count = 0;
199 if self.config.initialized.is_some() {
200 count += 1;
201 }
202 if self.config.version.is_some() {
203 count += 1;
204 }
205 if self.config.update_channel.is_some() {
206 count += 1;
207 }
208 if self.config.auto_update.is_some() {
209 count += 1;
210 }
211 if self.config.clippy_rules.is_some() {
212 count += 1;
213 }
214 if self.config.max_file_lines.is_some() {
215 count += 1;
216 }
217 if self.config.max_function_lines.is_some() {
218 count += 1;
219 }
220 if self.config.required_edition.is_some() {
221 count += 1;
222 }
223 if self.config.required_rust_version.is_some() {
224 count += 1;
225 }
226 if self.config.ban_underscore_bandaid.is_some() {
227 count += 1;
228 }
229 if self.config.require_documentation.is_some() {
230 count += 1;
231 }
232 if self.config.custom_rules.is_some() {
233 count += 1;
234 }
235 count
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct ImportOptions {
242 pub target_level: ConfigLevel,
244 pub overwrite: bool,
246 pub import_locks: bool,
248 pub require_justification: bool,
250}
251
252impl Default for ImportOptions {
253 fn default() -> Self {
254 Self {
255 target_level: ConfigLevel::Project,
256 overwrite: true,
257 import_locks: true,
258 require_justification: true,
259 }
260 }
261}
262
263pub async fn import_shared_config(
272 shared: &SharedConfig,
273 options: ImportOptions,
274) -> Result<ImportReport> {
275 let mut report = ImportReport::default();
276
277 let hier_config = HierarchicalConfig::load().await?;
279 let mut lock_manager = HierarchicalLockManager::load().await?;
280
281 let existing_partial = match options.target_level {
283 ConfigLevel::System => hier_config.system.clone().unwrap_or_default(),
284 ConfigLevel::User => hier_config.user.clone().unwrap_or_default(),
285 ConfigLevel::Project => hier_config.project.clone().unwrap_or_default(),
286 };
287
288 let merged_partial = if options.overwrite {
290 existing_partial.merge(shared.config.clone())
291 } else {
292 shared.config.clone().merge(existing_partial)
294 };
295
296 let mut conflicts = Vec::new();
298 if options.require_justification {
299 let locks = lock_manager.get_effective_locks();
301 for key in shared.config.list_keys() {
302 if let Some((level, entry)) = locks.get(&key) {
303 if *level > options.target_level {
305 conflicts.push(LockConflict {
306 key: key.clone(),
307 locked_at: *level,
308 current_value: entry.value.clone(),
309 attempted_value: shared.config.get_value(&key).unwrap_or_default(),
310 });
311 }
312 }
313 }
314 }
315
316 if !conflicts.is_empty() {
317 report.conflicts = conflicts;
318 return Ok(report);
319 }
320
321 HierarchicalConfig::save_partial_at_level(&merged_partial, options.target_level).await?;
323 report.config_imported = true;
324 report.config_keys_updated = merged_partial.count_set_fields();
325
326 if options.import_locks && !shared.locks.is_empty() {
328 for (key, lock_export) in &shared.locks {
329 if lock_manager
331 .is_locked_at_level(key, options.target_level)
332 .is_some()
333 {
334 report.locks_skipped.push(key.clone());
335 continue;
336 }
337
338 let entry = crate::config::locking::LockEntry::new(
340 &lock_export.value,
341 format!("Imported from shared config: {}", lock_export.reason),
342 options.target_level,
343 );
344
345 lock_manager.lock_with_entry(key, entry).await?;
346 report.locks_imported.push(key.clone());
347 }
348 }
349
350 info!(
351 "Imported {} config keys and {} locks to {} level",
352 report.config_keys_updated,
353 report.locks_imported.len(),
354 options.target_level.display_name()
355 );
356
357 Ok(report)
358}
359
360#[derive(Debug, Clone, Default)]
362pub struct ImportReport {
363 pub config_imported: bool,
365 pub config_keys_updated: usize,
367 pub locks_imported: Vec<String>,
369 pub locks_skipped: Vec<String>,
371 pub conflicts: Vec<LockConflict>,
373}
374
375#[derive(Debug, Clone)]
377pub struct LockConflict {
378 pub key: String,
380 pub locked_at: ConfigLevel,
382 pub current_value: String,
384 pub attempted_value: String,
386}
387
388pub trait PartialConfigExt {
390 fn list_keys(&self) -> Vec<String>;
392 fn get_value(&self, key: &str) -> Option<String>;
394 fn count_set_fields(&self) -> usize;
396}
397
398impl PartialConfigExt for PartialConfig {
399 fn list_keys(&self) -> Vec<String> {
400 let mut keys = Vec::new();
401 if self.initialized.is_some() {
402 keys.push("initialized".to_string());
403 }
404 if self.version.is_some() {
405 keys.push("version".to_string());
406 }
407 if self.update_channel.is_some() {
408 keys.push("update_channel".to_string());
409 }
410 if self.auto_update.is_some() {
411 keys.push("auto_update".to_string());
412 }
413 if self.clippy_rules.is_some() {
414 keys.push("clippy_rules".to_string());
415 }
416 if self.max_file_lines.is_some() {
417 keys.push("max_file_lines".to_string());
418 }
419 if self.max_function_lines.is_some() {
420 keys.push("max_function_lines".to_string());
421 }
422 if self.required_edition.is_some() {
423 keys.push("required_edition".to_string());
424 }
425 if self.required_rust_version.is_some() {
426 keys.push("required_rust_version".to_string());
427 }
428 if self.ban_underscore_bandaid.is_some() {
429 keys.push("ban_underscore_bandaid".to_string());
430 }
431 if self.require_documentation.is_some() {
432 keys.push("require_documentation".to_string());
433 }
434 if self.custom_rules.is_some() {
435 keys.push("custom_rules".to_string());
436 }
437 keys
438 }
439
440 fn get_value(&self, key: &str) -> Option<String> {
441 match key {
442 "initialized" => self.initialized.map(|v| v.to_string()),
443 "version" => self.version.clone(),
444 "update_channel" => self.update_channel.clone(),
445 "auto_update" => self.auto_update.map(|v| v.to_string()),
446 "clippy_rules" => self.clippy_rules.as_ref().map(|v| format!("{:?}", v)),
447 "max_file_lines" => self.max_file_lines.map(|v| v.to_string()),
448 "max_function_lines" => self.max_function_lines.map(|v| v.to_string()),
449 "required_edition" => self.required_edition.clone(),
450 "required_rust_version" => self.required_rust_version.clone(),
451 "ban_underscore_bandaid" => self.ban_underscore_bandaid.map(|v| v.to_string()),
452 "require_documentation" => self.require_documentation.map(|v| v.to_string()),
453 "custom_rules" => self.custom_rules.as_ref().map(|v| format!("{:?}", v)),
454 _ => None,
455 }
456 }
457
458 fn count_set_fields(&self) -> usize {
459 self.list_keys().len()
460 }
461}
462
463pub trait HierarchicalLockManagerExt {
465 #[allow(async_fn_in_trait)]
467 async fn get_locks_at_level(
468 &self,
469 level: ConfigLevel,
470 ) -> Result<HashMap<String, crate::config::locking::LockEntry>>;
471 #[allow(async_fn_in_trait)]
473 async fn lock_with_entry(
474 &mut self,
475 key: &str,
476 entry: crate::config::locking::LockEntry,
477 ) -> Result<()>;
478}
479
480impl HierarchicalLockManagerExt for HierarchicalLockManager {
481 async fn get_locks_at_level(
482 &self,
483 level: ConfigLevel,
484 ) -> Result<HashMap<String, crate::config::locking::LockEntry>> {
485 use crate::config::locking::LockedConfig;
486
487 if let Some(locked) = LockedConfig::load_from_level(level).await? {
488 Ok(locked.locks)
489 } else {
490 Ok(HashMap::new())
491 }
492 }
493
494 async fn lock_with_entry(
495 &mut self,
496 key: &str,
497 entry: crate::config::locking::LockEntry,
498 ) -> Result<()> {
499 use crate::config::locking::LockedConfig;
500
501 let level = entry.level;
502
503 let mut locks = LockedConfig::load_from_level(level)
505 .await?
506 .unwrap_or_default();
507 locks.lock(key.to_string(), entry);
508 locks.save_to_level(level).await?;
509
510 Ok(())
511 }
512}
513
514pub trait HierarchicalConfigExt {
516 #[allow(async_fn_in_trait)]
518 async fn save_partial_at_level(partial: &PartialConfig, level: ConfigLevel) -> Result<()>;
519}
520
521impl HierarchicalConfigExt for HierarchicalConfig {
522 async fn save_partial_at_level(partial: &PartialConfig, level: ConfigLevel) -> Result<()> {
523 use tokio::fs;
524
525 let path = level.path()?;
526
527 if let Some(parent) = path.parent() {
529 fs::create_dir_all(parent).await?;
530 }
531
532 let full_config = partial.clone().to_full_config();
534 let contents = toml::to_string_pretty(&full_config)
535 .map_err(|e| Error::config(format!("Failed to serialize config: {}", e)))?;
536
537 fs::write(&path, contents).await.map_err(|e| {
538 Error::config(format!(
539 "Failed to write {} config: {}",
540 level.display_name(),
541 e
542 ))
543 })?;
544
545 info!(
546 "Saved {} configuration to {}",
547 level.display_name(),
548 path.display()
549 );
550 Ok(())
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_shared_config_serialization() {
560 let config = PartialConfig {
561 required_edition: Some("2024".to_string()),
562 max_file_lines: Some(300),
563 ..Default::default()
564 };
565
566 let shared = SharedConfig {
567 version: "1.0.0".to_string(),
568 exported_from: ConfigLevel::Project,
569 exported_by: "test_user".to_string(),
570 exported_at: "2024-01-01T00:00:00Z".to_string(),
571 description: Some("Test config".to_string()),
572 config,
573 locks: HashMap::new(),
574 };
575
576 let toml = shared.to_toml().unwrap();
577 assert!(toml.contains("version"));
578 assert!(toml.contains("2024"));
579 }
580
581 #[test]
582 fn test_partial_config_ext() {
583 let partial = PartialConfig {
584 required_edition: Some("2024".to_string()),
585 max_file_lines: Some(300),
586 ..Default::default()
587 };
588
589 let keys = partial.list_keys();
590 assert!(keys.contains(&"required_edition".to_string()));
591 assert!(keys.contains(&"max_file_lines".to_string()));
592 assert_eq!(partial.count_set_fields(), 2);
593 }
594}