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    /// Last active tab in diff mode (e.g., "summary", "components")
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub last_tab: Option<String>,
170    /// Last active tab in view mode (e.g., "overview", "tree")
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub last_view_tab: Option<String>,
173}
174
175impl Default for TuiPreferences {
176    fn default() -> Self {
177        Self {
178            theme: "dark".to_string(),
179            last_tab: None,
180            last_view_tab: None,
181        }
182    }
183}
184
185impl TuiPreferences {
186    /// Get the path to the preferences file.
187    #[must_use]
188    pub fn config_path() -> Option<PathBuf> {
189        dirs::config_dir().map(|p| p.join("sbom-tools").join("preferences.json"))
190    }
191
192    /// Load preferences from disk, or return defaults if not found.
193    #[must_use]
194    pub fn load() -> Self {
195        Self::config_path()
196            .and_then(|p| std::fs::read_to_string(p).ok())
197            .and_then(|s| serde_json::from_str(&s).ok())
198            .unwrap_or_default()
199    }
200
201    /// Save preferences to disk.
202    pub fn save(&self) -> std::io::Result<()> {
203        if let Some(path) = Self::config_path() {
204            if let Some(parent) = path.parent() {
205                std::fs::create_dir_all(parent)?;
206            }
207            let json = serde_json::to_string_pretty(self)
208                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
209            std::fs::write(path, json)?;
210        }
211        Ok(())
212    }
213}
214
215// ============================================================================
216// TUI Configuration
217// ============================================================================
218
219/// TUI-specific configuration.
220#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
221#[serde(default)]
222pub struct TuiConfig {
223    /// Theme name: "dark", "light", or "high-contrast"
224    pub theme: String,
225    /// Show line numbers in code views
226    pub show_line_numbers: bool,
227    /// Enable mouse support
228    pub mouse_enabled: bool,
229    /// Initial matching threshold for TUI threshold tuning
230    #[schemars(range(min = 0.0, max = 1.0))]
231    pub initial_threshold: f64,
232}
233
234impl Default for TuiConfig {
235    fn default() -> Self {
236        Self {
237            theme: "dark".to_string(),
238            show_line_numbers: true,
239            mouse_enabled: true,
240            initial_threshold: 0.8,
241        }
242    }
243}
244
245// ============================================================================
246// Command-specific Configuration Types
247// ============================================================================
248
249/// Configuration for diff operations
250#[derive(Debug, Clone)]
251pub struct DiffConfig {
252    /// Paths to compare
253    pub paths: DiffPaths,
254    /// Output configuration
255    pub output: OutputConfig,
256    /// Matching configuration
257    pub matching: MatchingConfig,
258    /// Filtering options
259    pub filtering: FilterConfig,
260    /// Behavior flags
261    pub behavior: BehaviorConfig,
262    /// Graph-aware diffing configuration
263    pub graph_diff: GraphAwareDiffConfig,
264    /// Custom matching rules configuration
265    pub rules: MatchingRulesPathConfig,
266    /// Ecosystem-specific rules configuration
267    pub ecosystem_rules: EcosystemRulesConfig,
268    /// Enrichment configuration (always defined, runtime feature check)
269    pub enrichment: EnrichmentConfig,
270}
271
272/// Paths for diff operation
273#[derive(Debug, Clone)]
274pub struct DiffPaths {
275    /// Path to old/baseline SBOM
276    pub old: PathBuf,
277    /// Path to new SBOM
278    pub new: PathBuf,
279}
280
281/// Configuration for view operations
282#[derive(Debug, Clone)]
283pub struct ViewConfig {
284    /// Path to SBOM file
285    pub sbom_path: PathBuf,
286    /// Output configuration
287    pub output: OutputConfig,
288    /// Whether to validate against NTIA
289    pub validate_ntia: bool,
290    /// Filter by minimum vulnerability severity (critical, high, medium, low)
291    pub min_severity: Option<String>,
292    /// Only show components with vulnerabilities
293    pub vulnerable_only: bool,
294    /// Filter by ecosystem
295    pub ecosystem_filter: Option<String>,
296    /// Exit with code 2 if vulnerabilities are present
297    pub fail_on_vuln: bool,
298    /// Enrichment configuration
299    pub enrichment: EnrichmentConfig,
300}
301
302/// Configuration for multi-diff operations
303#[derive(Debug, Clone)]
304pub struct MultiDiffConfig {
305    /// Path to baseline SBOM
306    pub baseline: PathBuf,
307    /// Paths to target SBOMs
308    pub targets: Vec<PathBuf>,
309    /// Output configuration
310    pub output: OutputConfig,
311    /// Matching configuration
312    pub matching: MatchingConfig,
313    /// Filtering options
314    pub filtering: FilterConfig,
315    /// Behavior flags
316    pub behavior: BehaviorConfig,
317    /// Graph-aware diffing configuration
318    pub graph_diff: GraphAwareDiffConfig,
319    /// Custom matching rules configuration
320    pub rules: MatchingRulesPathConfig,
321    /// Ecosystem-specific rules configuration
322    pub ecosystem_rules: EcosystemRulesConfig,
323    /// Enrichment configuration
324    pub enrichment: EnrichmentConfig,
325}
326
327/// Configuration for timeline analysis
328#[derive(Debug, Clone)]
329pub struct TimelineConfig {
330    /// Paths to SBOMs in chronological order
331    pub sbom_paths: Vec<PathBuf>,
332    /// Output configuration
333    pub output: OutputConfig,
334    /// Matching configuration
335    pub matching: MatchingConfig,
336    /// Filtering options
337    pub filtering: FilterConfig,
338    /// Behavior flags
339    pub behavior: BehaviorConfig,
340    /// Graph-aware diffing configuration
341    pub graph_diff: GraphAwareDiffConfig,
342    /// Custom matching rules configuration
343    pub rules: MatchingRulesPathConfig,
344    /// Ecosystem-specific rules configuration
345    pub ecosystem_rules: EcosystemRulesConfig,
346    /// Enrichment configuration
347    pub enrichment: EnrichmentConfig,
348}
349
350/// Configuration for query operations (searching components across multiple SBOMs)
351#[derive(Debug, Clone)]
352pub struct QueryConfig {
353    /// Paths to SBOM files to search
354    pub sbom_paths: Vec<PathBuf>,
355    /// Output configuration
356    pub output: OutputConfig,
357    /// Enrichment configuration
358    pub enrichment: EnrichmentConfig,
359    /// Maximum number of results to return
360    pub limit: Option<usize>,
361    /// Group results by SBOM source
362    pub group_by_sbom: bool,
363}
364
365/// Configuration for matrix comparison
366#[derive(Debug, Clone)]
367pub struct MatrixConfig {
368    /// Paths to SBOMs
369    pub sbom_paths: Vec<PathBuf>,
370    /// Output configuration
371    pub output: OutputConfig,
372    /// Matching configuration
373    pub matching: MatchingConfig,
374    /// Similarity threshold for clustering (0.0-1.0)
375    pub cluster_threshold: f64,
376    /// Filtering options
377    pub filtering: FilterConfig,
378    /// Behavior flags
379    pub behavior: BehaviorConfig,
380    /// Graph-aware diffing configuration
381    pub graph_diff: GraphAwareDiffConfig,
382    /// Custom matching rules configuration
383    pub rules: MatchingRulesPathConfig,
384    /// Ecosystem-specific rules configuration
385    pub ecosystem_rules: EcosystemRulesConfig,
386    /// Enrichment configuration
387    pub enrichment: EnrichmentConfig,
388}
389
390/// Configuration for the `vex` subcommand.
391#[derive(Debug, Clone)]
392pub struct VexConfig {
393    /// Path to SBOM file
394    pub sbom_path: PathBuf,
395    /// Paths to external VEX documents
396    pub vex_paths: Vec<PathBuf>,
397    /// Output format
398    pub output_format: ReportFormat,
399    /// Output file path (None for stdout)
400    pub output_file: Option<PathBuf>,
401    /// Suppress non-essential output
402    pub quiet: bool,
403    /// Only show actionable vulnerabilities (exclude NotAffected/Fixed)
404    pub actionable_only: bool,
405    /// Filter by VEX state
406    pub filter_state: Option<String>,
407    /// Enrichment configuration (for OSV/EOL before VEX overlay)
408    pub enrichment: EnrichmentConfig,
409}
410
411// ============================================================================
412// Sub-configuration Types
413// ============================================================================
414
415/// Output-related configuration
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
417#[serde(default)]
418pub struct OutputConfig {
419    /// Output format
420    pub format: ReportFormat,
421    /// Output file path (None for stdout)
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub file: Option<PathBuf>,
424    /// Report types to include
425    pub report_types: ReportType,
426    /// Disable colored output
427    pub no_color: bool,
428    /// Streaming configuration for large SBOMs
429    pub streaming: StreamingConfig,
430    /// Optional export filename template for TUI exports.
431    ///
432    /// Placeholders: `{date}` (YYYY-MM-DD), `{time}` (HHMMSS),
433    /// `{format}` (json/md/html), `{command}` (diff/view).
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub export_template: Option<String>,
436}
437
438impl Default for OutputConfig {
439    fn default() -> Self {
440        Self {
441            format: ReportFormat::Auto,
442            file: None,
443            report_types: ReportType::All,
444            no_color: false,
445            streaming: StreamingConfig::default(),
446            export_template: None,
447        }
448    }
449}
450
451/// Streaming configuration for memory-efficient processing of large SBOMs.
452///
453/// When streaming is enabled, the tool uses streaming parsers and reporters
454/// to avoid loading entire SBOMs into memory. This is essential for SBOMs
455/// with thousands of components.
456#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
457#[serde(default)]
458pub struct StreamingConfig {
459    /// Enable streaming mode automatically for files larger than this threshold (in bytes).
460    /// Default: 10 MB (`10_485_760` bytes)
461    #[schemars(range(min = 0))]
462    pub threshold_bytes: u64,
463    /// Force streaming mode regardless of file size.
464    /// Useful for testing or when processing stdin.
465    pub force: bool,
466    /// Disable streaming mode entirely (always load full SBOMs into memory).
467    pub disabled: bool,
468    /// Enable streaming for stdin input (since size is unknown).
469    /// Default: true
470    pub stream_stdin: bool,
471}
472
473impl Default for StreamingConfig {
474    fn default() -> Self {
475        Self {
476            threshold_bytes: 10 * 1024 * 1024, // 10 MB
477            force: false,
478            disabled: false,
479            stream_stdin: true,
480        }
481    }
482}
483
484impl StreamingConfig {
485    /// Check if streaming should be used for a file of the given size.
486    #[must_use]
487    pub fn should_stream(&self, file_size: Option<u64>, is_stdin: bool) -> bool {
488        if self.disabled {
489            return false;
490        }
491        if self.force {
492            return true;
493        }
494        if is_stdin && self.stream_stdin {
495            return true;
496        }
497        file_size.map_or(self.stream_stdin, |size| size >= self.threshold_bytes)
498    }
499
500    /// Create a streaming config that always streams.
501    #[must_use]
502    pub fn always() -> Self {
503        Self {
504            force: true,
505            ..Default::default()
506        }
507    }
508
509    /// Create a streaming config that never streams.
510    #[must_use]
511    pub fn never() -> Self {
512        Self {
513            disabled: true,
514            ..Default::default()
515        }
516    }
517
518    /// Set the threshold in megabytes.
519    #[must_use]
520    pub const fn with_threshold_mb(mut self, mb: u64) -> Self {
521        self.threshold_bytes = mb * 1024 * 1024;
522        self
523    }
524}
525
526/// Matching and comparison configuration
527#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
528#[serde(default)]
529pub struct MatchingConfig {
530    /// Fuzzy matching preset name
531    pub fuzzy_preset: String,
532    /// Custom matching threshold (overrides preset)
533    #[serde(skip_serializing_if = "Option::is_none")]
534    #[schemars(range(min = 0.0, max = 1.0))]
535    pub threshold: Option<f64>,
536    /// Include unchanged components in output
537    pub include_unchanged: bool,
538}
539
540impl Default for MatchingConfig {
541    fn default() -> Self {
542        Self {
543            fuzzy_preset: "balanced".to_string(),
544            threshold: None,
545            include_unchanged: false,
546        }
547    }
548}
549
550impl MatchingConfig {
551    /// Convert preset name to `FuzzyMatchConfig`
552    #[must_use]
553    pub fn to_fuzzy_config(&self) -> FuzzyMatchConfig {
554        let mut config = FuzzyMatchConfig::from_preset(&self.fuzzy_preset).unwrap_or_else(|| {
555            tracing::warn!(
556                "Unknown fuzzy preset '{}', using 'balanced'. Valid: strict, balanced, permissive",
557                self.fuzzy_preset
558            );
559            FuzzyMatchConfig::balanced()
560        });
561
562        // Apply custom threshold if specified
563        if let Some(threshold) = self.threshold {
564            config = config.with_threshold(threshold);
565        }
566
567        config
568    }
569}
570
571/// Filtering options for diff results
572#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
573#[serde(default)]
574pub struct FilterConfig {
575    /// Only show items with changes
576    pub only_changes: bool,
577    /// Minimum severity filter
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub min_severity: Option<String>,
580    /// Exclude vulnerabilities with VEX status `not_affected` or fixed
581    #[serde(alias = "exclude_vex_not_affected")]
582    pub exclude_vex_resolved: bool,
583    /// Exit with error if introduced vulnerabilities lack VEX statements
584    pub fail_on_vex_gap: bool,
585}
586
587/// Behavior flags for diff operations
588#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
589#[serde(default)]
590pub struct BehaviorConfig {
591    /// Exit with code 2 if new vulnerabilities are introduced
592    pub fail_on_vuln: bool,
593    /// Exit with code 1 if any changes detected
594    pub fail_on_change: bool,
595    /// Suppress non-essential output
596    pub quiet: bool,
597    /// Show detailed match explanations for each matched component
598    pub explain_matches: bool,
599    /// Recommend optimal matching threshold based on the SBOMs
600    pub recommend_threshold: bool,
601}
602
603/// Graph-aware diffing configuration
604#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
605#[serde(default)]
606pub struct GraphAwareDiffConfig {
607    /// Enable graph-aware diffing
608    pub enabled: bool,
609    /// Detect component reparenting
610    pub detect_reparenting: bool,
611    /// Detect depth changes
612    pub detect_depth_changes: bool,
613    /// Maximum depth to analyze (0 = unlimited)
614    pub max_depth: u32,
615    /// Minimum impact level to include in output ("low", "medium", "high", "critical")
616    pub impact_threshold: Option<String>,
617    /// Relationship type filter — only include edges matching these types (empty = all)
618    pub relation_filter: Vec<String>,
619}
620
621impl GraphAwareDiffConfig {
622    /// Create enabled graph diff options with defaults
623    #[must_use]
624    pub const fn enabled() -> Self {
625        Self {
626            enabled: true,
627            detect_reparenting: true,
628            detect_depth_changes: true,
629            max_depth: 0,
630            impact_threshold: None,
631            relation_filter: Vec::new(),
632        }
633    }
634}
635
636/// Custom matching rules configuration
637#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
638#[serde(default)]
639pub struct MatchingRulesPathConfig {
640    /// Path to matching rules YAML file
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub rules_file: Option<PathBuf>,
643    /// Dry-run mode (show what would match without applying)
644    pub dry_run: bool,
645}
646
647/// Ecosystem-specific rules configuration
648#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
649#[serde(default)]
650pub struct EcosystemRulesConfig {
651    /// Path to ecosystem rules configuration file
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub config_file: Option<PathBuf>,
654    /// Disable ecosystem-specific normalization
655    pub disabled: bool,
656    /// Enable typosquat detection warnings
657    pub detect_typosquats: bool,
658}
659
660/// Enrichment configuration for vulnerability data sources.
661///
662/// This configuration is always defined regardless of the `enrichment` feature flag.
663/// When the feature is disabled, the configuration is silently ignored at runtime.
664#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
665#[serde(default)]
666pub struct EnrichmentConfig {
667    /// Enable enrichment (if false, no enrichment is performed)
668    pub enabled: bool,
669    /// Enrichment provider ("osv", "nvd", etc.)
670    pub provider: String,
671    /// Cache time-to-live in hours
672    #[schemars(range(min = 1))]
673    pub cache_ttl_hours: u64,
674    /// Maximum concurrent requests
675    #[schemars(range(min = 1))]
676    pub max_concurrent: usize,
677    /// Cache directory for vulnerability data
678    #[serde(skip_serializing_if = "Option::is_none")]
679    pub cache_dir: Option<std::path::PathBuf>,
680    /// Bypass cache and fetch fresh vulnerability data
681    pub bypass_cache: bool,
682    /// API timeout in seconds
683    #[schemars(range(min = 1))]
684    pub timeout_secs: u64,
685    /// Enable end-of-life detection via endoflife.date API
686    pub enable_eol: bool,
687    /// Paths to external VEX documents (OpenVEX format)
688    #[serde(default, skip_serializing_if = "Vec::is_empty")]
689    pub vex_paths: Vec<std::path::PathBuf>,
690}
691
692impl Default for EnrichmentConfig {
693    fn default() -> Self {
694        Self {
695            enabled: false,
696            provider: "osv".to_string(),
697            cache_ttl_hours: 24,
698            max_concurrent: 10,
699            cache_dir: None,
700            bypass_cache: false,
701            timeout_secs: 30,
702            enable_eol: false,
703            vex_paths: Vec::new(),
704        }
705    }
706}
707
708impl EnrichmentConfig {
709    /// Create an enabled enrichment config with OSV provider.
710    #[must_use]
711    pub fn osv() -> Self {
712        Self {
713            enabled: true,
714            provider: "osv".to_string(),
715            ..Default::default()
716        }
717    }
718
719    /// Create an enabled enrichment config with custom settings.
720    #[must_use]
721    pub fn with_cache_dir(mut self, dir: std::path::PathBuf) -> Self {
722        self.cache_dir = Some(dir);
723        self
724    }
725
726    /// Set the cache TTL in hours.
727    #[must_use]
728    pub const fn with_cache_ttl_hours(mut self, hours: u64) -> Self {
729        self.cache_ttl_hours = hours;
730        self
731    }
732
733    /// Enable cache bypass (refresh).
734    #[must_use]
735    pub const fn with_bypass_cache(mut self) -> Self {
736        self.bypass_cache = true;
737        self
738    }
739
740    /// Set the API timeout in seconds.
741    #[must_use]
742    pub const fn with_timeout_secs(mut self, secs: u64) -> Self {
743        self.timeout_secs = secs;
744        self
745    }
746
747    /// Set VEX document paths.
748    #[must_use]
749    pub fn with_vex_paths(mut self, paths: Vec<std::path::PathBuf>) -> Self {
750        self.vex_paths = paths;
751        self
752    }
753}
754
755// ============================================================================
756// Builder for DiffConfig
757// ============================================================================
758
759/// Builder for `DiffConfig`
760#[derive(Debug, Default)]
761pub struct DiffConfigBuilder {
762    old: Option<PathBuf>,
763    new: Option<PathBuf>,
764    output: OutputConfig,
765    matching: MatchingConfig,
766    filtering: FilterConfig,
767    behavior: BehaviorConfig,
768    graph_diff: GraphAwareDiffConfig,
769    rules: MatchingRulesPathConfig,
770    ecosystem_rules: EcosystemRulesConfig,
771    enrichment: EnrichmentConfig,
772}
773
774impl DiffConfigBuilder {
775    #[must_use]
776    pub fn new() -> Self {
777        Self::default()
778    }
779
780    #[must_use]
781    pub fn old_path(mut self, path: PathBuf) -> Self {
782        self.old = Some(path);
783        self
784    }
785
786    #[must_use]
787    pub fn new_path(mut self, path: PathBuf) -> Self {
788        self.new = Some(path);
789        self
790    }
791
792    #[must_use]
793    pub const fn output_format(mut self, format: ReportFormat) -> Self {
794        self.output.format = format;
795        self
796    }
797
798    #[must_use]
799    pub fn output_file(mut self, file: Option<PathBuf>) -> Self {
800        self.output.file = file;
801        self
802    }
803
804    #[must_use]
805    pub const fn report_types(mut self, types: ReportType) -> Self {
806        self.output.report_types = types;
807        self
808    }
809
810    #[must_use]
811    pub const fn no_color(mut self, no_color: bool) -> Self {
812        self.output.no_color = no_color;
813        self
814    }
815
816    #[must_use]
817    pub fn fuzzy_preset(mut self, preset: String) -> Self {
818        self.matching.fuzzy_preset = preset;
819        self
820    }
821
822    #[must_use]
823    pub const fn matching_threshold(mut self, threshold: Option<f64>) -> Self {
824        self.matching.threshold = threshold;
825        self
826    }
827
828    #[must_use]
829    pub const fn include_unchanged(mut self, include: bool) -> Self {
830        self.matching.include_unchanged = include;
831        self
832    }
833
834    #[must_use]
835    pub const fn only_changes(mut self, only: bool) -> Self {
836        self.filtering.only_changes = only;
837        self
838    }
839
840    #[must_use]
841    pub fn min_severity(mut self, severity: Option<String>) -> Self {
842        self.filtering.min_severity = severity;
843        self
844    }
845
846    #[must_use]
847    pub const fn fail_on_vuln(mut self, fail: bool) -> Self {
848        self.behavior.fail_on_vuln = fail;
849        self
850    }
851
852    #[must_use]
853    pub const fn fail_on_change(mut self, fail: bool) -> Self {
854        self.behavior.fail_on_change = fail;
855        self
856    }
857
858    #[must_use]
859    pub const fn quiet(mut self, quiet: bool) -> Self {
860        self.behavior.quiet = quiet;
861        self
862    }
863
864    #[must_use]
865    pub const fn explain_matches(mut self, explain: bool) -> Self {
866        self.behavior.explain_matches = explain;
867        self
868    }
869
870    #[must_use]
871    pub const fn recommend_threshold(mut self, recommend: bool) -> Self {
872        self.behavior.recommend_threshold = recommend;
873        self
874    }
875
876    #[must_use]
877    pub fn graph_diff(mut self, enabled: bool) -> Self {
878        self.graph_diff = if enabled {
879            GraphAwareDiffConfig::enabled()
880        } else {
881            GraphAwareDiffConfig::default()
882        };
883        self
884    }
885
886    #[must_use]
887    pub fn matching_rules_file(mut self, file: Option<PathBuf>) -> Self {
888        self.rules.rules_file = file;
889        self
890    }
891
892    #[must_use]
893    pub const fn dry_run_rules(mut self, dry_run: bool) -> Self {
894        self.rules.dry_run = dry_run;
895        self
896    }
897
898    #[must_use]
899    pub fn ecosystem_rules_file(mut self, file: Option<PathBuf>) -> Self {
900        self.ecosystem_rules.config_file = file;
901        self
902    }
903
904    #[must_use]
905    pub const fn disable_ecosystem_rules(mut self, disabled: bool) -> Self {
906        self.ecosystem_rules.disabled = disabled;
907        self
908    }
909
910    #[must_use]
911    pub const fn detect_typosquats(mut self, detect: bool) -> Self {
912        self.ecosystem_rules.detect_typosquats = detect;
913        self
914    }
915
916    #[must_use]
917    pub fn enrichment(mut self, config: EnrichmentConfig) -> Self {
918        self.enrichment = config;
919        self
920    }
921
922    #[must_use]
923    pub const fn enable_enrichment(mut self, enabled: bool) -> Self {
924        self.enrichment.enabled = enabled;
925        self
926    }
927
928    pub fn build(self) -> anyhow::Result<DiffConfig> {
929        let old = self
930            .old
931            .ok_or_else(|| anyhow::anyhow!("old path is required"))?;
932        let new = self
933            .new
934            .ok_or_else(|| anyhow::anyhow!("new path is required"))?;
935
936        Ok(DiffConfig {
937            paths: DiffPaths { old, new },
938            output: self.output,
939            matching: self.matching,
940            filtering: self.filtering,
941            behavior: self.behavior,
942            graph_diff: self.graph_diff,
943            rules: self.rules,
944            ecosystem_rules: self.ecosystem_rules,
945            enrichment: self.enrichment,
946        })
947    }
948}