Skip to main content

dry4rust/
command_dispatcher.rs

1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6use std::path::Path;
7
8use crate::analyzer::LanguageAnalyzer;
9use crate::cli::analysis_output::AnalysisOutput;
10use crate::cli::baseline_command::BaselineCommand;
11use crate::cli::check_command::CheckCommand;
12use crate::cli::checking::check_thresholds::CheckThresholds;
13use crate::cli::cleanup_command::CleanupCommand;
14use crate::cli::cli_error::CliError;
15use crate::cli::cli_error::CliResult;
16use crate::cli::cli_overrides::CliOverrides;
17use crate::cli::command::Command;
18use crate::cli::command_outcome::CommandOutcome;
19use crate::cli::ignore_command::IgnoreCommand;
20use crate::cli::ignored_command::IgnoredCommand;
21use crate::cli::output_format::OutputFormat;
22use crate::cli::report_command::ReportCommand;
23use crate::cli::stats_command::StatsCommand;
24use crate::output::reporter::Reporter;
25
26/// Routes a parsed [`Command`] to the type that serves it.
27///
28/// Two commands read and write the ignore file without analysing anything;
29/// the other four need an analysis first. Splitting on that distinction is
30/// the whole of the routing decision, and keeping it here rather than in
31/// `main` puts it where a test can reach it.
32pub struct CommandDispatcher<'a> {
33    analyzer: &'a dyn LanguageAnalyzer,
34    root: &'a Path,
35    format: OutputFormat,
36    overrides: CliOverrides,
37}
38
39impl<'a> CommandDispatcher<'a> {
40    #[must_use]
41    pub const fn new(
42        analyzer: &'a dyn LanguageAnalyzer,
43        root: &'a Path,
44        format: OutputFormat,
45        overrides: CliOverrides,
46    ) -> Self {
47        Self {
48            analyzer,
49            root,
50            format,
51            overrides,
52        }
53    }
54
55    /// Run `command` and return the text it produced and its verdict.
56    ///
57    /// # Errors
58    ///
59    /// Returns whatever the command returns before it can produce text: a
60    /// failed analysis, an I/O failure, or an invalid fingerprint. A breached
61    /// `check` ceiling is carried in the outcome rather than returned here.
62    pub fn dispatch(&self, command: &Command) -> CliResult<CommandOutcome> {
63        match command {
64            Command::Ignore {
65                fingerprint,
66                reason,
67            } => IgnoreCommand::new(self.root, fingerprint, reason.as_deref()).run(),
68            Command::Ignored => IgnoredCommand::new(self.root).run(),
69            _ => self.dispatch_analysed(command),
70        }
71    }
72
73    fn dispatch_analysed(&self, command: &Command) -> CliResult<CommandOutcome> {
74        let output = if matches!(command, Command::Baseline { .. }) {
75            AnalysisOutput::produce_ignoring_baseline(
76                self.analyzer,
77                self.root,
78                self.format,
79                &self.overrides,
80            )?
81        } else {
82            AnalysisOutput::produce(self.analyzer, self.root, self.format, &self.overrides)?
83        };
84        Self::report_warnings(&output);
85        self.render(command, &output)
86    }
87
88    fn report_warnings(output: &AnalysisOutput) {
89        for warning in &output.result.warnings {
90            eprintln!("Warning: {warning}");
91        }
92    }
93
94    fn render(&self, command: &Command, output: &AnalysisOutput) -> CliResult<CommandOutcome> {
95        let reporter: &dyn Reporter = &*output.reporter;
96        match command {
97            Command::Stats => StatsCommand::new(&output.result, reporter).run(),
98            Command::Report => ReportCommand::new(&output.result, reporter).run(),
99            Command::Check {
100                max_exact,
101                max_near,
102                max_exact_percent,
103                max_near_percent,
104            } => {
105                let thresholds = CheckThresholds::new(
106                    *max_exact,
107                    *max_near,
108                    *max_exact_percent,
109                    *max_near_percent,
110                )
111                .map_err(|e| CliError::InvalidConfig(e.to_string()))?;
112                CheckCommand::new(&output.config, &output.result, reporter, &thresholds).run()
113            }
114            Command::Cleanup { dry_run } => {
115                CleanupCommand::new(self.root, &output.result, *dry_run).run()
116            }
117            Command::Baseline { dry_run } => {
118                BaselineCommand::new(self.root, &output.config, &output.result, *dry_run).run()
119            }
120            Command::Ignore { .. } | Command::Ignored => unreachable!(),
121        }
122    }
123}