mithril_client_cli/commands/
deprecation.rs

1use clap::{
2    builder::{StyledStr, Styles},
3    error::{ContextKind, ContextValue},
4};
5
6use crate::ClapError;
7
8/// Stores the deprecated command name and the new command name to use.
9#[derive(Clone)]
10pub struct DeprecatedCommand {
11    command: String,
12    new_command: String,
13    additional_message: Option<String>,
14    alias: Option<String>,
15}
16
17impl DeprecatedCommand {
18    /// Create information about a deprecated command
19    pub fn new<S1: Into<String>, S2: Into<String>>(command: S1, new_command: S2) -> Self {
20        Self {
21            command: command.into(),
22            new_command: new_command.into(),
23            additional_message: None,
24            alias: None,
25        }
26    }
27
28    /// Add an additional message to the deprecation warning. i.e. `with option '--option'`
29    pub fn with_additional_message<S: Into<String>>(mut self, additional_message: S) -> Self {
30        self.additional_message = Some(additional_message.into());
31        self
32    }
33
34    /// Matches the deprecated command with an alias.
35    pub fn with_alias<S: Into<String>>(mut self, alias: S) -> Self {
36        self.alias = Some(alias.into());
37        self
38    }
39}
40
41/// Tool to handle deprecated Clap commands.
42pub struct Deprecation;
43
44impl Deprecation {
45    fn find_deprecated_command(
46        error: &ClapError,
47        deprecated_commands: Vec<DeprecatedCommand>,
48    ) -> Option<DeprecatedCommand> {
49        if let Some(context_value) = error.get(ContextKind::InvalidSubcommand) {
50            let command_name = context_value.to_string();
51            deprecated_commands.into_iter().find(|dc| {
52                command_name == dc.command || dc.alias.as_ref().is_some_and(|a| &command_name == a)
53            })
54        } else {
55            None
56        }
57    }
58
59    /// Modify the result to add information on deprecated commands.
60    pub fn handle_deprecated_commands<A>(
61        matches_result: Result<A, ClapError>,
62        styles: Styles,
63        deprecated_commands: Vec<DeprecatedCommand>,
64    ) -> Result<A, ClapError> {
65        matches_result.map_err(|mut e: ClapError| {
66            if let Some(deprecated_command) = Self::find_deprecated_command(&e, deprecated_commands)
67            {
68                let additional_message = deprecated_command
69                    .additional_message
70                    .map(|m| format!(" {m}"))
71                    .unwrap_or_default();
72                let message = format!(
73                    "'{}{}{}' command is deprecated, use '{}{}{}' command instead{additional_message}",
74                    styles.get_error().render(),
75                    deprecated_command.command,
76                    styles.get_error().render_reset(),
77                    styles.get_valid().render(),
78                    deprecated_command.new_command,
79                    styles.get_valid().render_reset(),
80                );
81                e.insert(
82                    ContextKind::Suggested,
83                    ContextValue::StyledStrs(vec![StyledStr::from(&message)]),
84                );
85            }
86            e
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use clap::error::{ContextKind, ContextValue, ErrorKind};
95    use clap::{CommandFactory, Parser, Subcommand};
96
97    #[derive(Parser, Debug, Clone)]
98    pub struct MyCommand {
99        #[clap(subcommand)]
100        command: MySubCommands,
101    }
102
103    #[derive(Subcommand, Debug, Clone)]
104    enum MySubCommands {}
105
106    fn build_error(invalid_subcommand_name: &str) -> ClapError {
107        let mut e = ClapError::new(ErrorKind::InvalidSubcommand).with_cmd(&MyCommand::command());
108        e.insert(
109            ContextKind::InvalidSubcommand,
110            ContextValue::String(invalid_subcommand_name.to_string()),
111        );
112        e
113    }
114
115    #[test]
116    fn invalid_sub_command_message_for_a_non_deprecated_command_is_not_modified() {
117        let error = build_error("invalid_command");
118        let default_error_message = error.to_string();
119
120        let result = Deprecation::handle_deprecated_commands(
121            Err(error) as Result<MyCommand, ClapError>,
122            Styles::plain(),
123            vec![DeprecatedCommand::new("old_command", "new_command")],
124        );
125        assert!(result.is_err());
126        let message = result.err().unwrap().to_string();
127        assert_eq!(default_error_message, message);
128    }
129
130    #[test]
131    fn replace_error_message_on_deprecated_commands_and_show_the_new_command_without_additional_message(
132    ) {
133        let error = build_error("old_command");
134
135        let result = Deprecation::handle_deprecated_commands(
136            Err(error) as Result<MyCommand, ClapError>,
137            Styles::plain(),
138            vec![DeprecatedCommand::new("old_command", "new_command")],
139        );
140        assert!(result.is_err());
141        let message = result.err().unwrap().to_string();
142        assert!(
143            message
144                .contains("'old_command' command is deprecated, use 'new_command' command instead"),
145            "Unexpected 'tip:' error message:\n{message}"
146        );
147    }
148
149    #[test]
150    fn replace_error_message_on_deprecated_commands_and_show_the_new_command_when_using_alias() {
151        let error = build_error("old_alias");
152
153        let result = Deprecation::handle_deprecated_commands(
154            Err(error) as Result<MyCommand, ClapError>,
155            Styles::plain(),
156            vec![DeprecatedCommand::new("old_command", "new_command").with_alias("old_alias")],
157        );
158        assert!(result.is_err());
159        let message = result.err().unwrap().to_string();
160        assert!(
161            message
162                .contains("'old_command' command is deprecated, use 'new_command' command instead"),
163            "Unexpected 'tip:' error message:\n{message}"
164        );
165    }
166
167    #[test]
168    fn replace_error_message_on_deprecated_commands_and_show_the_new_command_with_additional_message(
169    ) {
170        let error = build_error("old_command");
171
172        let result = Deprecation::handle_deprecated_commands(
173            Err(error) as Result<MyCommand, ClapError>,
174            Styles::plain(),
175            vec![DeprecatedCommand::new("old_command", "new_command")
176                .with_additional_message("'additional message'")],
177        );
178        assert!(result.is_err());
179        let message = result.err().unwrap().to_string();
180        assert!(
181            message.contains("'old_command' command is deprecated, use 'new_command' command instead 'additional message'"),
182            "Unexpected 'tip:' in error message:\n{message}"
183        );
184    }
185}