1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use clap::{
    builder::{StyledStr, Styles},
    error::{ContextKind, ContextValue},
};

use crate::ClapError;

/// Stores the deprecated command name and the new command name to use.
#[derive(Clone)]
pub struct DeprecatedCommand {
    command: String,
    new_command: String,
}

impl DeprecatedCommand {
    /// Create information about a deprecated command
    pub fn new<S: ToString>(command: S, new_command: S) -> Self {
        Self {
            command: command.to_string(),
            new_command: new_command.to_string(),
        }
    }
}

/// Tool to handle deprecated Clap commands.
pub struct Deprecation;

impl Deprecation {
    fn find_deprecated_command(
        error: &ClapError,
        deprecated_commands: Vec<DeprecatedCommand>,
    ) -> Option<DeprecatedCommand> {
        if let Some(context_value) = error.get(ContextKind::InvalidSubcommand) {
            let command_name = context_value.to_string();
            deprecated_commands
                .into_iter()
                .find(|dc| command_name == dc.command)
        } else {
            None
        }
    }

    /// Modify result to add information on deprecated commands.
    pub fn handle_deprecated_commands<A>(
        matches_result: Result<A, ClapError>,
        styles: Styles,
        deprecated_commands: Vec<DeprecatedCommand>,
    ) -> Result<A, ClapError> {
        matches_result.map_err(|mut e: ClapError| {
            if let Some(deprecated_command) = Self::find_deprecated_command(&e, deprecated_commands)
            {
                let message = format!(
                    "'{}{}{}' command is deprecated, use '{}{}{}' command instead",
                    styles.get_error().render(),
                    deprecated_command.command,
                    styles.get_error().render_reset(),
                    styles.get_valid().render(),
                    deprecated_command.new_command,
                    styles.get_valid().render_reset(),
                );
                e.insert(
                    ContextKind::Suggested,
                    ContextValue::StyledStrs(vec![StyledStr::from(&message)]),
                );
            }
            e
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::error::{ContextKind, ContextValue, ErrorKind};
    use clap::{CommandFactory, Parser, Subcommand};

    #[derive(Parser, Debug, Clone)]
    pub struct MyCommand {
        #[clap(subcommand)]
        command: MySubCommands,
    }

    #[derive(Subcommand, Debug, Clone)]
    enum MySubCommands {}

    #[test]
    fn invalid_sub_command_message_for_a_non_deprecated_command_is_not_modified() {
        fn build_error() -> Result<MyCommand, ClapError> {
            let mut e =
                ClapError::new(ErrorKind::InvalidSubcommand).with_cmd(&MyCommand::command());

            e.insert(
                ContextKind::InvalidSubcommand,
                ContextValue::String("invalid_command".to_string()),
            );

            Err(e)
        }

        let default_error_message = build_error().err().unwrap().to_string();

        let result = Deprecation::handle_deprecated_commands(
            build_error(),
            Styles::plain(),
            vec![DeprecatedCommand::new("old_command", "new_command")],
        );
        assert!(result.is_err());
        let message = result.err().unwrap().to_string();
        assert_eq!(default_error_message, message);
    }

    #[test]
    fn replace_error_message_on_deprecated_commands_and_show_the_new_command() {
        let mut e = ClapError::new(ErrorKind::InvalidSubcommand).with_cmd(&MyCommand::command());
        e.insert(
            ContextKind::InvalidSubcommand,
            ContextValue::String("old_command".to_string()),
        );

        let result = Deprecation::handle_deprecated_commands(
            Err(e) as Result<MyCommand, ClapError>,
            Styles::plain(),
            vec![DeprecatedCommand::new("old_command", "new_command")],
        );
        assert!(result.is_err());
        let message = result.err().unwrap().to_string();
        assert!(message.contains("'old_command'"));
        assert!(message.contains("deprecated"));
        assert!(message.contains("'new_command'"));
    }
}