use std::collections::HashSet;
use crate::errors::DataProfilerError;
use crate::locale::Locale;
use crate::quality::{MetricPack, QualityDimension};
use crate::semantic::SemanticHints;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AnalysisOptions {
columns: Option<Vec<String>>,
metric_packs: Option<Vec<MetricPack>>,
quality_dimensions: Option<Vec<QualityDimension>>,
locale: Option<Locale>,
semantic_hints: SemanticHints,
}
impl AnalysisOptions {
pub fn with_columns(mut self, columns: Option<Vec<String>>) -> Self {
self.columns = columns;
self
}
pub fn with_metric_packs(mut self, packs: Option<Vec<MetricPack>>) -> Self {
self.metric_packs = packs;
self
}
pub fn with_quality_dimensions(mut self, dimensions: Option<Vec<QualityDimension>>) -> Self {
self.quality_dimensions = dimensions;
self
}
pub fn with_locale(mut self, locale: Option<Locale>) -> Self {
self.locale = locale;
self
}
pub fn with_semantic_hints(mut self, hints: SemanticHints) -> Self {
self.semantic_hints = hints;
self
}
pub fn effective_metric_packs(&self) -> Option<Vec<MetricPack>> {
MetricPack::resolve_with_dimensions(
self.metric_packs.as_deref(),
self.quality_dimensions.as_deref(),
)
}
pub fn include_statistics(&self) -> bool {
MetricPack::include_statistics(self.effective_metric_packs().as_deref())
}
pub fn include_patterns(&self) -> bool {
MetricPack::include_patterns(self.effective_metric_packs().as_deref())
}
pub fn include_quality(&self) -> bool {
MetricPack::include_quality(self.effective_metric_packs().as_deref())
}
pub fn locale(&self) -> Option<Locale> {
self.locale
}
pub fn quality_dimensions(&self) -> Option<&[QualityDimension]> {
self.quality_dimensions.as_deref()
}
pub fn semantic_hints(&self) -> &SemanticHints {
&self.semantic_hints
}
pub fn columns(&self) -> Option<&[String]> {
self.columns.as_deref()
}
pub fn has_column_projection(&self) -> bool {
self.columns.is_some()
}
pub fn column_indices(
&self,
available: &[String],
) -> Result<Option<Vec<usize>>, DataProfilerError> {
let Some(requested) = &self.columns else {
return Ok(None);
};
let mut seen = HashSet::with_capacity(requested.len());
let duplicates = requested
.iter()
.filter(|name| !seen.insert(name.as_str()))
.cloned()
.collect::<Vec<_>>();
if !duplicates.is_empty() {
return Err(DataProfilerError::invalid_config(
&format!(
"column projection contains duplicate name(s): {}",
duplicates.join(", ")
),
"List each projected column once.",
));
}
let available_set = available.iter().map(String::as_str).collect::<HashSet<_>>();
let missing = requested
.iter()
.filter(|name| !available_set.contains(name.as_str()))
.cloned()
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(DataProfilerError::invalid_config(
&format!(
"column projection names column(s) absent from the source: {}",
missing.join(", ")
),
"Choose column names from the source schema.",
));
}
Ok(Some(
available
.iter()
.enumerate()
.filter_map(|(index, name)| seen.contains(name.as_str()).then_some(index))
.collect(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_analyzes_everything() {
let options = AnalysisOptions::default();
assert!(options.include_statistics());
assert!(options.include_patterns());
assert!(options.include_quality());
assert_eq!(options.locale(), None);
assert_eq!(options.quality_dimensions(), None);
assert_eq!(options.columns(), None);
assert!(!options.has_column_projection());
}
#[test]
fn schema_only_deselects_every_other_pack() {
let options = AnalysisOptions::default().with_metric_packs(Some(vec![MetricPack::Schema]));
assert!(!options.include_statistics());
assert!(!options.include_patterns());
assert!(!options.include_quality());
}
#[test]
fn empty_dimension_selection_removes_the_quality_pack() {
let options = AnalysisOptions::default().with_quality_dimensions(Some(vec![]));
assert!(!options.include_quality());
assert!(options.include_statistics());
assert!(options.include_patterns());
}
#[test]
fn resolution_does_not_depend_on_setter_order() {
let packs = vec![MetricPack::Schema, MetricPack::Quality];
let dims_first = AnalysisOptions::default()
.with_quality_dimensions(Some(vec![]))
.with_metric_packs(Some(packs.clone()));
let packs_first = AnalysisOptions::default()
.with_metric_packs(Some(packs))
.with_quality_dimensions(Some(vec![]));
assert_eq!(
dims_first.effective_metric_packs(),
packs_first.effective_metric_packs()
);
assert!(!dims_first.include_quality());
}
#[test]
fn a_locale_alone_does_not_re_enable_pattern_detection() {
let options = AnalysisOptions::default()
.with_locale(Some(Locale::It))
.with_metric_packs(Some(vec![MetricPack::Schema]));
assert_eq!(options.locale(), Some(Locale::It));
assert!(!options.include_patterns());
}
#[test]
fn column_projection_resolves_in_source_order() {
let options = AnalysisOptions::default()
.with_columns(Some(vec!["city".to_string(), "id".to_string()]));
let available = vec!["id".to_string(), "name".to_string(), "city".to_string()];
assert_eq!(
options.column_indices(&available).unwrap(),
Some(vec![0, 2])
);
assert!(options.has_column_projection());
}
#[test]
fn column_projection_rejects_unknown_and_duplicate_names() {
let available = vec!["id".to_string(), "city".to_string()];
let unknown = AnalysisOptions::default().with_columns(Some(vec!["name".to_string()]));
assert!(unknown.column_indices(&available).is_err());
let duplicate =
AnalysisOptions::default().with_columns(Some(vec!["id".to_string(), "id".to_string()]));
assert!(duplicate.column_indices(&available).is_err());
}
}