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::io::Write;
7use std::path::Path;
8
9use crate::analyzer::LanguageAnalyzer;
10use crate::cli::analysis_output::AnalysisOutput;
11use crate::cli::baseline_command::BaselineCommand;
12use crate::cli::check_command::CheckCommand;
13use crate::cli::checking::check_thresholds::CheckThresholds;
14use crate::cli::cleanup_command::CleanupCommand;
15use crate::cli::cli_error::CliError;
16use crate::cli::cli_error::CliResult;
17use crate::cli::cli_overrides::CliOverrides;
18use crate::cli::command::Command;
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`, writing whatever it produces to `writer`.
56    ///
57    /// # Errors
58    ///
59    /// Returns whatever the command returns: a failed analysis, an I/O
60    /// failure, or [`crate::cli::cli_error::CliError::CheckFailed`] when
61    /// `check` finds more duplication than its ceilings allow.
62    pub fn dispatch(&self, command: &Command, writer: &mut impl Write) -> CliResult {
63        match command {
64            Command::Ignore {
65                fingerprint,
66                reason,
67            } => IgnoreCommand::new(self.root, fingerprint, reason.as_deref()).run(writer),
68            Command::Ignored => IgnoredCommand::new(self.root).run(writer),
69            _ => self.dispatch_analysed(command, writer),
70        }
71    }
72
73    fn dispatch_analysed(&self, command: &Command, writer: &mut impl Write) -> CliResult {
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, writer)
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(
95        &self,
96        command: &Command,
97        output: &AnalysisOutput,
98        writer: &mut impl Write,
99    ) -> CliResult {
100        let reporter: &dyn Reporter = &*output.reporter;
101        match command {
102            Command::Stats => StatsCommand::new(&output.result, reporter).run(writer),
103            Command::Report => ReportCommand::new(&output.result, reporter).run(writer),
104            Command::Check {
105                max_exact,
106                max_near,
107                max_exact_percent,
108                max_near_percent,
109            } => {
110                let thresholds = CheckThresholds::new(
111                    *max_exact,
112                    *max_near,
113                    *max_exact_percent,
114                    *max_near_percent,
115                )
116                .map_err(|e| CliError::InvalidConfig(e.to_string()))?;
117                CheckCommand::new(&output.config, &output.result, reporter, &thresholds).run(writer)
118            }
119            Command::Cleanup { dry_run } => {
120                CleanupCommand::new(self.root, &output.result, *dry_run).run(writer)
121            }
122            Command::Baseline { dry_run } => {
123                BaselineCommand::new(self.root, &output.config, &output.result, *dry_run)
124                    .run(writer)
125            }
126            Command::Ignore { .. } | Command::Ignored => unreachable!(),
127        }
128    }
129}