1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::errors::DataProfilerError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13#[serde(default)]
14pub struct QualityScoreWeights {
15 pub completeness: f64,
16 pub consistency: f64,
17 pub uniqueness: f64,
18 pub accuracy: f64,
19 pub timeliness: f64,
20 pub validity: f64,
21 pub precision: f64,
22}
23
24impl Default for QualityScoreWeights {
25 fn default() -> Self {
26 Self {
27 completeness: 0.25,
28 consistency: 0.20,
29 uniqueness: 0.15,
30 accuracy: 0.15,
31 timeliness: 0.10,
32 validity: 0.10,
33 precision: 0.05,
34 }
35 }
36}
37
38impl QualityScoreWeights {
39 pub fn is_default(&self) -> bool {
40 self == &Self::default()
41 }
42
43 pub fn is_valid(&self) -> bool {
44 let weights = [
45 self.completeness,
46 self.consistency,
47 self.uniqueness,
48 self.accuracy,
49 self.timeliness,
50 self.validity,
51 self.precision,
52 ];
53 weights
54 .iter()
55 .all(|weight| weight.is_finite() && *weight >= 0.0)
56 && weights.iter().any(|weight| *weight > 0.0)
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct IsoQualityConfig {
64 pub max_null_percentage: f64,
66 pub null_report_threshold: f64,
68 pub min_type_consistency: f64,
70 pub duplicate_report_threshold: f64,
72 pub high_cardinality_threshold: f64,
74 pub outlier_iqr_multiplier: f64,
76 pub outlier_min_samples: usize,
78 pub max_data_age_years: f64,
80 pub stale_data_threshold: f64,
82 #[serde(default)]
84 pub score_weights: QualityScoreWeights,
85}
86
87impl Default for IsoQualityConfig {
88 fn default() -> Self {
89 Self {
90 max_null_percentage: 50.0,
91 null_report_threshold: 10.0,
92 min_type_consistency: 95.0,
93 duplicate_report_threshold: 5.0,
94 high_cardinality_threshold: 95.0,
95 outlier_iqr_multiplier: 1.5,
96 outlier_min_samples: 4,
97 max_data_age_years: 5.0,
98 stale_data_threshold: 20.0,
99 score_weights: QualityScoreWeights::default(),
100 }
101 }
102}
103
104impl IsoQualityConfig {
105 pub fn strict() -> Self {
107 Self {
108 max_null_percentage: 30.0,
109 null_report_threshold: 5.0,
110 min_type_consistency: 98.0,
111 duplicate_report_threshold: 1.0,
112 high_cardinality_threshold: 98.0,
113 outlier_iqr_multiplier: 1.5,
114 outlier_min_samples: 10,
115 max_data_age_years: 2.0,
116 stale_data_threshold: 10.0,
117 score_weights: QualityScoreWeights::default(),
118 }
119 }
120
121 pub fn lenient() -> Self {
123 Self {
124 max_null_percentage: 70.0,
125 null_report_threshold: 20.0,
126 min_type_consistency: 90.0,
127 duplicate_report_threshold: 10.0,
128 high_cardinality_threshold: 90.0,
129 outlier_iqr_multiplier: 2.0,
130 outlier_min_samples: 4,
131 max_data_age_years: 10.0,
132 stale_data_threshold: 30.0,
133 score_weights: QualityScoreWeights::default(),
134 }
135 }
136}
137
138const DEFAULT_OUTPUT_FORMAT: &str = "text";
148
149const DEFAULT_COLORED_OUTPUT: bool = true;
152
153const DEFAULT_VERBOSITY: u8 = 1;
157
158const DEFAULT_SHOW_PROGRESS: bool = true;
161
162const DEFAULT_QUALITY_ENABLED: bool = true;
167
168const DEFAULT_ENGINE: &str = "auto";
172
173const DEFAULT_PARALLEL_PROCESSING: bool = true;
176
177const DEFAULT_MAX_MEMORY_MB: usize = 0;
181
182const DEFAULT_MEMORY_MONITORING: bool = true;
185
186const DEFAULT_AUTO_STREAMING_THRESHOLD_MB: f64 = 100.0;
190
191#[cfg(feature = "database")]
193const DEFAULT_DB_CONNECTION_TIMEOUT_SECS: u64 = 30;
196
197#[cfg(feature = "database")]
198const DEFAULT_DB_BATCH_SIZE: usize = 10_000;
201
202#[cfg(feature = "database")]
203const DEFAULT_DB_MAX_CONNECTIONS: usize = 10;
206
207#[cfg(feature = "database")]
208const DEFAULT_DB_SSL_ENABLED: bool = true;
211
212#[cfg(feature = "database")]
213const DEFAULT_DB_SAMPLING_ENABLED: bool = true;
216
217#[cfg(feature = "database")]
218const DEFAULT_DB_SAMPLE_SIZE: usize = 100_000;
222
223#[cfg(feature = "database")]
224const DEFAULT_DB_AUTO_SAMPLE_THRESHOLD: usize = 1_000_000;
227
228#[derive(Debug, Clone, Serialize, Deserialize, Default)]
230pub struct DataprofConfig {
231 pub output: OutputConfig,
233
234 pub quality: QualityConfig,
236
237 pub engine: EngineConfig,
239
240 #[cfg(feature = "database")]
242 pub database: Option<DatabaseSettings>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct OutputConfig {
247 pub default_format: String,
249
250 pub colored: bool,
252
253 pub verbosity: u8,
255
256 pub show_progress: bool,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct QualityConfig {
262 pub enabled: bool,
264
265 pub iso_thresholds: IsoQualityConfig,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct EngineConfig {
279 pub default_engine: String,
281
282 pub default_chunk_size: Option<usize>,
284
285 pub parallel: bool,
287
288 pub max_concurrent: usize,
290
291 pub memory: MemoryConfig,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct MemoryConfig {
297 pub max_usage_mb: usize,
299
300 pub monitor: bool,
302
303 pub auto_streaming_threshold_mb: f64,
305}
306
307#[cfg(feature = "database")]
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct DatabaseSettings {
310 pub connection_timeout: u64,
312
313 pub batch_size: usize,
315
316 pub max_connections: usize,
318
319 pub ssl_enabled: bool,
321
322 pub sampling: DatabaseSamplingConfig,
324}
325
326#[cfg(feature = "database")]
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct DatabaseSamplingConfig {
329 pub enabled: bool,
331
332 pub default_sample_size: usize,
334
335 pub auto_sample_threshold: usize,
337}
338
339impl Default for OutputConfig {
340 fn default() -> Self {
341 Self {
342 default_format: DEFAULT_OUTPUT_FORMAT.to_string(),
343 colored: DEFAULT_COLORED_OUTPUT,
344 verbosity: DEFAULT_VERBOSITY,
345 show_progress: DEFAULT_SHOW_PROGRESS,
346 }
347 }
348}
349
350impl Default for QualityConfig {
351 fn default() -> Self {
352 Self {
353 enabled: DEFAULT_QUALITY_ENABLED,
354 iso_thresholds: IsoQualityConfig::default(),
355 }
356 }
357}
358
359impl Default for EngineConfig {
360 fn default() -> Self {
361 Self {
362 default_engine: DEFAULT_ENGINE.to_string(),
363 default_chunk_size: None,
364 parallel: DEFAULT_PARALLEL_PROCESSING,
365 max_concurrent: num_cpus::get(),
366 memory: MemoryConfig::default(),
367 }
368 }
369}
370
371impl Default for MemoryConfig {
372 fn default() -> Self {
373 Self {
374 max_usage_mb: DEFAULT_MAX_MEMORY_MB,
375 monitor: DEFAULT_MEMORY_MONITORING,
376 auto_streaming_threshold_mb: DEFAULT_AUTO_STREAMING_THRESHOLD_MB,
377 }
378 }
379}
380
381#[cfg(feature = "database")]
382impl Default for DatabaseSettings {
383 fn default() -> Self {
384 Self {
385 connection_timeout: DEFAULT_DB_CONNECTION_TIMEOUT_SECS,
386 batch_size: DEFAULT_DB_BATCH_SIZE,
387 max_connections: DEFAULT_DB_MAX_CONNECTIONS,
388 ssl_enabled: DEFAULT_DB_SSL_ENABLED,
389 sampling: DatabaseSamplingConfig::default(),
390 }
391 }
392}
393
394#[cfg(feature = "database")]
395impl Default for DatabaseSamplingConfig {
396 fn default() -> Self {
397 Self {
398 enabled: DEFAULT_DB_SAMPLING_ENABLED,
399 default_sample_size: DEFAULT_DB_SAMPLE_SIZE,
400 auto_sample_threshold: DEFAULT_DB_AUTO_SAMPLE_THRESHOLD,
401 }
402 }
403}
404
405impl DataprofConfig {
406 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, DataProfilerError> {
408 let content = fs::read_to_string(path)?;
409 let config: DataprofConfig = toml::from_str(&content)?;
410 Ok(config)
411 }
412
413 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DataProfilerError> {
415 let content = toml::to_string_pretty(self)?;
416 fs::write(path, content)?;
417 Ok(())
418 }
419
420 pub fn load_with_discovery() -> Self {
431 let mut config_paths = vec![
433 PathBuf::from(".dataprof.toml"),
435 PathBuf::from("dataprof.toml"),
436 ];
437
438 if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
440 let user_config = PathBuf::from(home)
441 .join(".config")
442 .join("dataprof")
443 .join("config.toml");
444 config_paths.push(user_config);
445 }
446
447 for path in &config_paths {
449 if path.exists() {
450 match Self::load_from_file(path) {
451 Ok(mut config) => {
452 log::info!("Loaded configuration from: {}", path.display());
453 config.apply_env_overrides();
455 return config;
456 }
457 Err(e) => {
458 log::warn!(
459 "Found config file at {} but failed to load: {}",
460 path.display(),
461 e
462 );
463 }
464 }
465 }
466 }
467
468 log::debug!("No configuration file found. Using defaults with environment overrides.");
470 let mut config = Self::default();
471 config.apply_env_overrides();
472 config
473 }
474
475 pub fn apply_env_overrides(&mut self) {
477 if let Ok(format) = std::env::var("DATAPROF_FORMAT") {
479 self.output.default_format = format;
480 }
481
482 if let Ok(verbosity) = std::env::var("DATAPROF_VERBOSITY")
484 && let Ok(level) = verbosity.parse::<u8>()
485 {
486 self.output.verbosity = level;
487 }
488
489 if let Ok(engine) = std::env::var("DATAPROF_ENGINE") {
491 self.engine.default_engine = engine;
492 }
493
494 if let Ok(quality) = std::env::var("DATAPROF_QUALITY")
496 && let Ok(enabled) = quality.parse::<bool>()
497 {
498 self.quality.enabled = enabled;
499 }
500
501 if std::env::var("NO_COLOR").is_ok() {
503 self.output.colored = false;
504 }
505
506 if let Ok(progress) = std::env::var("DATAPROF_PROGRESS")
508 && let Ok(enabled) = progress.parse::<bool>()
509 {
510 self.output.show_progress = enabled;
511 }
512 }
513
514 pub fn apply_overrides(
516 &mut self,
517 format: Option<&str>,
518 quality: Option<bool>,
519 progress: Option<bool>,
520 ) {
521 if let Some(format) = format {
522 self.output.default_format = format.to_string();
523 }
524
525 if let Some(quality) = quality {
526 self.quality.enabled = quality;
527 }
528
529 if let Some(progress) = progress {
530 self.output.show_progress = progress;
531 }
532 }
533
534 #[deprecated(note = "use DataprofConfig::apply_overrides instead")]
536 pub fn merge_with_cli_args(
537 &mut self,
538 format: Option<&str>,
539 quality: Option<bool>,
540 progress: Option<bool>,
541 ) {
542 self.apply_overrides(format, quality, progress);
543 }
544
545 pub fn create_sample_config<P: AsRef<Path>>(path: P) -> Result<(), DataProfilerError> {
547 let sample_config = Self::default();
548 sample_config.save_to_file(path)?;
549 Ok(())
550 }
551}
552
553impl DataprofConfig {
555 pub fn validate(&self) -> Result<(), DataProfilerError> {
566 let valid_formats = ["text", "json", "csv", "plain"];
568 if !valid_formats.contains(&self.output.default_format.as_str()) {
569 return Err(DataProfilerError::ConfigValidationError {
570 message: format!(
571 "Invalid output format '{}'. Valid formats: {}\n\
572 → Fix: Set output.default_format to one of the valid formats in your config file.",
573 self.output.default_format,
574 valid_formats.join(", ")
575 ),
576 });
577 }
578
579 if self.output.verbosity > 3 {
581 return Err(DataProfilerError::ConfigValidationError {
582 message: format!(
583 "Invalid verbosity level {}. Must be between 0 (quiet) and 3 (debug).\n\
584 → Fix: Set output.verbosity to 0, 1, 2, or 3 in your config file.",
585 self.output.verbosity
586 ),
587 });
588 }
589
590 let iso = &self.quality.iso_thresholds;
592
593 if iso.outlier_iqr_multiplier <= 0.0 {
594 return Err(DataProfilerError::ConfigValidationError {
595 message: format!(
596 "IQR multiplier must be positive (standard value: 1.5), got {}.\n\
597 → Fix: Set quality.iso_thresholds.outlier_iqr_multiplier to a positive value.\n\
598 → Recommended: Use 1.5 (ISO standard) for normal cases, 3.0 for lenient detection.",
599 iso.outlier_iqr_multiplier
600 ),
601 });
602 }
603
604 if iso.max_null_percentage < 0.0 || iso.max_null_percentage > 100.0 {
605 return Err(DataProfilerError::ConfigValidationError {
606 message: format!(
607 "Max null percentage must be between 0 and 100, got {}.\n\
608 → Fix: Set quality.iso_thresholds.max_null_percentage to a value between 0.0 and 100.0.",
609 iso.max_null_percentage
610 ),
611 });
612 }
613
614 if iso.null_report_threshold < 0.0 || iso.null_report_threshold > 100.0 {
615 return Err(DataProfilerError::ConfigValidationError {
616 message: format!(
617 "Null report threshold must be between 0 and 100, got {}.\n\
618 → Fix: Set quality.iso_thresholds.null_report_threshold to a value between 0.0 and 100.0.",
619 iso.null_report_threshold
620 ),
621 });
622 }
623
624 if iso.min_type_consistency < 0.0 || iso.min_type_consistency > 100.0 {
625 return Err(DataProfilerError::ConfigValidationError {
626 message: format!(
627 "Min type consistency must be between 0 and 100, got {}.\n\
628 → Fix: Set quality.iso_thresholds.min_type_consistency to a value between 0.0 and 100.0.",
629 iso.min_type_consistency
630 ),
631 });
632 }
633
634 if iso.high_cardinality_threshold < 0.0 || iso.high_cardinality_threshold > 100.0 {
635 return Err(DataProfilerError::ConfigValidationError {
636 message: format!(
637 "High cardinality threshold must be between 0 and 100, got {}.\n\
638 → Fix: Set quality.iso_thresholds.high_cardinality_threshold to a value between 0.0 and 100.0.",
639 iso.high_cardinality_threshold
640 ),
641 });
642 }
643
644 if iso.duplicate_report_threshold < 0.0 || iso.duplicate_report_threshold > 100.0 {
645 return Err(DataProfilerError::ConfigValidationError {
646 message: format!(
647 "Duplicate report threshold must be between 0 and 100, got {}.\n\
648 → Fix: Set quality.iso_thresholds.duplicate_report_threshold to a value between 0.0 and 100.0.",
649 iso.duplicate_report_threshold
650 ),
651 });
652 }
653
654 if iso.max_data_age_years < 0.0 {
655 return Err(DataProfilerError::ConfigValidationError {
656 message: format!(
657 "Max data age must be non-negative, got {} years.\n\
658 → Fix: Set quality.iso_thresholds.max_data_age_years to a positive value.",
659 iso.max_data_age_years
660 ),
661 });
662 }
663
664 if iso.stale_data_threshold < 0.0 || iso.stale_data_threshold > 100.0 {
665 return Err(DataProfilerError::ConfigValidationError {
666 message: format!(
667 "Stale data threshold must be between 0 and 100, got {}.\n\
668 → Fix: Set quality.iso_thresholds.stale_data_threshold to a value between 0.0 and 100.0.",
669 iso.stale_data_threshold
670 ),
671 });
672 }
673
674 if !iso.score_weights.is_valid() {
675 return Err(DataProfilerError::ConfigValidationError {
676 message: "Quality score weights must be finite and non-negative, with at least one positive weight.\n\
677 → Fix: Set quality.iso_thresholds.score_weights to valid relative weights."
678 .to_string(),
679 });
680 }
681
682 let valid_engines = ["auto", "streaming", "memory_efficient", "true_streaming"];
684 if !valid_engines.contains(&self.engine.default_engine.as_str()) {
685 return Err(DataProfilerError::ConfigValidationError {
686 message: format!(
687 "Invalid engine '{}'. Valid engines: {}\n\
688 → Fix: Set engine.default_engine to one of the valid engines in your config file.\n\
689 → Recommended: Use 'auto' for automatic selection based on file size.",
690 self.engine.default_engine,
691 valid_engines.join(", ")
692 ),
693 });
694 }
695
696 if self.engine.memory.auto_streaming_threshold_mb < 0.0 {
698 return Err(DataProfilerError::ConfigValidationError {
699 message: format!(
700 "Auto-streaming threshold must be non-negative, got {} MB.\n\
701 → Fix: Set engine.memory.auto_streaming_threshold_mb to a positive value.\n\
702 → Recommended: 100.0 MB is a good default for most systems.",
703 self.engine.memory.auto_streaming_threshold_mb
704 ),
705 });
706 }
707
708 if let Some(chunk_size) = self.engine.default_chunk_size {
710 if chunk_size == 0 {
711 return Err(DataProfilerError::ConfigValidationError {
712 message: format!(
713 "Chunk size must be greater than 0, got {}.\n\
714 → Fix: Set engine.default_chunk_size to a positive value or null for adaptive sizing.\n\
715 → Recommended: 8192-65536 rows for most CSV files.",
716 chunk_size
717 ),
718 });
719 }
720
721 if chunk_size > 1_000_000 {
722 return Err(DataProfilerError::ConfigValidationError {
723 message: format!(
724 "Chunk size {} is very large and may cause memory issues.\n\
725 → Fix: Set engine.default_chunk_size to a smaller value.\n\
726 → Recommended: 8192-65536 rows for most CSV files.",
727 chunk_size
728 ),
729 });
730 }
731 }
732
733 if self.engine.max_concurrent == 0 {
735 return Err(DataProfilerError::ConfigValidationError {
736 message: "Max concurrent operations must be greater than 0.\n\
737 → Fix: Set engine.max_concurrent to a positive value.\n\
738 → Recommended: Use num_cpus::get() or leave unspecified for automatic detection."
739 .to_string(),
740 });
741 }
742
743 #[cfg(feature = "database")]
745 if let Some(ref db) = self.database {
746 if db.connection_timeout == 0 {
747 return Err(DataProfilerError::ConfigValidationError {
748 message: "Database connection timeout must be greater than 0 seconds.\n\
749 → Fix: Set database.connection_timeout to a positive value.\n\
750 → Recommended: 30 seconds for most network conditions."
751 .to_string(),
752 });
753 }
754
755 if db.batch_size == 0 {
756 return Err(DataProfilerError::ConfigValidationError {
757 message: "Database batch size must be greater than 0.\n\
758 → Fix: Set database.batch_size to a positive value.\n\
759 → Recommended: 10000 rows for most databases."
760 .to_string(),
761 });
762 }
763
764 if db.max_connections == 0 {
765 return Err(DataProfilerError::ConfigValidationError {
766 message: "Database max connections must be greater than 0.\n\
767 → Fix: Set database.max_connections to a positive value.\n\
768 → Recommended: 10 connections for most use cases."
769 .to_string(),
770 });
771 }
772
773 if db.sampling.default_sample_size == 0 {
774 return Err(DataProfilerError::ConfigValidationError {
775 message: "Database sample size must be greater than 0.\n\
776 → Fix: Set database.sampling.default_sample_size to a positive value.\n\
777 → Recommended: 100000 rows for statistical significance."
778 .to_string(),
779 });
780 }
781 }
782
783 Ok(())
784 }
785}
786
787#[derive(Debug, Clone)]
824pub struct DataprofConfigBuilder {
825 output: OutputConfig,
826 quality: QualityConfig,
827 engine: EngineConfig,
828 #[cfg(feature = "database")]
829 database: Option<DatabaseSettings>,
830}
831
832impl DataprofConfigBuilder {
833 pub fn new() -> Self {
835 Self {
836 output: OutputConfig::default(),
837 quality: QualityConfig::default(),
838 engine: EngineConfig::default(),
839 #[cfg(feature = "database")]
840 database: Some(DatabaseSettings::default()),
841 }
842 }
843
844 pub fn output_format(mut self, format: &str) -> Self {
852 self.output.default_format = format.to_string();
853 self
854 }
855
856 pub fn colored(mut self, enabled: bool) -> Self {
858 self.output.colored = enabled;
859 self
860 }
861
862 pub fn verbosity(mut self, level: u8) -> Self {
864 self.output.verbosity = level;
865 self
866 }
867
868 pub fn show_progress(mut self, enabled: bool) -> Self {
870 self.output.show_progress = enabled;
871 self
872 }
873
874 pub fn quality_enabled(mut self, enabled: bool) -> Self {
889 self.quality.enabled = enabled;
890 self
891 }
892
893 pub fn iso_quality_thresholds(mut self, thresholds: IsoQualityConfig) -> Self {
895 self.quality.iso_thresholds = thresholds;
896 self
897 }
898
899 pub fn quality_score_weights(mut self, weights: QualityScoreWeights) -> Self {
901 self.quality.iso_thresholds.score_weights = weights;
902 self
903 }
904
905 pub fn iso_quality_profile_strict(mut self) -> Self {
913 self.quality.iso_thresholds = IsoQualityConfig::strict();
914 self
915 }
916
917 pub fn iso_quality_profile_lenient(mut self) -> Self {
925 self.quality.iso_thresholds = IsoQualityConfig::lenient();
926 self
927 }
928
929 pub fn engine(mut self, engine: &str) -> Self {
937 self.engine.default_engine = engine.to_string();
938 self
939 }
940
941 pub fn chunk_size(mut self, size: usize) -> Self {
943 self.engine.default_chunk_size = Some(size);
944 self
945 }
946
947 pub fn parallel(mut self, enabled: bool) -> Self {
949 self.engine.parallel = enabled;
950 self
951 }
952
953 pub fn max_concurrent(mut self, max: usize) -> Self {
955 self.engine.max_concurrent = max;
956 self
957 }
958
959 pub fn max_memory_mb(mut self, mb: usize) -> Self {
961 self.engine.memory.max_usage_mb = mb;
962 self
963 }
964
965 pub fn auto_streaming_threshold_mb(mut self, mb: f64) -> Self {
967 self.engine.memory.auto_streaming_threshold_mb = mb;
968 self
969 }
970
971 #[cfg(feature = "database")]
976 pub fn db_connection_timeout(mut self, seconds: u64) -> Self {
978 if let Some(ref mut db) = self.database {
979 db.connection_timeout = seconds;
980 }
981 self
982 }
983
984 #[cfg(feature = "database")]
985 pub fn db_batch_size(mut self, size: usize) -> Self {
987 if let Some(ref mut db) = self.database {
988 db.batch_size = size;
989 }
990 self
991 }
992
993 #[cfg(feature = "database")]
994 pub fn db_sampling_enabled(mut self, enabled: bool) -> Self {
996 if let Some(ref mut db) = self.database {
997 db.sampling.enabled = enabled;
998 }
999 self
1000 }
1001
1002 pub fn ci_preset() -> Self {
1013 Self::new()
1014 .colored(false)
1015 .show_progress(false)
1016 .output_format("json")
1017 .verbosity(2)
1018 }
1019
1020 pub fn interactive_preset() -> Self {
1027 Self::new()
1028 .colored(true)
1029 .show_progress(true)
1030 .output_format("text")
1031 .verbosity(1)
1032 }
1033
1034 pub fn production_quality_preset() -> Self {
1041 Self::new()
1042 .iso_quality_profile_strict()
1043 .quality_enabled(true)
1044 .max_memory_mb(512) }
1046
1047 pub fn build(self) -> Result<DataprofConfig, DataProfilerError> {
1055 let config = DataprofConfig {
1056 output: self.output,
1057 quality: self.quality,
1058 engine: self.engine,
1059 #[cfg(feature = "database")]
1060 database: self.database,
1061 };
1062
1063 config.validate()?;
1065
1066 Ok(config)
1067 }
1068
1069 pub fn build_unchecked(self) -> DataprofConfig {
1074 DataprofConfig {
1075 output: self.output,
1076 quality: self.quality,
1077 engine: self.engine,
1078 #[cfg(feature = "database")]
1079 database: self.database,
1080 }
1081 }
1082}
1083
1084impl Default for DataprofConfigBuilder {
1085 fn default() -> Self {
1086 Self::new()
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093
1094 #[test]
1095 fn legacy_quality_config_defaults_score_weights() {
1096 let mut value = serde_json::to_value(IsoQualityConfig::default()).unwrap();
1097 value
1098 .as_object_mut()
1099 .expect("quality config object")
1100 .remove("score_weights");
1101
1102 let restored: IsoQualityConfig = serde_json::from_value(value).unwrap();
1103 assert_eq!(restored.score_weights, QualityScoreWeights::default());
1104 }
1105
1106 #[test]
1107 fn five_dimension_weights_gain_new_dimension_defaults() {
1108 let weights: QualityScoreWeights = serde_json::from_str(
1109 r#"{
1110 "completeness": 0.30,
1111 "consistency": 0.25,
1112 "uniqueness": 0.20,
1113 "accuracy": 0.15,
1114 "timeliness": 0.10
1115 }"#,
1116 )
1117 .unwrap();
1118
1119 assert_eq!(weights.validity, 0.10);
1120 assert_eq!(weights.precision, 0.05);
1121 }
1122
1123 #[test]
1124 fn config_validation_rejects_invalid_score_weights() {
1125 let weights = QualityScoreWeights {
1126 completeness: 0.0,
1127 consistency: 0.0,
1128 uniqueness: 0.0,
1129 accuracy: 0.0,
1130 timeliness: 0.0,
1131 validity: 0.0,
1132 precision: 0.0,
1133 };
1134 let config = DataprofConfigBuilder::new()
1135 .quality_score_weights(weights)
1136 .build();
1137
1138 assert!(config.is_err());
1139 }
1140}