dragonfly_plugin/
command.rs1use crate::{server::Server, types};
9
10use tokio::sync::mpsc;
11
12pub struct Ctx<'a> {
18 pub server: &'a Server,
19 pub sender: String,
20}
21
22impl<'a> Ctx<'a> {
23 pub fn new(server: &'a Server, player_uuid: String) -> Self {
24 Self {
25 server,
26 sender: player_uuid,
27 }
28 }
29
30 pub async fn reply(
34 &self,
35 msg: impl Into<String>,
36 ) -> Result<(), mpsc::error::SendError<types::PluginToHost>> {
37 self.server.send_chat(self.sender.clone(), msg.into()).await
38 }
39}
40
41pub trait CommandRegistry {
43 fn get_commands(&self) -> Vec<types::CommandSpec> {
44 Vec::new()
45 }
46
47 #[allow(async_fn_in_trait)]
49 async fn dispatch_commands(
50 &self,
51 _server: &crate::Server,
52 _event: &mut crate::event::EventContext<'_, types::CommandEvent>,
53 ) -> bool {
54 false
55 }
56}
57
58#[derive(Debug)]
59pub enum CommandParseError {
60 NoMatch,
61 Missing(&'static str),
62 Invalid(&'static str),
63 UnknownSubcommand,
64}
65
66impl std::fmt::Display for CommandParseError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 CommandParseError::NoMatch => {
70 write!(f, "command did not match")
71 }
72 CommandParseError::Missing(name) => {
73 write!(f, "missing required argument `{name}`")
74 }
75 CommandParseError::Invalid(name) => {
76 write!(f, "invalid value for argument `{name}`")
77 }
78 CommandParseError::UnknownSubcommand => {
79 write!(f, "unknown subcommand")
80 }
81 }
82 }
83}
84
85impl std::error::Error for CommandParseError {}
86
87pub fn parse_required_arg<T>(
89 args: &[String],
90 index: usize,
91 name: &'static str,
92) -> Result<T, CommandParseError>
93where
94 T: std::str::FromStr,
95{
96 let s = args.get(index).ok_or(CommandParseError::Missing(name))?;
97 s.parse().map_err(|_| CommandParseError::Invalid(name))
98}
99
100pub fn parse_optional_arg<T>(
105 args: &[String],
106 index: usize,
107 name: &'static str,
108) -> Result<Option<T>, CommandParseError>
109where
110 T: std::str::FromStr,
111{
112 match args.get(index) {
113 None => Ok(None),
114 Some(s) if s.is_empty() => Ok(None),
115 Some(s) => s
116 .parse()
117 .map(Some)
118 .map_err(|_| CommandParseError::Invalid(name)),
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn parse_required_arg_ok() {
128 let args = vec!["42".to_string()];
129 let value: i32 = parse_required_arg(&args, 0, "amount").unwrap();
130 assert_eq!(value, 42);
131 }
132
133 #[test]
134 fn parse_required_arg_missing() {
135 let args: Vec<String> = Vec::new();
136 let err = parse_required_arg::<i32>(&args, 0, "amount").unwrap_err();
137 match err {
138 CommandParseError::Missing(name) => assert_eq!(name, "amount"),
139 e => panic!("expected Missing, got {e:?}"),
140 }
141 }
142
143 #[test]
144 fn parse_required_arg_invalid() {
145 let args = vec!["not-a-number".to_string()];
146 let err = parse_required_arg::<i32>(&args, 0, "amount").unwrap_err();
147 match err {
148 CommandParseError::Invalid(name) => assert_eq!(name, "amount"),
149 e => panic!("expected Invalid, got {e:?}"),
150 }
151 }
152
153 #[test]
154 fn parse_optional_arg_none_when_missing_or_empty() {
155 let args: Vec<String> = Vec::new();
157 let value: Option<i32> = parse_optional_arg(&args, 0, "amount").unwrap();
158 assert!(value.is_none());
159
160 let args = vec!["".to_string()];
162 let value: Option<i32> = parse_optional_arg(&args, 0, "amount").unwrap();
163 assert!(value.is_none());
164 }
165
166 #[test]
167 fn parse_optional_arg_some_when_valid() {
168 let args = vec!["7".to_string()];
169 let value: Option<i32> = parse_optional_arg(&args, 0, "amount").unwrap();
170 assert_eq!(value, Some(7));
171 }
172
173 #[test]
174 fn parse_optional_arg_error_when_invalid() {
175 let args = vec!["nope".to_string()];
176 let err = parse_optional_arg::<i32>(&args, 0, "amount").unwrap_err();
177 match err {
178 CommandParseError::Invalid(name) => assert_eq!(name, "amount"),
179 e => panic!("expected Invalid, got {e:?}"),
180 }
181 }
182
183 #[test]
184 fn display_messages_are_human_friendly() {
185 let err = CommandParseError::Missing("amount");
186 assert!(err.to_string().contains("missing required argument"));
187 assert!(err.to_string().contains("amount"));
188
189 let err = CommandParseError::Invalid("amount");
190 assert!(err.to_string().contains("invalid value for argument"));
191
192 let err = CommandParseError::UnknownSubcommand;
193 assert!(err.to_string().contains("unknown subcommand"));
194 }
195}