mithril_client_cli/commands/
deprecation.rs1use clap::{
2 builder::{StyledStr, Styles},
3 error::{ContextKind, ContextValue},
4};
5
6use crate::ClapError;
7
8#[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 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 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 pub fn with_alias<S: Into<String>>(mut self, alias: S) -> Self {
36 self.alias = Some(alias.into());
37 self
38 }
39}
40
41pub 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 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}