1pub mod profiles;
4
5use crate::error::{CliError, Result};
6use serde::{Deserialize, Serialize};
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::{Duration, Instant};
11use voirs_sdk::config::AppConfig;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CliConfig {
16 #[serde(flatten)]
18 pub core: AppConfig,
19
20 pub cli: CliSettings,
22}
23
24pub type Config = CliConfig;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CliSettings {
30 pub default_output_format: String,
32
33 pub default_voice: Option<String>,
35
36 pub default_quality: String,
38
39 pub colored_output: bool,
41
42 pub show_progress: bool,
44
45 pub auto_play: bool,
47
48 pub output_directory: Option<PathBuf>,
50
51 pub ssml_validation: SsmlValidationLevel,
53
54 pub history_size: usize,
56
57 pub download: DownloadSettings,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum SsmlValidationLevel {
64 None,
66 Warn,
68 Strict,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct DownloadSettings {
75 pub parallel_downloads: usize,
77
78 pub retry_attempts: usize,
80
81 pub verify_checksums: bool,
83
84 pub preferred_mirrors: Vec<String>,
86}
87
88impl Default for CliConfig {
89 fn default() -> Self {
90 Self {
91 core: AppConfig::default(),
92 cli: CliSettings::default(),
93 }
94 }
95}
96
97impl Default for CliSettings {
98 fn default() -> Self {
99 Self {
100 default_output_format: "wav".to_string(),
101 default_voice: None,
102 default_quality: "high".to_string(),
103 colored_output: true,
104 show_progress: true,
105 auto_play: false,
106 output_directory: None,
107 ssml_validation: SsmlValidationLevel::Warn,
108 history_size: 100,
109 download: DownloadSettings::default(),
110 }
111 }
112}
113
114impl Default for DownloadSettings {
115 fn default() -> Self {
116 Self {
117 parallel_downloads: 3,
118 retry_attempts: 3,
119 verify_checksums: true,
120 preferred_mirrors: vec![
121 "https://huggingface.co".to_string(),
122 "https://github.com".to_string(),
123 ],
124 }
125 }
126}
127
128pub struct ConfigManager {
130 config_path: PathBuf,
131 config: CliConfig,
132}
133
134impl ConfigManager {
135 pub fn new() -> Result<Self> {
137 let config_path = Self::find_config_file().unwrap_or_else(Self::default_config_path);
138
139 let config = if config_path.exists() {
140 Self::load_from_file(&config_path)?
141 } else {
142 CliConfig::default()
143 };
144
145 Ok(Self {
146 config_path,
147 config,
148 })
149 }
150
151 pub fn with_path<P: AsRef<Path>>(path: P) -> Result<Self> {
153 let config_path = path.as_ref().to_path_buf();
154
155 let config = if config_path.exists() {
156 Self::load_from_file(&config_path)?
157 } else {
158 CliConfig::default()
159 };
160
161 Ok(Self {
162 config_path,
163 config,
164 })
165 }
166
167 pub fn config(&self) -> &CliConfig {
169 &self.config
170 }
171
172 pub fn config_mut(&mut self) -> &mut CliConfig {
174 &mut self.config
175 }
176
177 pub fn save(&self) -> Result<()> {
179 if let Some(parent) = self.config_path.parent() {
181 fs::create_dir_all(parent).map_err(|e| {
182 CliError::file_operation("create directory", &parent.display().to_string(), e)
183 })?;
184 }
185
186 let content = toml::to_string_pretty(&self.config).map_err(CliError::from)?;
187
188 fs::write(&self.config_path, content).map_err(|e| {
189 CliError::file_operation("write", &self.config_path.display().to_string(), e)
190 })?;
191
192 Ok(())
193 }
194
195 pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
197 match key {
198 "default_output_format" => {
199 self.config.cli.default_output_format = value.to_string();
200 }
201 "default_voice" => {
202 self.config.cli.default_voice = if value.is_empty() {
203 None
204 } else {
205 Some(value.to_string())
206 };
207 }
208 "default_quality" => {
209 if ["low", "medium", "high", "ultra"].contains(&value) {
210 self.config.cli.default_quality = value.to_string();
211 } else {
212 return Err(CliError::invalid_parameter(
213 key,
214 "must be one of: low, medium, high, ultra",
215 ));
216 }
217 }
218 "colored_output" => {
219 self.config.cli.colored_output = value
220 .parse()
221 .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
222 }
223 "show_progress" => {
224 self.config.cli.show_progress = value
225 .parse()
226 .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
227 }
228 "auto_play" => {
229 self.config.cli.auto_play = value
230 .parse()
231 .map_err(|_| CliError::invalid_parameter(key, "must be true or false"))?;
232 }
233 "output_directory" => {
234 self.config.cli.output_directory = if value.is_empty() {
235 None
236 } else {
237 Some(PathBuf::from(value))
238 };
239 }
240 _ => {
241 return Err(CliError::invalid_parameter(
242 key,
243 "unknown configuration key",
244 ));
245 }
246 }
247
248 Ok(())
249 }
250
251 pub fn get_value(&self, key: &str) -> Option<String> {
253 match key {
254 "default_output_format" => Some(self.config.cli.default_output_format.clone()),
255 "default_voice" => self.config.cli.default_voice.clone(),
256 "default_quality" => Some(self.config.cli.default_quality.clone()),
257 "colored_output" => Some(self.config.cli.colored_output.to_string()),
258 "show_progress" => Some(self.config.cli.show_progress.to_string()),
259 "auto_play" => Some(self.config.cli.auto_play.to_string()),
260 "output_directory" => self
261 .config
262 .cli
263 .output_directory
264 .as_ref()
265 .map(|p| p.display().to_string()),
266 _ => None,
267 }
268 }
269
270 pub fn apply_env_overrides(&mut self) {
272 if let Ok(format) = env::var("VOIRS_OUTPUT_FORMAT") {
273 self.config.cli.default_output_format = format;
274 }
275
276 if let Ok(voice) = env::var("VOIRS_DEFAULT_VOICE") {
277 self.config.cli.default_voice = Some(voice);
278 }
279
280 if let Ok(quality) = env::var("VOIRS_QUALITY") {
281 if ["low", "medium", "high", "ultra"].contains(&quality.as_str()) {
282 self.config.cli.default_quality = quality;
283 }
284 }
285
286 if let Ok(colored) = env::var("VOIRS_COLORED_OUTPUT") {
287 if let Ok(value) = colored.parse() {
288 self.config.cli.colored_output = value;
289 }
290 }
291
292 if let Ok(progress) = env::var("VOIRS_SHOW_PROGRESS") {
293 if let Ok(value) = progress.parse() {
294 self.config.cli.show_progress = value;
295 }
296 }
297
298 if let Ok(output_dir) = env::var("VOIRS_OUTPUT_DIR") {
299 self.config.cli.output_directory = Some(PathBuf::from(output_dir));
300 }
301 }
302
303 pub fn validate(&self) -> Result<Vec<String>> {
305 let mut warnings = Vec::new();
306
307 if let Some(ref voice) = self.config.cli.default_voice {
309 match self.validate_voice_exists(voice) {
311 Ok(true) => {
312 }
314 Ok(false) => {
315 warnings.push(format!(
316 "Default voice '{}' does not exist. Use 'voirs voices list' to see available voices.",
317 voice
318 ));
319 }
320 Err(_) => {
321 warnings.push(format!(
324 "Could not verify existence of default voice '{}'. Voice system may not be initialized.",
325 voice
326 ));
327 }
328 }
329 }
330
331 if let Some(ref output_dir) = self.config.cli.output_directory {
333 if !output_dir.exists() {
334 warnings.push(format!(
335 "Output directory '{}' does not exist",
336 output_dir.display()
337 ));
338 } else if !output_dir.is_dir() {
339 return Err(CliError::config(format!(
340 "Output directory '{}' is not a directory",
341 output_dir.display()
342 )));
343 }
344 }
345
346 if self.config.cli.download.parallel_downloads == 0 {
348 return Err(CliError::config(
349 "parallel_downloads must be greater than 0",
350 ));
351 }
352
353 if self.config.cli.download.parallel_downloads > 10 {
354 warnings.push("parallel_downloads > 10 may cause server rate limiting".to_string());
355 }
356
357 Ok(warnings)
358 }
359
360 fn validate_voice_exists(&self, voice_id: &str) -> Result<bool> {
362 let voice_dirs = self.get_voice_directories();
367
368 for voice_dir in voice_dirs {
369 let voice_config_path = voice_dir.join(voice_id).join("voice.json");
370 if voice_config_path.exists() {
371 return Ok(true);
373 }
374 }
375
376 Ok(false)
378 }
379
380 fn get_voice_directories(&self) -> Vec<PathBuf> {
382 let mut dirs = Vec::new();
383
384 if let Some(home) = dirs::home_dir() {
386 dirs.push(home.join(".voirs").join("voices"));
387 }
388
389 #[cfg(target_os = "linux")]
391 {
392 if let Ok(xdg_data_home) = std::env::var("XDG_DATA_HOME") {
393 dirs.push(PathBuf::from(xdg_data_home).join("voirs").join("voices"));
394 } else if let Some(home) = dirs::home_dir() {
395 dirs.push(
396 home.join(".local")
397 .join("share")
398 .join("voirs")
399 .join("voices"),
400 );
401 }
402 }
403
404 #[cfg(target_os = "macos")]
406 {
407 if let Some(home) = dirs::home_dir() {
408 dirs.push(
409 home.join("Library")
410 .join("Application Support")
411 .join("voirs")
412 .join("voices"),
413 );
414 }
415 }
416
417 #[cfg(target_os = "windows")]
419 {
420 if let Ok(appdata) = std::env::var("APPDATA") {
421 dirs.push(PathBuf::from(appdata).join("voirs").join("voices"));
422 }
423 }
424
425 dirs.push(PathBuf::from("./voices"));
427
428 dirs
429 }
430
431 pub fn config_path(&self) -> &Path {
433 &self.config_path
434 }
435
436 fn load_from_file<P: AsRef<Path>>(path: P) -> Result<CliConfig> {
438 let content = fs::read_to_string(path.as_ref()).map_err(|e| {
439 CliError::file_operation("read", &path.as_ref().display().to_string(), e)
440 })?;
441
442 if let Ok(config) = toml::from_str::<CliConfig>(&content) {
444 Ok(config)
445 } else {
446 serde_json::from_str::<CliConfig>(&content)
447 .map_err(|e| CliError::config(format!("Invalid configuration format: {}", e)))
448 }
449 }
450
451 fn find_config_file() -> Option<PathBuf> {
453 let possible_paths = [
454 env::current_dir().ok().map(|d| d.join("voirs.toml")),
455 env::current_dir().ok().map(|d| d.join("voirs.json")),
456 Self::config_dir().map(|d| d.join("voirs.toml")),
457 Self::config_dir().map(|d| d.join("voirs.json")),
458 env::var("VOIRS_CONFIG").ok().map(PathBuf::from),
459 ];
460
461 possible_paths
462 .into_iter()
463 .flatten()
464 .find(|path| path.exists())
465 }
466
467 fn default_config_path() -> PathBuf {
469 Self::config_dir()
470 .unwrap_or_else(|| env::current_dir().unwrap())
471 .join("voirs.toml")
472 }
473
474 fn config_dir() -> Option<PathBuf> {
476 if let Some(config_dir) = env::var_os("XDG_CONFIG_HOME") {
477 Some(PathBuf::from(config_dir).join("voirs"))
478 } else if let Some(home_dir) = env::var_os("HOME") {
479 Some(PathBuf::from(home_dir).join(".config").join("voirs"))
480 } else {
481 env::var_os("APPDATA").map(|app_data| PathBuf::from(app_data).join("voirs"))
482 }
483 }
484}
485
486pub mod utils {
488 use super::*;
489
490 pub fn create_default_config<P: AsRef<Path>>(path: P) -> Result<()> {
492 let config = CliConfig::default();
493 let content = toml::to_string_pretty(&config).map_err(CliError::from)?;
494
495 if let Some(parent) = path.as_ref().parent() {
496 fs::create_dir_all(parent).map_err(|e| {
497 CliError::file_operation("create directory", &parent.display().to_string(), e)
498 })?;
499 }
500
501 fs::write(path.as_ref(), content).map_err(|e| {
502 CliError::file_operation("write", &path.as_ref().display().to_string(), e)
503 })?;
504
505 Ok(())
506 }
507
508 pub fn migrate_config<P: AsRef<Path>>(old_path: P, new_path: P) -> Result<()> {
510 let old_content = fs::read_to_string(old_path.as_ref()).map_err(|e| {
511 CliError::file_operation("read", &old_path.as_ref().display().to_string(), e)
512 })?;
513
514 let old_config: serde_json::Value = serde_json::from_str(&old_content)
516 .map_err(|e| CliError::config(format!("Cannot parse old config: {}", e)))?;
517
518 let mut new_config = CliConfig::default();
520
521 if let Some(output_format) = old_config.get("output_format") {
523 if let Some(format_str) = output_format.as_str() {
524 new_config.cli.default_output_format = format_str.to_string();
525 }
526 }
527
528 let content = toml::to_string_pretty(&new_config).map_err(CliError::from)?;
530
531 fs::write(new_path.as_ref(), content).map_err(|e| {
532 CliError::file_operation("write", &new_path.as_ref().display().to_string(), e)
533 })?;
534
535 Ok(())
536 }
537
538 pub fn export_config<P: AsRef<Path>>(
540 config: &CliConfig,
541 path: P,
542 format: ConfigFormat,
543 ) -> Result<()> {
544 let content = match format {
545 ConfigFormat::Toml => toml::to_string_pretty(config)?,
546 ConfigFormat::Json => serde_json::to_string_pretty(config)?,
547 ConfigFormat::Yaml => serde_yaml::to_string(config)
548 .map_err(|e| CliError::config(format!("YAML serialization error: {}", e)))?,
549 };
550
551 fs::write(path.as_ref(), content).map_err(|e| {
552 CliError::file_operation("write", &path.as_ref().display().to_string(), e)
553 })?;
554
555 Ok(())
556 }
557}
558
559pub struct EnhancedConfigLoader {
561 cache: Option<(PathBuf, std::time::SystemTime, CliConfig)>,
562}
563
564impl EnhancedConfigLoader {
565 pub fn new() -> Self {
567 Self { cache: None }
568 }
569
570 pub fn load_config<P: AsRef<Path>>(&mut self, path: P) -> Result<CliConfig> {
572 let path = path.as_ref();
573 let start_time = Instant::now();
574
575 if let Some((cached_path, cached_time, ref cached_config)) = &self.cache {
577 if cached_path == path {
578 if let Ok(metadata) = fs::metadata(path) {
579 if let Ok(modified) = metadata.modified() {
580 if modified <= *cached_time {
581 return Ok(cached_config.clone());
583 }
584 }
585 }
586 }
587 }
588
589 let content = fs::read_to_string(path)
591 .map_err(|e| CliError::file_operation("read", &path.display().to_string(), e))?;
592
593 let config = self.parse_config_content(&content, path)?;
594
595 if let Ok(metadata) = fs::metadata(path) {
597 if let Ok(modified) = metadata.modified() {
598 self.cache = Some((path.to_path_buf(), modified, config.clone()));
599 }
600 }
601
602 let load_time = start_time.elapsed();
603 if load_time > Duration::from_millis(100) {
604 eprintln!("Warning: Configuration loading took {:?}", load_time);
605 }
606
607 Ok(config)
608 }
609
610 fn parse_config_content<P: AsRef<Path>>(&self, content: &str, path: P) -> Result<CliConfig> {
612 let extension = path.as_ref().extension().and_then(|ext| ext.to_str());
613
614 match extension {
616 Some("toml") => {
617 match toml::from_str::<CliConfig>(content) {
618 Ok(config) => return Ok(config),
619 Err(e) => {
620 eprintln!("TOML parsing failed: {}, trying fallback formats", e);
622 }
623 }
624 }
625 Some("json") => match serde_json::from_str::<CliConfig>(content) {
626 Ok(config) => return Ok(config),
627 Err(e) => {
628 eprintln!("JSON parsing failed: {}, trying fallback formats", e);
629 }
630 },
631 Some("yaml") | Some("yml") => match serde_yaml::from_str::<CliConfig>(content) {
632 Ok(config) => return Ok(config),
633 Err(e) => {
634 eprintln!("YAML parsing failed: {}, trying fallback formats", e);
635 }
636 },
637 _ => {
638 }
640 }
641
642 let trimmed = content.trim();
644
645 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
647 if let Ok(config) = toml::from_str::<CliConfig>(content) {
648 return Ok(config);
649 }
650 }
651
652 if trimmed.starts_with('{') && trimmed.ends_with('}') {
654 if let Ok(config) = serde_json::from_str::<CliConfig>(content) {
655 return Ok(config);
656 }
657 }
658
659 if let Ok(config) = serde_yaml::from_str::<CliConfig>(content) {
661 return Ok(config);
662 }
663
664 Err(CliError::config(format!(
665 "Unable to parse configuration file '{}' - tried TOML, JSON, and YAML formats",
666 path.as_ref().display()
667 )))
668 }
669
670 pub fn clear_cache(&mut self) {
672 self.cache = None;
673 }
674
675 pub fn is_cached<P: AsRef<Path>>(&self, path: P) -> bool {
677 if let Some((cached_path, _, _)) = &self.cache {
678 cached_path == path.as_ref()
679 } else {
680 false
681 }
682 }
683
684 pub fn cache_stats(&self) -> Option<(PathBuf, std::time::SystemTime)> {
686 self.cache
687 .as_ref()
688 .map(|(path, time, _)| (path.clone(), *time))
689 }
690}
691
692impl Default for EnhancedConfigLoader {
693 fn default() -> Self {
694 Self::new()
695 }
696}
697
698pub mod validation {
700 use super::*;
701 use std::time::{Duration, Instant};
702
703 pub fn validate_config_detailed(config: &CliConfig) -> Result<ValidationReport> {
705 let mut report = ValidationReport::new();
706 let start_time = Instant::now();
707
708 validate_cli_settings(&config.cli, &mut report)?;
710
711 validate_core_config(&config.core, &mut report)?;
713
714 let validation_time = start_time.elapsed();
716 if validation_time > Duration::from_millis(50) {
717 report.add_warning(format!(
718 "Configuration validation took {:?} - consider optimizing",
719 validation_time
720 ));
721 }
722
723 Ok(report)
724 }
725
726 fn validate_cli_settings(settings: &CliSettings, report: &mut ValidationReport) -> Result<()> {
728 let valid_formats = ["wav", "mp3", "flac", "ogg", "m4a"];
730 if !valid_formats.contains(&settings.default_output_format.as_str()) {
731 report.add_error(format!(
732 "Invalid default output format '{}'. Valid formats: {}",
733 settings.default_output_format,
734 valid_formats.join(", ")
735 ));
736 }
737
738 let valid_qualities = ["low", "medium", "high", "ultra"];
740 if !valid_qualities.contains(&settings.default_quality.as_str()) {
741 report.add_error(format!(
742 "Invalid default quality '{}'. Valid qualities: {}",
743 settings.default_quality,
744 valid_qualities.join(", ")
745 ));
746 }
747
748 if let Some(ref output_dir) = settings.output_directory {
750 if !output_dir.exists() {
751 report.add_warning(format!(
752 "Output directory '{}' does not exist",
753 output_dir.display()
754 ));
755 } else if !output_dir.is_dir() {
756 report.add_error(format!(
757 "Output directory '{}' is not a directory",
758 output_dir.display()
759 ));
760 }
761 }
762
763 if settings.download.parallel_downloads == 0 {
765 report.add_error("parallel_downloads must be greater than 0".to_string());
766 } else if settings.download.parallel_downloads > 20 {
767 report.add_warning(format!(
768 "parallel_downloads ({}) is very high and may cause issues",
769 settings.download.parallel_downloads
770 ));
771 }
772
773 if settings.download.retry_attempts > 10 {
774 report.add_warning(format!(
775 "retry_attempts ({}) is very high and may cause long delays",
776 settings.download.retry_attempts
777 ));
778 }
779
780 Ok(())
781 }
782
783 fn validate_core_config(config: &AppConfig, report: &mut ValidationReport) -> Result<()> {
785 match config.pipeline.device.as_str() {
787 "cpu" => {
788 report.add_info("Using CPU device - synthesis will be slower than GPU".to_string());
789 }
790 "gpu" | "cuda" => {
791 report.add_info("GPU acceleration enabled - ensure CUDA is available".to_string());
792 #[cfg(not(feature = "cuda"))]
793 report.add_warning(
794 "GPU device specified but CUDA feature not enabled in build".to_string(),
795 );
796 }
797 "metal" => {
798 report.add_info("Metal acceleration enabled - macOS only".to_string());
799 #[cfg(not(target_os = "macos"))]
800 report.add_error("Metal device is only available on macOS".to_string());
801 #[cfg(not(feature = "metal"))]
802 report.add_warning(
803 "Metal device specified but metal feature not enabled in build".to_string(),
804 );
805 }
806 other => {
807 report.add_error(format!(
808 "Invalid device '{}' - must be 'cpu', 'gpu', 'cuda', or 'metal'",
809 other
810 ));
811 }
812 }
813
814 if let Some(threads) = config.pipeline.num_threads {
816 if threads == 0 {
817 report.add_error("num_threads must be greater than 0".to_string());
818 } else if threads > num_cpus::get() * 2 {
819 report.add_warning(format!(
820 "num_threads ({}) exceeds 2x CPU count ({}) - may cause overhead",
821 threads,
822 num_cpus::get()
823 ));
824 }
825 }
826
827 let sample_rate = config.pipeline.default_synthesis.sample_rate;
829 match sample_rate {
830 8000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000 => {
831 }
833 rate if rate < 8000 => {
834 report.add_error(format!("sample_rate {} is too low - minimum 8000 Hz", rate));
835 }
836 rate if rate > 48000 => {
837 report.add_warning(format!(
838 "sample_rate {} is very high - may increase processing time",
839 rate
840 ));
841 }
842 rate => {
843 report.add_warning(format!(
844 "non-standard sample_rate {} - common rates: 16000, 22050, 44100, 48000",
845 rate
846 ));
847 }
848 }
849
850 if let Some(cache_dir) = &config.pipeline.cache_dir {
852 if !cache_dir.exists() {
853 report.add_warning(format!(
854 "cache directory does not exist: {}",
855 cache_dir.display()
856 ));
857 } else if !cache_dir.is_dir() {
858 report.add_error(format!(
859 "cache path exists but is not a directory: {}",
860 cache_dir.display()
861 ));
862 }
863 }
864
865 let max_cache_size_mb = config.pipeline.max_cache_size_mb;
867 if max_cache_size_mb == 0 {
868 report.add_warning("cache disabled (max_cache_size_mb = 0)".to_string());
869 } else if max_cache_size_mb > 10240 {
870 report.add_warning(format!(
871 "very large cache size ({} MB) may consume excessive memory",
872 max_cache_size_mb
873 ));
874 }
875
876 if config.pipeline.use_gpu && config.pipeline.device == "cpu" {
878 report.add_warning(
879 "use_gpu is true but device is set to 'cpu' - inconsistent configuration"
880 .to_string(),
881 );
882 }
883
884 Ok(())
885 }
886
887 #[derive(Debug, Clone)]
889 pub struct ValidationReport {
890 pub errors: Vec<String>,
891 pub warnings: Vec<String>,
892 pub info: Vec<String>,
893 }
894
895 impl ValidationReport {
896 pub fn new() -> Self {
897 Self {
898 errors: Vec::new(),
899 warnings: Vec::new(),
900 info: Vec::new(),
901 }
902 }
903
904 pub fn add_error(&mut self, error: String) {
905 self.errors.push(error);
906 }
907
908 pub fn add_warning(&mut self, warning: String) {
909 self.warnings.push(warning);
910 }
911
912 pub fn add_info(&mut self, info: String) {
913 self.info.push(info);
914 }
915
916 pub fn is_valid(&self) -> bool {
917 self.errors.is_empty()
918 }
919
920 pub fn has_warnings(&self) -> bool {
921 !self.warnings.is_empty()
922 }
923
924 pub fn summary(&self) -> String {
925 format!(
926 "Validation complete: {} errors, {} warnings, {} info messages",
927 self.errors.len(),
928 self.warnings.len(),
929 self.info.len()
930 )
931 }
932 }
933
934 impl Default for ValidationReport {
935 fn default() -> Self {
936 Self::new()
937 }
938 }
939}
940
941pub enum ConfigFormat {
943 Toml,
944 Json,
945 Yaml,
946}