Skip to main content

steel_core/command/brigadier/
node.rs

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