Skip to main content

steel_core/command/brigadier/
node.rs

1//! Command graph nodes and registration errors.
2
3use super::{
4    ArgumentSuggestionContext, BrigadierRuntime, CommandArgumentParser, CommandRuntime,
5    SuggestionProvider, SuggestionsBuilder,
6};
7use std::{fmt, sync::Arc};
8use thiserror::Error;
9
10type RequirementPredicate<S> = Arc<dyn Fn(&S) -> bool + Send + Sync>;
11type SyncSuggestionProvider<S, A> = Arc<dyn SuggestionProvider<S, A>>;
12
13/// Identifies a node in one command dispatcher.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub(crate) struct NodeId {
16    pub(super) dispatcher: u64,
17    pub(super) index: usize,
18}
19
20impl NodeId {
21    pub(super) const fn new(dispatcher: u64, index: usize) -> Self {
22        Self { dispatcher, index }
23    }
24}
25
26/// Selects either an existing dispatcher node or the root currently being registered.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub(crate) enum CommandRedirectTarget {
29    /// A dispatcher node that already exists.
30    Node(NodeId),
31    /// The root of the command tree containing this redirect.
32    CommandRoot,
33}
34
35impl From<NodeId> for CommandRedirectTarget {
36    fn from(value: NodeId) -> Self {
37        Self::Node(value)
38    }
39}
40
41/// The externally relevant category of a command node.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub(crate) enum NodeKind {
44    /// The dispatcher root.
45    Root,
46    /// A literal token.
47    Literal,
48    /// A parsed argument.
49    Argument,
50}
51
52/// A source predicate attached to a command node.
53pub(crate) struct CommandRequirement<S> {
54    predicate: Option<RequirementPredicate<S>>,
55    kind: Option<CommandRequirementKind>,
56}
57
58/// Why a command node has a source requirement.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub(crate) enum CommandRequirementKind {
61    /// Access depends on Steel's authorization state.
62    Authorization,
63    /// Access depends on execution context rather than permission.
64    Context,
65}
66
67impl<S> CommandRequirement<S> {
68    /// Creates a requirement that permits every source.
69    pub(crate) const fn allow_all() -> Self {
70        Self {
71            predicate: None,
72            kind: None,
73        }
74    }
75
76    /// Creates a permission-backed requirement with stable identity.
77    pub(crate) fn authorization(predicate: impl Fn(&S) -> bool + Send + Sync + 'static) -> Self {
78        Self {
79            predicate: Some(Arc::new(predicate)),
80            kind: Some(CommandRequirementKind::Authorization),
81        }
82    }
83
84    /// Creates a non-permission source requirement with stable identity.
85    pub(crate) fn contextual(predicate: impl Fn(&S) -> bool + Send + Sync + 'static) -> Self {
86        Self {
87            predicate: Some(Arc::new(predicate)),
88            kind: Some(CommandRequirementKind::Context),
89        }
90    }
91
92    /// Returns whether `source` can use the node.
93    pub(crate) fn allows(&self, source: &S) -> bool {
94        self.predicate
95            .as_ref()
96            .is_none_or(|predicate| predicate(source))
97    }
98
99    /// Returns whether the client should treat this node as permission restricted.
100    pub(crate) const fn is_authorization(&self) -> bool {
101        matches!(self.kind, Some(CommandRequirementKind::Authorization))
102    }
103
104    pub(super) fn and(self, other: Self) -> Self
105    where
106        S: 'static,
107    {
108        let kind = match (self.kind, other.kind) {
109            (Some(CommandRequirementKind::Authorization), _)
110            | (_, Some(CommandRequirementKind::Authorization)) => {
111                Some(CommandRequirementKind::Authorization)
112            }
113            (Some(CommandRequirementKind::Context), _)
114            | (_, Some(CommandRequirementKind::Context)) => Some(CommandRequirementKind::Context),
115            (None, None) => None,
116        };
117        let predicate = match (self.predicate, other.predicate) {
118            (None, None) => None,
119            (Some(predicate), None) | (None, Some(predicate)) => Some(predicate),
120            (Some(first), Some(second)) => {
121                let combined: RequirementPredicate<S> =
122                    Arc::new(move |source| first(source) && second(source));
123                Some(combined)
124            }
125        };
126        Self { predicate, kind }
127    }
128
129    pub(super) fn is_compatible_with(&self, other: &Self) -> bool {
130        self.kind == other.kind
131            && match (&self.predicate, &other.predicate) {
132                (None, None) => true,
133                (Some(first), Some(second)) => Arc::ptr_eq(first, second),
134                (None, Some(_)) | (Some(_), None) => false,
135            }
136    }
137}
138
139impl<S> Clone for CommandRequirement<S> {
140    fn clone(&self) -> Self {
141        Self {
142            predicate: self.predicate.as_ref().map(Arc::clone),
143            kind: self.kind,
144        }
145    }
146}
147
148impl<S> fmt::Debug for CommandRequirement<S> {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter
151            .debug_struct("CommandRequirement")
152            .field(
153                "predicate",
154                &self.predicate.as_ref().map(|_| "<source predicate>"),
155            )
156            .field("kind", &self.kind)
157            .finish()
158    }
159}
160
161pub(super) struct CommandRedirect<S, R = BrigadierRuntime>
162where
163    R: CommandRuntime<S>,
164{
165    pub(super) target: CommandRedirectTarget,
166    pub(super) modifier: Option<Arc<R::Modifier>>,
167    pub(super) forks: bool,
168}
169
170impl<S, R> CommandRedirect<S, R>
171where
172    R: CommandRuntime<S>,
173{
174    pub(super) const fn identity(target: CommandRedirectTarget) -> Self {
175        Self {
176            target,
177            modifier: None,
178            forks: false,
179        }
180    }
181
182    pub(super) const fn with_modifier(
183        target: CommandRedirectTarget,
184        modifier: Arc<R::Modifier>,
185        forks: bool,
186    ) -> Self {
187        Self {
188            target,
189            modifier: Some(modifier),
190            forks,
191        }
192    }
193
194    fn resolve_command_root(&mut self, command_root: NodeId) {
195        if self.target == CommandRedirectTarget::CommandRoot {
196            self.target = CommandRedirectTarget::Node(command_root);
197        }
198    }
199
200    fn is_compatible_with(&self, other: &Self) -> bool {
201        self.target == other.target
202            && self.forks == other.forks
203            && match (&self.modifier, &other.modifier) {
204                (None, None) => true,
205                (Some(first), Some(second)) => Arc::ptr_eq(first, second),
206                (None, Some(_)) | (Some(_), None) => false,
207            }
208    }
209}
210
211impl<S, R> Clone for CommandRedirect<S, R>
212where
213    R: CommandRuntime<S>,
214{
215    fn clone(&self) -> Self {
216        Self {
217            target: self.target,
218            modifier: self.modifier.as_ref().map(Arc::clone),
219            forks: self.forks,
220        }
221    }
222}
223
224pub(super) enum CommandNodeData<S, A: CommandArgumentParser<S>> {
225    Root,
226    Literal(Box<str>),
227    Argument(Box<str>, ArgumentData<S, A>),
228}
229
230impl<S, A: CommandArgumentParser<S>> CommandNodeData<S, A> {
231    pub(super) fn name(&self) -> &str {
232        match self {
233            Self::Root => "",
234            Self::Literal(name) | Self::Argument(name, _) => name,
235        }
236    }
237
238    pub(super) const fn kind(&self) -> NodeKind {
239        match self {
240            Self::Root => NodeKind::Root,
241            Self::Literal(_) => NodeKind::Literal,
242            Self::Argument { .. } => NodeKind::Argument,
243        }
244    }
245}
246
247impl<S, A: CommandArgumentParser<S>> CommandNodeData<S, A>
248where
249    A: PartialEq,
250{
251    fn collision_with(&self, other: &Self) -> Option<RegistrationErrorKind> {
252        let name = other.name().into();
253        match (self, other) {
254            (Self::Literal(first), Self::Literal(second)) if first == second => None,
255            (
256                Self::Argument(
257                    first_name,
258                    ArgumentData {
259                        argument_type: first_type,
260                        ..
261                    },
262                ),
263                Self::Argument(
264                    second_name,
265                    ArgumentData {
266                        argument_type: second_type,
267                        ..
268                    },
269                ),
270            ) if first_name == second_name && first_type == second_type => None,
271            (Self::Argument(first, _), Self::Argument(second, _)) if first == second => {
272                Some(RegistrationErrorKind::ArgumentTypeCollision { name })
273            }
274            _ => Some(RegistrationErrorKind::NodeKindCollision {
275                name,
276                existing: self.kind(),
277                incoming: other.kind(),
278            }),
279        }
280    }
281}
282
283impl<S, A: CommandArgumentParser<S>> Clone for CommandNodeData<S, A>
284where
285    A: Clone,
286{
287    fn clone(&self) -> Self {
288        match self {
289            Self::Root => Self::Root,
290            Self::Literal(name) => Self::Literal(name.clone()),
291            Self::Argument(name, argument_data) => {
292                Self::Argument(name.clone(), argument_data.clone())
293            }
294        }
295    }
296}
297
298pub(crate) struct ArgumentData<S, A> {
299    pub(super) argument_type: A,
300    pub(super) custom_suggestions: Option<SyncSuggestionProvider<S, A>>,
301}
302
303impl<S, A> ArgumentData<S, A> {
304    pub(crate) const fn new(argument_type: A) -> Self {
305        Self {
306            argument_type,
307            custom_suggestions: None,
308        }
309    }
310
311    pub(crate) const fn argument_type(&self) -> &A {
312        &self.argument_type
313    }
314}
315
316impl<S, A> ArgumentData<S, A> {
317    pub(crate) fn list_suggestions(
318        &self,
319        context: &ArgumentSuggestionContext<'_, S, <A as CommandArgumentParser<S>>::Value>,
320        builder: &mut SuggestionsBuilder<'_>,
321    ) where
322        A: CommandArgumentParser<S>,
323    {
324        if let Some(suggestions) = self.custom_suggestions.as_ref() {
325            suggestions.list_suggestions(context, builder);
326        } else {
327            self.argument_type.list_suggestions(context, builder);
328        }
329    }
330
331    pub(crate) fn has_custom_suggestions(&self) -> bool {
332        self.custom_suggestions.is_some()
333    }
334}
335
336impl<S, A: CommandArgumentParser<S>> Clone for ArgumentData<S, A>
337where
338    A: Clone,
339{
340    fn clone(&self) -> Self {
341        Self {
342            argument_type: self.argument_type.clone(),
343            custom_suggestions: self.custom_suggestions.clone(),
344        }
345    }
346}
347
348/// One node stored in the dispatcher's arena.
349pub(crate) struct CommandNode<S, R = BrigadierRuntime>
350where
351    R: CommandRuntime<S>,
352{
353    pub(super) data: CommandNodeData<S, R::Argument>,
354    pub(super) children: Vec<NodeId>,
355    pub(super) executor: Option<Arc<R::Executor>>,
356    pub(super) requirement: CommandRequirement<S>,
357    pub(super) execution_requirement: CommandRequirement<S>,
358    pub(super) redirect: Option<CommandRedirect<S, R>>,
359}
360
361impl<S, R> CommandNode<S, R>
362where
363    R: CommandRuntime<S>,
364{
365    pub(super) const fn root() -> Self {
366        Self {
367            data: CommandNodeData::Root,
368            children: Vec::new(),
369            executor: None,
370            requirement: CommandRequirement::allow_all(),
371            execution_requirement: CommandRequirement::allow_all(),
372            redirect: None,
373        }
374    }
375
376    /// Returns the node name.
377    pub(crate) fn name(&self) -> &str {
378        self.data.name()
379    }
380
381    /// Returns whether this node has a command callback.
382    pub(crate) const fn is_executable(&self) -> bool {
383        self.executor.is_some()
384    }
385
386    /// Returns whether this source may run the node's executor.
387    pub(crate) fn can_execute(&self, source: &S) -> bool {
388        self.executor.is_some()
389            && self.requirement.allows(source)
390            && self.execution_requirement.allows(source)
391    }
392
393    /// Returns this node's redirect target.
394    pub(crate) fn redirect(&self) -> Option<NodeId> {
395        self.redirect
396            .as_ref()
397            .map(|redirect| match redirect.target {
398                CommandRedirectTarget::Node(target) => target,
399                CommandRedirectTarget::CommandRoot => {
400                    unreachable!("registered command redirects have concrete targets")
401                }
402            })
403    }
404
405    /// Returns whether this node's redirect forks its command source.
406    pub(crate) fn is_forked_redirect(&self) -> bool {
407        self.redirect
408            .as_ref()
409            .is_some_and(|redirect| redirect.forks)
410    }
411
412    /// Returns whether this node transforms sources while redirecting.
413    pub(crate) fn has_redirect_modifier(&self) -> bool {
414        self.redirect
415            .as_ref()
416            .is_some_and(|redirect| redirect.modifier.is_some())
417    }
418
419    /// Returns the externally visible node category.
420    pub(crate) const fn kind(&self) -> NodeKind {
421        self.data.kind()
422    }
423
424    /// Returns this node's argument data when it is an argument node.
425    pub(crate) const fn argument_data(&self) -> Option<&ArgumentData<S, R::Argument>> {
426        match &self.data {
427            CommandNodeData::Argument(_, argument_data) => Some(argument_data),
428            CommandNodeData::Root | CommandNodeData::Literal(_) => None,
429        }
430    }
431
432    /// Returns this node's argument parser when it is an argument node.
433    pub(crate) const fn argument_type(&self) -> Option<&R::Argument> {
434        match &self.data {
435            CommandNodeData::Argument(_, argument_data) => Some(argument_data.argument_type()),
436            CommandNodeData::Root | CommandNodeData::Literal(_) => None,
437        }
438    }
439
440    /// Returns whether this node is available to `source`.
441    pub(crate) fn allows(&self, source: &S) -> bool {
442        self.requirement.allows(source)
443    }
444
445    /// Returns whether this node is guarded by authorization.
446    pub(crate) const fn is_restricted(&self) -> bool {
447        self.requirement.is_authorization() || self.execution_requirement.is_authorization()
448    }
449
450    pub(super) fn validate_compatible(
451        &self,
452        incoming: &UnregisteredCommandNode<S, R>,
453    ) -> Result<(), RegistrationError> {
454        if let Some(kind) = self.data.collision_with(&incoming.data) {
455            return Err(RegistrationError::new(kind));
456        }
457        if !self.requirement.is_compatible_with(&incoming.requirement) {
458            return Err(RegistrationError::new(
459                RegistrationErrorKind::RequirementCollision {
460                    name: incoming.name().into(),
461                },
462            ));
463        }
464        if !self
465            .execution_requirement
466            .is_compatible_with(&incoming.execution_requirement)
467        {
468            return Err(RegistrationError::new(
469                RegistrationErrorKind::RequirementCollision {
470                    name: incoming.name().into(),
471                },
472            ));
473        }
474        if !redirects_are_compatible(self.redirect.as_ref(), incoming.redirect.as_ref()) {
475            return Err(RegistrationError::new(
476                RegistrationErrorKind::RedirectCollision {
477                    name: incoming.name().into(),
478                },
479            ));
480        }
481        Ok(())
482    }
483}
484
485pub(super) struct UnregisteredCommandNode<S, R = BrigadierRuntime>
486where
487    R: CommandRuntime<S>,
488{
489    pub(super) data: CommandNodeData<S, R::Argument>,
490    pub(super) children: Vec<Self>,
491    pub(super) executor: Option<Arc<R::Executor>>,
492    pub(super) requirement: CommandRequirement<S>,
493    pub(super) execution_requirement: CommandRequirement<S>,
494    pub(super) redirect: Option<CommandRedirect<S, R>>,
495}
496
497impl<S, R> UnregisteredCommandNode<S, R>
498where
499    R: CommandRuntime<S>,
500{
501    pub(super) fn name(&self) -> &str {
502        self.data.name()
503    }
504
505    pub(super) const fn kind(&self) -> NodeKind {
506        self.data.kind()
507    }
508
509    pub(super) fn resolve_command_root(&mut self, command_root: NodeId) {
510        if let Some(redirect) = &mut self.redirect {
511            redirect.resolve_command_root(command_root);
512        }
513        for child in &mut self.children {
514            child.resolve_command_root(command_root);
515        }
516    }
517
518    pub(super) fn merge(&mut self, mut incoming: Self) -> Result<(), RegistrationError> {
519        self.validate_compatible(&incoming)?;
520        if incoming.executor.is_some() {
521            self.executor = incoming.executor.take();
522        }
523        for child in incoming.children {
524            merge_or_push(&mut self.children, child)?;
525        }
526        Ok(())
527    }
528
529    fn validate_compatible(&self, incoming: &Self) -> Result<(), RegistrationError> {
530        if let Some(kind) = self.data.collision_with(&incoming.data) {
531            return Err(RegistrationError::new(kind));
532        }
533        if !self.requirement.is_compatible_with(&incoming.requirement) {
534            return Err(RegistrationError::new(
535                RegistrationErrorKind::RequirementCollision {
536                    name: incoming.name().into(),
537                },
538            ));
539        }
540        if !self
541            .execution_requirement
542            .is_compatible_with(&incoming.execution_requirement)
543        {
544            return Err(RegistrationError::new(
545                RegistrationErrorKind::RequirementCollision {
546                    name: incoming.name().into(),
547                },
548            ));
549        }
550        if !redirects_are_compatible(self.redirect.as_ref(), incoming.redirect.as_ref()) {
551            return Err(RegistrationError::new(
552                RegistrationErrorKind::RedirectCollision {
553                    name: incoming.name().into(),
554                },
555            ));
556        }
557        Ok(())
558    }
559}
560
561fn redirects_are_compatible<S, R>(
562    first: Option<&CommandRedirect<S, R>>,
563    second: Option<&CommandRedirect<S, R>>,
564) -> bool
565where
566    R: CommandRuntime<S>,
567{
568    match (first, second) {
569        (None, None) => true,
570        (Some(first), Some(second)) => first.is_compatible_with(second),
571        (None, Some(_)) | (Some(_), None) => false,
572    }
573}
574
575pub(super) fn merge_or_push<S, R>(
576    nodes: &mut Vec<UnregisteredCommandNode<S, R>>,
577    incoming: UnregisteredCommandNode<S, R>,
578) -> Result<(), RegistrationError>
579where
580    R: CommandRuntime<S>,
581{
582    let Some(existing) = nodes
583        .iter_mut()
584        .find(|existing| existing.name() == incoming.name())
585    else {
586        nodes.push(incoming);
587        return Ok(());
588    };
589    existing.merge(incoming)
590}
591
592/// A command registration failure.
593#[derive(Debug, Error)]
594#[error("{kind}")]
595pub(crate) struct RegistrationError {
596    kind: RegistrationErrorKind,
597}
598
599impl RegistrationError {
600    pub(super) const fn new(kind: RegistrationErrorKind) -> Self {
601        Self { kind }
602    }
603
604    /// Returns the specific registration failure.
605    pub(crate) const fn kind(&self) -> &RegistrationErrorKind {
606        &self.kind
607    }
608}
609
610/// Identifies why command registration failed.
611#[derive(Clone, Debug, Error, PartialEq, Eq)]
612pub(crate) enum RegistrationErrorKind {
613    /// Only literals may be registered directly under the root.
614    #[error("only literal command nodes can be registered at the root")]
615    ArgumentRoot,
616    /// The two nodes sharing a name have different categories.
617    #[error("command node '{name}' is already registered as {existing:?}, not {incoming:?}")]
618    NodeKindCollision {
619        name: Box<str>,
620        existing: NodeKind,
621        incoming: NodeKind,
622    },
623    /// Argument nodes sharing a name use different parsers.
624    #[error("argument node '{name}' is already registered with a different parser")]
625    ArgumentTypeCollision { name: Box<str> },
626    /// Nodes sharing a name use predicates with different identities.
627    #[error("command node '{name}' is already registered with a different requirement")]
628    RequirementCollision { name: Box<str> },
629    /// Nodes sharing a name have different redirects.
630    #[error("command node '{name}' is already registered with a different redirect")]
631    RedirectCollision { name: Box<str> },
632    /// A redirected node also has children.
633    #[error("redirected command node '{name}' cannot have children")]
634    RedirectWithChildren { name: Box<str> },
635    /// A redirect points outside its dispatcher.
636    #[error("redirect target {target:?} does not belong to this dispatcher")]
637    InvalidRedirectTarget { target: NodeId },
638}