dragonfly_plugin/
command.rs

1//! Command helpers and traits used by the `#[derive(Command)]` macro.
2//!
3//! Plugin authors usually interact with:
4//! - `Ctx`, the per-command execution context (for replying to the sender).
5//! - `CommandRegistry`, which is implemented for you by `#[derive(Plugin)]`.
6//! - `CommandParseError`, surfaced as friendly messages to players.
7
8use crate::{server::Server, types};
9
10use tokio::sync::mpsc;
11
12/// Per-command execution context.
13///
14/// This context is constructed by the runtime when a command matches,
15/// and exposes the `Server` handle plus the UUID of the player that
16/// issued the command.
17pub 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    /// Sends a chat message back to the command sender.
31    ///
32    /// This is a convenience wrapper around `Server::send_chat`.
33    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
41/// Trait plugins use to expose commands to the host.
42pub trait CommandRegistry {
43    fn get_commands(&self) -> Vec<types::CommandSpec> {
44        Vec::new()
45    }
46
47    /// Dispatch to registered commands. Returns true if a command was handled.
48    #[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
87/// Parse a required argument at the given index.
88pub 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
100/// Parse an optional argument at the given index.
101/// Returns Ok(None) if the argument is missing.
102/// Returns Ok(Some(value)) if present and parseable.
103/// Returns Err if present but invalid.
104pub 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        // Missing index
156        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        // Present but empty string
161        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}