Skip to main content

sbom_tools/config/
types.rs

1//! Configuration types for sbom-tools operations.
2//!
3//! Provides structured configuration for diff, view, and multi-comparison operations.
4
5use crate::matching::FuzzyMatchConfig;
6use crate::reports::{ReportFormat, ReportType};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11// ============================================================================
12// Unified Application Configuration
13// ============================================================================
14
15/// Unified application configuration that can be loaded from CLI args or config files.
16///
17/// This is the top-level configuration struct that aggregates all configuration
18/// options. It can be constructed from CLI arguments, config files, or both
19/// (with CLI overriding file settings).
20#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
21#[serde(default)]
22pub struct AppConfig {
23    /// Matching configuration (thresholds, presets)
24    pub matching: MatchingConfig,
25    /// Output configuration (format, file, colors)
26    pub output: OutputConfig,
27    /// Filtering options
28    pub filtering: FilterConfig,
29    /// Behavior flags
30    pub behavior: BehaviorConfig,
31    /// Graph-aware diffing configuration
32    pub graph_diff: GraphAwareDiffConfig,
33    /// Custom matching rules configuration
34    pub rules: MatchingRulesPathConfig,
35    /// Ecosystem-specific rules configuration
36    pub ecosystem_rules: EcosystemRulesConfig,
37    /// TUI-specific configuration
38    pub tui: TuiConfig,
39    /// Enrichment configuration (OSV, etc.)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub enrichment: Option<EnrichmentConfig>,
42}
43
44impl AppConfig {
45    /// Create a new `AppConfig` with default values.
46    #[must_use] 
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Create an `AppConfig` builder.
52    pub fn builder() -> AppConfigBuilder {
53        AppConfigBuilder::default()
54    }
55}
56
57// ============================================================================
58// Builder for AppConfig
59// ============================================================================
60
61/// Builder for constructing `AppConfig` with fluent API.
62#[derive(Debug, Default)]
63#[must_use]
64pub struct AppConfigBuilder {
65    config: AppConfig,
66}
67
68impl AppConfigBuilder {
69    /// Set the fuzzy matching preset.
70    pub fn fuzzy_preset(mut self, preset: impl Into<String>) -> Self {
71        self.config.matching.fuzzy_preset = preset.into();
72        self
73    }
74
75    /// Set the matching threshold.
76    pub const fn matching_threshold(mut self, threshold: f64) -> Self {
77        self.config.matching.threshold = Some(threshold);
78        self
79    }
80
81    /// Set the output format.
82    pub const fn output_format(mut self, format: ReportFormat) -> Self {
83        self.config.output.format = format;
84        self
85    }
86
87    /// Set the output file.
88    pub fn output_file(mut self, file: Option<PathBuf>) -> Self {
89        self.config.output.file = file;
90        self
91    }
92
93    /// Disable colored output.
94    pub const fn no_color(mut self, no_color: bool) -> Self {
95        self.config.output.no_color = no_color;
96        self
97    }
98
99    /// Include unchanged components.
100    pub const fn include_unchanged(mut self, include: bool) -> Self {
101        self.config.matching.include_unchanged = include;
102        self
103    }
104
105    /// Enable fail-on-vulnerability mode.
106    pub const fn fail_on_vuln(mut self, fail: bool) -> Self {
107        self.config.behavior.fail_on_vuln = fail;
108        self
109    }
110
111    /// Enable fail-on-change mode.
112    pub const fn fail_on_change(mut self, fail: bool) -> Self {
113        self.config.behavior.fail_on_change = fail;
114        self
115    }
116
117    /// Enable quiet mode.
118    pub const fn quiet(mut self, quiet: bool) -> Self {
119        self.config.behavior.quiet = quiet;
120        self
121    }
122
123    /// Enable graph-aware diffing.
124    pub fn graph_diff(mut self, enabled: bool) -> Self {
125        self.config.graph_diff = if enabled {
126            GraphAwareDiffConfig::enabled()
127        } else {
128            GraphAwareDiffConfig::default()
129        };
130        self
131    }
132
133    /// Set matching rules file.
134    pub fn matching_rules_file(mut self, file: Option<PathBuf>) -> Self {
135        self.config.rules.rules_file = file;
136        self
137    }
138
139    /// Set ecosystem rules file.
140    pub fn ecosystem_rules_file(mut self, file: Option<PathBuf>) -> Self {
141        self.config.ecosystem_rules.config_file = file;
142        self
143    }
144
145    /// Enable enrichment.
146    pub fn enrichment(mut self, config: EnrichmentConfig) -> Self {
147        self.config.enrichment = Some(config);
148        self
149    }
150
151    /// Build the `AppConfig`.
152    #[must_use] 
153    pub fn build(self) -> AppConfig {
154        self.config
155    }
156}
157
158// ============================================================================
159// TUI Preferences (persisted)
160// ============================================================================
161
162/// TUI preferences that persist across sessions.
163#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
164pub struct TuiPreferences {
165    /// Theme name: "dark", "light", or "high-contrast"
166    pub theme: String,
167}
168
169impl Default for TuiPreferences {
170    fn default() -> Self {
171        Self {
172            theme: "dark".to_string(),
173        }
174    }
175}
176
177impl TuiPreferences {
178    /// Get the path to the preferences file.
179    #[must_use] 
180    pub fn config_path() -> Option<PathBuf> {
181        dirs::config_dir().map(|p| p.join("sbom-tools").join("preferences.json"))
182    }
183
184    /// Load preferences from disk, or return defaults if not found.
185    #[must_use] 
186    pub fn load() -> Self {
187        Self::config_path()
188            .and_then(|p| std::fs::read_to_string(p).ok())
189            .and_then(|s| serde_json::from_str(&s).ok())
190            .unwrap_or_default()
191    }
192
193    /// Save preferences to disk.
194    pub fn save(&self) -> std::io::Result<()> {
195        if let Some(path) = Self::config_path() {
196            if let Some(parent) = path.parent() {
197                std::fs::create_dir_all(parent)?;
198            }
199            let json = serde_json::to_string_pretty(self)
200                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
201            std::fs::write(path, json)?;
202        }
203        Ok(())
204    }
205}
206
207// ============================================================================
208// TUI Configuration
209// ============================================================================
210
211/// TUI-specific configuration.
212#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
213#[serde(default)]
214pub struct TuiConfig {
215    /// Theme name: "dark", "light", or "high-contrast"
216    pub theme: String,
217    /// Show line numbers in code views
218    pub show_line_numbers: bool,
219    /// Enable mouse support
220    pub mouse_enabled: bool,
221    /// Initial matching threshold for TUI threshold tuning
222    #[schemars(range(min = 0.0, max = 1.0))]
223    pub initial_threshold: f64,
224}
225
226impl Default for TuiConfig {
227    fn default() -> Self {
228        Self {
229            theme: "dark".to_string(),
230            show_line_numbers: true,
231            mouse_enabled: true,
232            initial_threshold: 0.8,
233        }
234    }
235}
236
237// ============================================================================
238// Command-specific Configuration Types
239// ============================================================================
240
241/// Configuration for diff operations
242#[derive(Debug, Clone)]
243pub struct DiffConfig {
244    /// Paths to compare
245    pub paths: DiffPaths,
246    /// Output configuration
247    pub output: OutputConfig,
248    /// Matching configuration
249    pub matching: MatchingConfig,
250    /// Filtering options
251    pub filtering: FilterConfig,
252    /// Behavior flags
253    pub behavior: BehaviorConfig,
254    /// Graph-aware diffing configuration
255    pub graph_diff: GraphAwareDiffConfig,
256    /// Custom matching rules configuration
257    pub rules: MatchingRulesPathConfig,
258    /// Ecosystem-specific rules configuration
259    pub ecosystem_rules: EcosystemRulesConfig,
260    /// Enrichment configuration (always defined, runtime feature check)
261    pub enrichment: EnrichmentConfig,
262}
263
264/// Paths for diff operation
265#[derive(Debug, Clone)]
266pub struct DiffPaths {
267    /// Path to old/baseline SBOM
268    pub old: PathBuf,
269    /// Path to new SBOM
270    pub new: PathBuf,
271}
272
273/// Configuration for view operations
274#[derive(Debug, Clone)]
275pub struct ViewConfig {
276    /// Path to SBOM file
277    pub sbom_path: PathBuf,
278    /// Output configuration
279    pub output: OutputConfig,
280    /// Whether to validate against NTIA
281    pub validate_ntia: bool,
282    /// Filter by minimum vulnerability severity (critical, high, medium, low)
283    pub min_severity: Option<String>,
284    /// Only show components with vulnerabilities
285    pub vulnerable_only: bool,
286    /// Filter by ecosystem
287    pub ecosystem_filter: Option<String>,
288    /// Enrichment configuration
289    pub enrichment: EnrichmentConfig,
290}
291
292/// Configuration for multi-diff operations
293#[derive(Debug, Clone)]
294pub struct MultiDiffConfig {
295    /// Path to baseline SBOM
296    pub baseline: PathBuf,
297    /// Paths to target SBOMs
298    pub targets: Vec<PathBuf>,
299    /// Output configuration
300    pub output: OutputConfig,
301    /// Matching configuration
302    pub matching: MatchingConfig,
303}
304
305/// Configuration for timeline analysis
306#[derive(Debug, Clone)]
307pub struct TimelineConfig {
308    /// Paths to SBOMs in chronological order
309    pub sbom_paths: Vec<PathBuf>,
310    /// Output configuration
311    pub output: OutputConfig,
312    /// Matching configuration
313    pub matching: MatchingConfig,
314}
315
316/// Configuration for query operations (searching components across multiple SBOMs)
317#[derive(Debug, Clone)]
318pub struct QueryConfig {
319    /// Paths to SBOM files to search
320    pub sbom_paths: Vec<PathBuf>,
321    /// Output configuration
322    pub output: OutputConfig,
323    /// Enrichment configuration
324    pub enrichment: EnrichmentConfig,
325    /// Maximum number of results to return
326    pub limit: Option<usize>,
327    /// Group results by SBOM source
328    pub group_by_sbom: bool,
329}
330
331/// Configuration for matrix comparison
332#[derive(Debug, Clone)]
333pub struct MatrixConfig {
334    /// Paths to SBOMs
335    pub sbom_paths: Vec<PathBuf>,
336    /// Output configuration
337    pub output: OutputConfig,
338    /// Matching configuration
339    pub matching: MatchingConfig,
340    /// Similarity threshold for clustering (0.0-1.0)
341    pub cluster_threshold: f64,
342}
343
344// ============================================================================
345// Sub-configuration Types
346// ============================================================================
347
348/// Output-related configuration
349#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
350#[serde(default)]
351pub struct OutputConfig {
352    /// Output format
353    pub format: ReportFormat,
354    /// Output file path (None for stdout)
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub file: Option<PathBuf>,
357    /// Report types to include
358    pub report_types: ReportType,
359    /// Disable colored output
360    pub no_color: bool,
361    /// Streaming configuration for large SBOMs
362    pub streaming: StreamingConfig,
363}
364
365impl Default for OutputConfig {
366    fn default() -> Self {
367        Self {
368            format: ReportFormat::Auto,
369            file: None,
370            report_types: ReportType::All,
371            no_color: false,
372            streaming: StreamingConfig::default(),
373        }
374    }
375}
376
377/// Streaming configuration for memory-efficient processing of large SBOMs.
378///
379/// When streaming is enabled, the tool uses streaming parsers and reporters
380/// to avoid loading entire SBOMs into memory. This is essential for SBOMs
381/// with thousands of components.
382#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
383#[serde(default)]
384pub struct StreamingConfig {
385    /// Enable streaming mode automatically for files larger than this threshold (in bytes).
386    /// Default: 10 MB (`10_485_760` bytes)
387    #[schemars(range(min = 0))]
388    pub threshold_bytes: u64,
389    /// Force streaming mode regardless of file size.
390    /// Useful for testing or when processing stdin.
391    pub force: bool,
392    /// Disable streaming mode entirely (always load full SBOMs into memory).
393    pub disabled: bool,
394    /// Enable streaming for stdin input (since size is unknown).
395    /// Default: true
396    pub stream_stdin: bool,
397}
398
399impl Default for StreamingConfig {
400    fn default() -> Self {
401        Self {
402            threshold_bytes: 10 * 1024 * 1024, // 10 MB
403            force: false,
404            disabled: false,
405            stream_stdin: true,
406        }
407    }
408}
409
410impl StreamingConfig {
411    /// Check if streaming should be used for a file of the given size.
412    #[must_use] 
413    pub fn should_stream(&self, file_size: Option<u64>, is_stdin: bool) -> bool {
414        if self.disabled {
415            return false;
416        }
417        if self.force {
418            return true;
419        }
420        if is_stdin && self.stream_stdin {
421            return true;
422        }
423        file_size.map_or(self.stream_stdin, |size| size >= self.threshold_bytes)
424    }
425
426    /// Create a streaming config that always streams.
427    #[must_use] 
428    pub fn always() -> Self {
429        Self {
430            force: true,
431            ..Default::default()
432        }
433    }
434
435    /// Create a streaming config that never streams.
436    #[must_use] 
437    pub fn never() -> Self {
438        Self {
439            disabled: true,
440            ..Default::default()
441        }
442    }
443
444    /// Set the threshold in megabytes.
445    #[must_use]
446    pub const fn with_threshold_mb(mut self, mb: u64) -> Self {
447        self.threshold_bytes = mb * 1024 * 1024;
448        self
449    }
450}
451
452/// Matching and comparison configuration
453#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
454#[serde(default)]
455pub struct MatchingConfig {
456    /// Fuzzy matching preset name
457    pub fuzzy_preset: String,
458    /// Custom matching threshold (overrides preset)
459    #[serde(skip_serializing_if = "Option::is_none")]
460    #[schemars(range(min = 0.0, max = 1.0))]
461    pub threshold: Option<f64>,
462    /// Include unchanged components in output
463    pub include_unchanged: bool,
464}
465
466impl Default for MatchingConfig {
467    fn default() -> Self {
468        Self {
469            fuzzy_preset: "balanced".to_string(),
470            threshold: None,
471            include_unchanged: false,
472        }
473    }
474}
475
476impl MatchingConfig {
477    /// Convert preset name to `FuzzyMatchConfig`
478    #[must_use] 
479    pub fn to_fuzzy_config(&self) -> FuzzyMatchConfig {
480        let mut config = FuzzyMatchConfig::from_preset(&self.fuzzy_preset).unwrap_or_else(|| {
481            tracing::warn!(
482                "Unknown fuzzy preset '{}', using 'balanced'. Valid: strict, balanced, permissive",
483                self.fuzzy_preset
484            );
485            FuzzyMatchConfig::balanced()
486        });
487
488        // Apply custom threshold if specified
489        if let Some(threshold) = self.threshold {
490            config = config.with_threshold(threshold);
491        }
492
493        config
494    }
495}
496
497/// Filtering options for diff results
498#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
499#[serde(default)]
500pub struct FilterConfig {
501    /// Only show items with changes
502    pub only_changes: bool,
503    /// Minimum severity filter
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub min_severity: Option<String>,
506    /// Exclude vulnerabilities with VEX status `not_affected` or fixed
507    #[serde(alias = "exclude_vex_not_affected")]
508    pub exclude_vex_resolved: bool,
509}
510
511/// Behavior flags for diff operations
512#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
513#[serde(default)]
514pub struct BehaviorConfig {
515    /// Exit with code 2 if new vulnerabilities are introduced
516    pub fail_on_vuln: bool,
517    /// Exit with code 1 if any changes detected
518    pub fail_on_change: bool,
519    /// Suppress non-essential output
520    pub quiet: bool,
521    /// Show detailed match explanations for each matched component
522    pub explain_matches: bool,
523    /// Recommend optimal matching threshold based on the SBOMs
524    pub recommend_threshold: bool,
525}
526
527/// Graph-aware diffing configuration
528#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
529#[serde(default)]
530pub struct GraphAwareDiffConfig {
531    /// Enable graph-aware diffing
532    pub enabled: bool,
533    /// Detect component reparenting
534    pub detect_reparenting: bool,
535    /// Detect depth changes
536    pub detect_depth_changes: bool,
537}
538
539impl GraphAwareDiffConfig {
540    /// Create enabled graph diff options with defaults
541    #[must_use] 
542    pub const fn enabled() -> Self {
543        Self {
544            enabled: true,
545            detect_reparenting: true,
546            detect_depth_changes: true,
547        }
548    }
549}
550
551/// Custom matching rules configuration
552#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
553#[serde(default)]
554pub struct MatchingRulesPathConfig {
555    /// Path to matching rules YAML file
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub rules_file: Option<PathBuf>,
558    /// Dry-run mode (show what would match without applying)
559    pub dry_run: bool,
560}
561
562/// Ecosystem-specific rules configuration
563#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
564#[serde(default)]
565pub struct EcosystemRulesConfig {
566    /// Path to ecosystem rules configuration file
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub config_file: Option<PathBuf>,
569    /// Disable ecosystem-specific normalization
570    pub disabled: bool,
571    /// Enable typosquat detection warnings
572    pub detect_typosquats: bool,
573}
574
575/// Enrichment configuration for vulnerability data sources.
576///
577/// This configuration is always defined regardless of the `enrichment` feature flag.
578/// When the feature is disabled, the configuration is silently ignored at runtime.
579#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
580#[serde(default)]
581pub struct EnrichmentConfig {
582    /// Enable enrichment (if false, no enrichment is performed)
583    pub enabled: bool,
584    /// Enrichment provider ("osv", "nvd", etc.)
585    pub provider: String,
586    /// Cache time-to-live in hours
587    #[schemars(range(min = 1))]
588    pub cache_ttl_hours: u64,
589    /// Maximum concurrent requests
590    #[schemars(range(min = 1))]
591    pub max_concurrent: usize,
592    /// Cache directory for vulnerability data
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub cache_dir: Option<std::path::PathBuf>,
595    /// Bypass cache and fetch fresh vulnerability data
596    pub bypass_cache: bool,
597    /// API timeout in seconds
598    #[schemars(range(min = 1))]
599    pub timeout_secs: u64,
600    /// Enable end-of-life detection via endoflife.date API
601    pub enable_eol: bool,
602    /// Paths to external VEX documents (OpenVEX format)
603    #[serde(default, skip_serializing_if = "Vec::is_empty")]
604    pub vex_paths: Vec<std::path::PathBuf>,
605}
606
607impl Default for EnrichmentConfig {
608    fn default() -> Self {
609        Self {
610            enabled: false,
611            provider: "osv".to_string(),
612            cache_ttl_hours: 24,
613            max_concurrent: 10,
614            cache_dir: None,
615            bypass_cache: false,
616            timeout_secs: 30,
617            enable_eol: false,
618            vex_paths: Vec::new(),
619        }
620    }
621}
622
623impl EnrichmentConfig {
624    /// Create an enabled enrichment config with OSV provider.
625    #[must_use] 
626    pub fn osv() -> Self {
627        Self {
628            enabled: true,
629            provider: "osv".to_string(),
630            ..Default::default()
631        }
632    }
633
634    /// Create an enabled enrichment config with custom settings.
635    #[must_use]
636    pub fn with_cache_dir(mut self, dir: std::path::PathBuf) -> Self {
637        self.cache_dir = Some(dir);
638        self
639    }
640
641    /// Set the cache TTL in hours.
642    #[must_use]
643    pub const fn with_cache_ttl_hours(mut self, hours: u64) -> Self {
644        self.cache_ttl_hours = hours;
645        self
646    }
647
648    /// Enable cache bypass (refresh).
649    #[must_use]
650    pub const fn with_bypass_cache(mut self) -> Self {
651        self.bypass_cache = true;
652        self
653    }
654
655    /// Set the API timeout in seconds.
656    #[must_use]
657    pub const fn with_timeout_secs(mut self, secs: u64) -> Self {
658        self.timeout_secs = secs;
659        self
660    }
661
662    /// Set VEX document paths.
663    #[must_use]
664    pub fn with_vex_paths(mut self, paths: Vec<std::path::PathBuf>) -> Self {
665        self.vex_paths = paths;
666        self
667    }
668}
669
670// ============================================================================
671// Builder for DiffConfig
672// ============================================================================
673
674/// Builder for `DiffConfig`
675#[derive(Debug, Default)]
676pub struct DiffConfigBuilder {
677    old: Option<PathBuf>,
678    new: Option<PathBuf>,
679    output: OutputConfig,
680    matching: MatchingConfig,
681    filtering: FilterConfig,
682    behavior: BehaviorConfig,
683    graph_diff: GraphAwareDiffConfig,
684    rules: MatchingRulesPathConfig,
685    ecosystem_rules: EcosystemRulesConfig,
686    enrichment: EnrichmentConfig,
687}
688
689impl DiffConfigBuilder {
690    #[must_use] 
691    pub fn new() -> Self {
692        Self::default()
693    }
694
695    #[must_use]
696    pub fn old_path(mut self, path: PathBuf) -> Self {
697        self.old = Some(path);
698        self
699    }
700
701    #[must_use]
702    pub fn new_path(mut self, path: PathBuf) -> Self {
703        self.new = Some(path);
704        self
705    }
706
707    #[must_use]
708    pub const fn output_format(mut self, format: ReportFormat) -> Self {
709        self.output.format = format;
710        self
711    }
712
713    #[must_use]
714    pub fn output_file(mut self, file: Option<PathBuf>) -> Self {
715        self.output.file = file;
716        self
717    }
718
719    #[must_use]
720    pub const fn report_types(mut self, types: ReportType) -> Self {
721        self.output.report_types = types;
722        self
723    }
724
725    #[must_use]
726    pub const fn no_color(mut self, no_color: bool) -> Self {
727        self.output.no_color = no_color;
728        self
729    }
730
731    #[must_use]
732    pub fn fuzzy_preset(mut self, preset: String) -> Self {
733        self.matching.fuzzy_preset = preset;
734        self
735    }
736
737    #[must_use]
738    pub const fn matching_threshold(mut self, threshold: Option<f64>) -> Self {
739        self.matching.threshold = threshold;
740        self
741    }
742
743    #[must_use]
744    pub const fn include_unchanged(mut self, include: bool) -> Self {
745        self.matching.include_unchanged = include;
746        self
747    }
748
749    #[must_use]
750    pub const fn only_changes(mut self, only: bool) -> Self {
751        self.filtering.only_changes = only;
752        self
753    }
754
755    #[must_use]
756    pub fn min_severity(mut self, severity: Option<String>) -> Self {
757        self.filtering.min_severity = severity;
758        self
759    }
760
761    #[must_use]
762    pub const fn fail_on_vuln(mut self, fail: bool) -> Self {
763        self.behavior.fail_on_vuln = fail;
764        self
765    }
766
767    #[must_use]
768    pub const fn fail_on_change(mut self, fail: bool) -> Self {
769        self.behavior.fail_on_change = fail;
770        self
771    }
772
773    #[must_use]
774    pub const fn quiet(mut self, quiet: bool) -> Self {
775        self.behavior.quiet = quiet;
776        self
777    }
778
779    #[must_use]
780    pub const fn explain_matches(mut self, explain: bool) -> Self {
781        self.behavior.explain_matches = explain;
782        self
783    }
784
785    #[must_use]
786    pub const fn recommend_threshold(mut self, recommend: bool) -> Self {
787        self.behavior.recommend_threshold = recommend;
788        self
789    }
790
791    #[must_use]
792    pub fn graph_diff(mut self, enabled: bool) -> Self {
793        self.graph_diff = if enabled {
794            GraphAwareDiffConfig::enabled()
795        } else {
796            GraphAwareDiffConfig::default()
797        };
798        self
799    }
800
801    #[must_use]
802    pub fn matching_rules_file(mut self, file: Option<PathBuf>) -> Self {
803        self.rules.rules_file = file;
804        self
805    }
806
807    #[must_use]
808    pub const fn dry_run_rules(mut self, dry_run: bool) -> Self {
809        self.rules.dry_run = dry_run;
810        self
811    }
812
813    #[must_use]
814    pub fn ecosystem_rules_file(mut self, file: Option<PathBuf>) -> Self {
815        self.ecosystem_rules.config_file = file;
816        self
817    }
818
819    #[must_use]
820    pub const fn disable_ecosystem_rules(mut self, disabled: bool) -> Self {
821        self.ecosystem_rules.disabled = disabled;
822        self
823    }
824
825    #[must_use]
826    pub const fn detect_typosquats(mut self, detect: bool) -> Self {
827        self.ecosystem_rules.detect_typosquats = detect;
828        self
829    }
830
831    #[must_use]
832    pub fn enrichment(mut self, config: EnrichmentConfig) -> Self {
833        self.enrichment = config;
834        self
835    }
836
837    #[must_use]
838    pub const fn enable_enrichment(mut self, enabled: bool) -> Self {
839        self.enrichment.enabled = enabled;
840        self
841    }
842
843    pub fn build(self) -> anyhow::Result<DiffConfig> {
844        let old = self.old.ok_or_else(|| anyhow::anyhow!("old path is required"))?;
845        let new = self.new.ok_or_else(|| anyhow::anyhow!("new path is required"))?;
846
847        Ok(DiffConfig {
848            paths: DiffPaths { old, new },
849            output: self.output,
850            matching: self.matching,
851            filtering: self.filtering,
852            behavior: self.behavior,
853            graph_diff: self.graph_diff,
854            rules: self.rules,
855            ecosystem_rules: self.ecosystem_rules,
856            enrichment: self.enrichment,
857        })
858    }
859}