dataprof_core/
analysis_options.rs1use std::collections::HashSet;
12
13use crate::errors::DataProfilerError;
14use crate::locale::Locale;
15use crate::quality::{MetricPack, QualityDimension};
16use crate::semantic::SemanticHints;
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct AnalysisOptions {
24 columns: Option<Vec<String>>,
25 metric_packs: Option<Vec<MetricPack>>,
26 quality_dimensions: Option<Vec<QualityDimension>>,
27 locale: Option<Locale>,
28 semantic_hints: SemanticHints,
29}
30
31impl AnalysisOptions {
32 pub fn with_columns(mut self, columns: Option<Vec<String>>) -> Self {
37 self.columns = columns;
38 self
39 }
40
41 pub fn with_metric_packs(mut self, packs: Option<Vec<MetricPack>>) -> Self {
43 self.metric_packs = packs;
44 self
45 }
46
47 pub fn with_quality_dimensions(mut self, dimensions: Option<Vec<QualityDimension>>) -> Self {
49 self.quality_dimensions = dimensions;
50 self
51 }
52
53 pub fn with_locale(mut self, locale: Option<Locale>) -> Self {
59 self.locale = locale;
60 self
61 }
62
63 pub fn with_semantic_hints(mut self, hints: SemanticHints) -> Self {
65 self.semantic_hints = hints;
66 self
67 }
68
69 pub fn effective_metric_packs(&self) -> Option<Vec<MetricPack>> {
74 MetricPack::resolve_with_dimensions(
75 self.metric_packs.as_deref(),
76 self.quality_dimensions.as_deref(),
77 )
78 }
79
80 pub fn include_statistics(&self) -> bool {
82 MetricPack::include_statistics(self.effective_metric_packs().as_deref())
83 }
84
85 pub fn include_patterns(&self) -> bool {
87 MetricPack::include_patterns(self.effective_metric_packs().as_deref())
88 }
89
90 pub fn include_quality(&self) -> bool {
95 MetricPack::include_quality(self.effective_metric_packs().as_deref())
96 }
97
98 pub fn locale(&self) -> Option<Locale> {
104 self.locale
105 }
106
107 pub fn quality_dimensions(&self) -> Option<&[QualityDimension]> {
109 self.quality_dimensions.as_deref()
110 }
111
112 pub fn semantic_hints(&self) -> &SemanticHints {
114 &self.semantic_hints
115 }
116
117 pub fn columns(&self) -> Option<&[String]> {
119 self.columns.as_deref()
120 }
121
122 pub fn has_column_projection(&self) -> bool {
124 self.columns.is_some()
125 }
126
127 pub fn column_indices(
133 &self,
134 available: &[String],
135 ) -> Result<Option<Vec<usize>>, DataProfilerError> {
136 let Some(requested) = &self.columns else {
137 return Ok(None);
138 };
139
140 let mut seen = HashSet::with_capacity(requested.len());
141 let duplicates = requested
142 .iter()
143 .filter(|name| !seen.insert(name.as_str()))
144 .cloned()
145 .collect::<Vec<_>>();
146 if !duplicates.is_empty() {
147 return Err(DataProfilerError::invalid_config(
148 &format!(
149 "column projection contains duplicate name(s): {}",
150 duplicates.join(", ")
151 ),
152 "List each projected column once.",
153 ));
154 }
155
156 let available_set = available.iter().map(String::as_str).collect::<HashSet<_>>();
157 let missing = requested
158 .iter()
159 .filter(|name| !available_set.contains(name.as_str()))
160 .cloned()
161 .collect::<Vec<_>>();
162 if !missing.is_empty() {
163 return Err(DataProfilerError::invalid_config(
164 &format!(
165 "column projection names column(s) absent from the source: {}",
166 missing.join(", ")
167 ),
168 "Choose column names from the source schema.",
169 ));
170 }
171
172 Ok(Some(
173 available
174 .iter()
175 .enumerate()
176 .filter_map(|(index, name)| seen.contains(name.as_str()).then_some(index))
177 .collect(),
178 ))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn default_analyzes_everything() {
188 let options = AnalysisOptions::default();
189 assert!(options.include_statistics());
190 assert!(options.include_patterns());
191 assert!(options.include_quality());
192 assert_eq!(options.locale(), None);
193 assert_eq!(options.quality_dimensions(), None);
194 assert_eq!(options.columns(), None);
195 assert!(!options.has_column_projection());
196 }
197
198 #[test]
199 fn schema_only_deselects_every_other_pack() {
200 let options = AnalysisOptions::default().with_metric_packs(Some(vec![MetricPack::Schema]));
201 assert!(!options.include_statistics());
202 assert!(!options.include_patterns());
203 assert!(!options.include_quality());
204 }
205
206 #[test]
207 fn empty_dimension_selection_removes_the_quality_pack() {
208 let options = AnalysisOptions::default().with_quality_dimensions(Some(vec![]));
209 assert!(!options.include_quality());
210 assert!(options.include_statistics());
212 assert!(options.include_patterns());
213 }
214
215 #[test]
216 fn resolution_does_not_depend_on_setter_order() {
217 let packs = vec![MetricPack::Schema, MetricPack::Quality];
218 let dims_first = AnalysisOptions::default()
219 .with_quality_dimensions(Some(vec![]))
220 .with_metric_packs(Some(packs.clone()));
221 let packs_first = AnalysisOptions::default()
222 .with_metric_packs(Some(packs))
223 .with_quality_dimensions(Some(vec![]));
224 assert_eq!(
225 dims_first.effective_metric_packs(),
226 packs_first.effective_metric_packs()
227 );
228 assert!(!dims_first.include_quality());
229 }
230
231 #[test]
232 fn a_locale_alone_does_not_re_enable_pattern_detection() {
233 let options = AnalysisOptions::default()
234 .with_locale(Some(Locale::It))
235 .with_metric_packs(Some(vec![MetricPack::Schema]));
236 assert_eq!(options.locale(), Some(Locale::It));
237 assert!(!options.include_patterns());
238 }
239
240 #[test]
241 fn column_projection_resolves_in_source_order() {
242 let options = AnalysisOptions::default()
243 .with_columns(Some(vec!["city".to_string(), "id".to_string()]));
244 let available = vec!["id".to_string(), "name".to_string(), "city".to_string()];
245
246 assert_eq!(
247 options.column_indices(&available).unwrap(),
248 Some(vec![0, 2])
249 );
250 assert!(options.has_column_projection());
251 }
252
253 #[test]
254 fn column_projection_rejects_unknown_and_duplicate_names() {
255 let available = vec!["id".to_string(), "city".to_string()];
256 let unknown = AnalysisOptions::default().with_columns(Some(vec!["name".to_string()]));
257 assert!(unknown.column_indices(&available).is_err());
258
259 let duplicate =
260 AnalysisOptions::default().with_columns(Some(vec!["id".to_string(), "id".to_string()]));
261 assert!(duplicate.column_indices(&available).is_err());
262 }
263}