Skip to main content

steel_core/command/brigadier/
error.rs

1//! Command parsing errors.
2
3use std::{error::Error, fmt};
4
5use steel_utils::translations;
6use text_components::{Modifier, TextComponent, format::Color, interactivity::ClickEvent};
7
8const CONTEXT_AMOUNT: usize = 10;
9
10/// Identifies a Brigadier parsing or command execution error.
11#[derive(Clone, Debug, PartialEq)]
12pub(crate) enum CommandSyntaxErrorKind {
13    /// No executable command matched the input.
14    UnknownCommand,
15    /// A command matched, but trailing input did not.
16    UnknownArgument,
17    /// A command supplied a rich runtime failure message.
18    Dynamic(Box<TextComponent>),
19    /// A quoted string did not start with a quote.
20    ExpectedStartOfQuote,
21    /// A quoted string reached the end of its input.
22    ExpectedEndOfQuote,
23    /// A quoted string contained an unsupported escape.
24    InvalidEscape(char),
25    /// A boolean did not contain `true` or `false`.
26    InvalidBool(Box<str>),
27    /// An integer could not be parsed.
28    InvalidInt(Box<str>),
29    /// No integer was present.
30    ExpectedInt,
31    /// A long could not be parsed.
32    InvalidLong(Box<str>),
33    /// No long was present.
34    ExpectedLong,
35    /// A double could not be parsed.
36    InvalidDouble(Box<str>),
37    /// No double was present.
38    ExpectedDouble,
39    /// A float could not be parsed.
40    InvalidFloat(Box<str>),
41    /// No float was present.
42    ExpectedFloat,
43    /// No boolean was present.
44    ExpectedBool,
45    /// An expected symbol was not present.
46    ExpectedSymbol(char),
47    /// A literal node did not match its configured text.
48    LiteralIncorrect(Box<str>),
49    /// An integer was below its configured minimum.
50    IntegerTooLow { found: i32, minimum: i32 },
51    /// An integer was above its configured maximum.
52    IntegerTooHigh { found: i32, maximum: i32 },
53    /// A long was below its configured minimum.
54    LongTooLow { found: i64, minimum: i64 },
55    /// A long was above its configured maximum.
56    LongTooHigh { found: i64, maximum: i64 },
57    /// A float was below its configured minimum.
58    FloatTooLow { found: f32, minimum: f32 },
59    /// A float was above its configured maximum.
60    FloatTooHigh { found: f32, maximum: f32 },
61    /// A double was below its configured minimum.
62    DoubleTooLow { found: f64, minimum: f64 },
63    /// A double was above its configured maximum.
64    DoubleTooHigh { found: f64, maximum: f64 },
65    /// A parsed argument had trailing non-whitespace data.
66    ExpectedArgumentSeparator,
67}
68
69impl fmt::Display for CommandSyntaxErrorKind {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::UnknownCommand => formatter.write_str("Unknown command"),
73            Self::UnknownArgument => formatter.write_str("Incorrect argument for command"),
74            Self::Dynamic(message) => write!(formatter, "{message}"),
75            Self::ExpectedStartOfQuote => formatter.write_str("Expected quote to start a string"),
76            Self::ExpectedEndOfQuote => formatter.write_str("Unclosed quoted string"),
77            Self::InvalidEscape(character) => write!(
78                formatter,
79                "Invalid escape sequence '{character}' in quoted string"
80            ),
81            Self::InvalidBool(value) => write!(
82                formatter,
83                "Invalid bool, expected true or false but found '{value}'"
84            ),
85            Self::InvalidInt(value) => write!(formatter, "Invalid integer '{value}'"),
86            Self::ExpectedInt => formatter.write_str("Expected integer"),
87            Self::InvalidLong(value) => write!(formatter, "Invalid long '{value}'"),
88            Self::ExpectedLong => formatter.write_str("Expected long"),
89            Self::InvalidDouble(value) => write!(formatter, "Invalid double '{value}'"),
90            Self::ExpectedDouble => formatter.write_str("Expected double"),
91            Self::InvalidFloat(value) => write!(formatter, "Invalid float '{value}'"),
92            Self::ExpectedFloat => formatter.write_str("Expected float"),
93            Self::ExpectedBool => formatter.write_str("Expected bool"),
94            Self::ExpectedSymbol(symbol) => write!(formatter, "Expected '{symbol}'"),
95            Self::LiteralIncorrect(expected) => write!(formatter, "Expected literal {expected}"),
96            Self::IntegerTooLow { found, minimum } => write!(
97                formatter,
98                "Integer must not be less than {minimum}, found {found}"
99            ),
100            Self::IntegerTooHigh { found, maximum } => write!(
101                formatter,
102                "Integer must not be more than {maximum}, found {found}"
103            ),
104            Self::LongTooLow { found, minimum } => write!(
105                formatter,
106                "Long must not be less than {minimum}, found {found}"
107            ),
108            Self::LongTooHigh { found, maximum } => write!(
109                formatter,
110                "Long must not be more than {maximum}, found {found}"
111            ),
112            Self::FloatTooLow { found, minimum } => write!(
113                formatter,
114                "Float must not be less than {minimum}, found {found}"
115            ),
116            Self::FloatTooHigh { found, maximum } => write!(
117                formatter,
118                "Float must not be more than {maximum}, found {found}"
119            ),
120            Self::DoubleTooLow { found, minimum } => write!(
121                formatter,
122                "Double must not be less than {minimum}, found {found}"
123            ),
124            Self::DoubleTooHigh { found, maximum } => write!(
125                formatter,
126                "Double must not be more than {maximum}, found {found}"
127            ),
128            Self::ExpectedArgumentSeparator => formatter
129                .write_str("Expected whitespace to end one argument, but found trailing data"),
130        }
131    }
132}
133
134impl CommandSyntaxErrorKind {
135    fn component(&self) -> TextComponent {
136        match self {
137            Self::UnknownCommand => TextComponent::from(&translations::COMMAND_UNKNOWN_COMMAND),
138            Self::UnknownArgument => TextComponent::from(&translations::COMMAND_UNKNOWN_ARGUMENT),
139            Self::Dynamic(message) => message.as_ref().clone(),
140            Self::ExpectedStartOfQuote => {
141                TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_START)
142            }
143            Self::ExpectedEndOfQuote => {
144                TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_END)
145            }
146            Self::InvalidEscape(character) => translations::PARSING_QUOTE_ESCAPE
147                .message([character.to_string()])
148                .component(),
149            Self::InvalidBool(value) => translations::PARSING_BOOL_INVALID
150                .message([value.to_string()])
151                .component(),
152            Self::InvalidInt(value) => translations::PARSING_INT_INVALID
153                .message([value.to_string()])
154                .component(),
155            Self::ExpectedInt => TextComponent::from(&translations::PARSING_INT_EXPECTED),
156            Self::InvalidLong(value) => translations::PARSING_LONG_INVALID
157                .message([value.to_string()])
158                .component(),
159            Self::ExpectedLong => TextComponent::from(&translations::PARSING_LONG_EXPECTED),
160            Self::InvalidDouble(value) => translations::PARSING_DOUBLE_INVALID
161                .message([value.to_string()])
162                .component(),
163            Self::ExpectedDouble => TextComponent::from(&translations::PARSING_DOUBLE_EXPECTED),
164            Self::InvalidFloat(value) => translations::PARSING_FLOAT_INVALID
165                .message([value.to_string()])
166                .component(),
167            Self::ExpectedFloat => TextComponent::from(&translations::PARSING_FLOAT_EXPECTED),
168            Self::ExpectedBool => TextComponent::from(&translations::PARSING_BOOL_EXPECTED),
169            Self::ExpectedSymbol(symbol) => translations::PARSING_EXPECTED
170                .message([symbol.to_string()])
171                .component(),
172            Self::LiteralIncorrect(expected) => translations::ARGUMENT_LITERAL_INCORRECT
173                .message([expected.to_string()])
174                .component(),
175            Self::IntegerTooLow { found, minimum } => translations::ARGUMENT_INTEGER_LOW
176                .message([minimum.to_string(), found.to_string()])
177                .component(),
178            Self::IntegerTooHigh { found, maximum } => translations::ARGUMENT_INTEGER_BIG
179                .message([maximum.to_string(), found.to_string()])
180                .component(),
181            Self::LongTooLow { found, minimum } => translations::ARGUMENT_LONG_LOW
182                .message([minimum.to_string(), found.to_string()])
183                .component(),
184            Self::LongTooHigh { found, maximum } => translations::ARGUMENT_LONG_BIG
185                .message([maximum.to_string(), found.to_string()])
186                .component(),
187            Self::FloatTooLow { found, minimum } => translations::ARGUMENT_FLOAT_LOW
188                .message([minimum.to_string(), found.to_string()])
189                .component(),
190            Self::FloatTooHigh { found, maximum } => translations::ARGUMENT_FLOAT_BIG
191                .message([maximum.to_string(), found.to_string()])
192                .component(),
193            Self::DoubleTooLow { found, minimum } => translations::ARGUMENT_DOUBLE_LOW
194                .message([minimum.to_string(), found.to_string()])
195                .component(),
196            Self::DoubleTooHigh { found, maximum } => translations::ARGUMENT_DOUBLE_BIG
197                .message([maximum.to_string(), found.to_string()])
198                .component(),
199            Self::ExpectedArgumentSeparator => {
200                TextComponent::from(&translations::COMMAND_EXPECTED_SEPARATOR)
201            }
202        }
203    }
204}
205
206/// A Brigadier-compatible parsing error with input context.
207///
208/// Dynamic floating-point values use Rust's standard display formatting; parsing and bounds
209/// behavior remain Brigadier-compatible.
210#[derive(Clone, Debug, PartialEq)]
211pub(crate) struct CommandSyntaxError {
212    kind: CommandSyntaxErrorKind,
213    context: Option<CommandErrorContext>,
214}
215
216#[derive(Clone, Debug, PartialEq)]
217struct CommandErrorContext {
218    input: Box<str>,
219    cursor: usize,
220    byte_cursor: usize,
221}
222
223impl CommandSyntaxError {
224    pub(super) fn new(
225        kind: CommandSyntaxErrorKind,
226        input: &str,
227        cursor: usize,
228        byte_cursor: usize,
229    ) -> Self {
230        Self {
231            kind,
232            context: Some(CommandErrorContext {
233                input: input.into(),
234                cursor,
235                byte_cursor,
236            }),
237        }
238    }
239
240    /// Creates a runtime command failure without parser input context.
241    pub(crate) fn dynamic(message: impl Into<TextComponent>) -> Self {
242        Self {
243            kind: CommandSyntaxErrorKind::Dynamic(Box::new(message.into())),
244            context: None,
245        }
246    }
247
248    /// Returns the specific built-in error.
249    pub(crate) const fn kind(&self) -> &CommandSyntaxErrorKind {
250        &self.kind
251    }
252
253    /// Returns the command input that failed.
254    pub(crate) fn input(&self) -> Option<&str> {
255        self.context.as_ref().map(|context| context.input.as_ref())
256    }
257
258    /// Returns the failure position in UTF-16 code units.
259    pub(crate) const fn cursor(&self) -> Option<usize> {
260        match &self.context {
261            Some(context) => Some(context.cursor),
262            None => None,
263        }
264    }
265
266    /// Returns the error message without input context.
267    pub(crate) fn raw_message(&self) -> String {
268        self.kind.to_string()
269    }
270
271    /// Returns the vanilla translatable component for this error.
272    pub(crate) fn message_component(&self) -> TextComponent {
273        self.kind.component()
274    }
275
276    /// Builds vanilla's styled, clickable parser-context line.
277    pub(crate) fn context_component(&self) -> Option<TextComponent> {
278        let context = self.context.as_ref()?;
279        let input_before_cursor = &context.input[..context.byte_cursor];
280        let mut context_start = context.byte_cursor;
281        let mut context_length = 0;
282
283        for (byte_index, character) in input_before_cursor.char_indices().rev() {
284            let character_length = character.len_utf16();
285            if context_length + character_length > CONTEXT_AMOUNT {
286                break;
287            }
288            context_length += character_length;
289            context_start = byte_index;
290        }
291
292        let suggested_command = if context.input.starts_with('/') {
293            context.input.to_string()
294        } else {
295            format!("/{}", context.input)
296        };
297        let mut component = TextComponent::new()
298            .color(Color::Gray)
299            .click_event(ClickEvent::suggest_command(suggested_command));
300        if context.cursor > CONTEXT_AMOUNT {
301            component = component.add_child(TextComponent::const_plain("..."));
302        }
303        component = component.add_child(TextComponent::plain(
304            context.input[context_start..context.byte_cursor].to_owned(),
305        ));
306        if context.byte_cursor < context.input.len() {
307            component = component.add_child(
308                TextComponent::plain(context.input[context.byte_cursor..].to_owned())
309                    .color(Color::Red)
310                    .underlined(true),
311            );
312        }
313        Some(
314            component.add_child(
315                TextComponent::from(&translations::COMMAND_CONTEXT_HERE)
316                    .color(Color::Red)
317                    .italic(true),
318            ),
319        )
320    }
321
322    /// Returns the input immediately before the error marker.
323    pub(crate) fn context(&self) -> Option<String> {
324        let context = self.context.as_ref()?;
325        let input_before_cursor = &context.input[..context.byte_cursor];
326        let mut context_start = context.byte_cursor;
327        let mut context_length = 0;
328
329        for (byte_index, character) in input_before_cursor.char_indices().rev() {
330            let character_length = character.len_utf16();
331            if context_length + character_length > CONTEXT_AMOUNT {
332                break;
333            }
334            context_length += character_length;
335            context_start = byte_index;
336        }
337
338        let prefix = if context.cursor > CONTEXT_AMOUNT {
339            "..."
340        } else {
341            ""
342        };
343        Some(format!(
344            "{prefix}{}<--[HERE]",
345            &context.input[context_start..context.byte_cursor]
346        ))
347    }
348}
349
350impl fmt::Display for CommandSyntaxError {
351    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352        let Some(context) = &self.context else {
353            return self.kind.fmt(formatter);
354        };
355        let Some(display_context) = self.context() else {
356            return self.kind.fmt(formatter);
357        };
358        write!(
359            formatter,
360            "{} at position {}: {display_context}",
361            self.kind, context.cursor
362        )
363    }
364}
365
366impl Error for CommandSyntaxError {}