Skip to main content

steel_core/command/registration/
mod.rs

1//! Stable command identities and collision-aware dispatcher construction.
2
3use std::{collections::BTreeSet, iter::once};
4
5use rustc_hash::{FxHashMap, FxHashSet};
6use steel_utils::Identifier;
7use thiserror::Error;
8
9use super::{
10    brigadier::{
11        CommandDispatcher, CommandNodeBuilder, CommandRequirement, CommandRequirementRoute, NodeId,
12        RegistrationError,
13    },
14    execution::{CommandPermissionSource, SteelCommandRuntime},
15};
16use crate::permission::{
17    PermissionExpr, PermissionKey, PermissionKeyError, PermissionSegment, PermissionState,
18};
19
20type CommandFactory<S> =
21    dyn FnOnce(NodeId) -> CommandNodeBuilder<S, SteelCommandRuntime> + Send + 'static;
22
23pub(crate) const ENTITY_SELECTOR_PERMISSION_KEY: &str = "minecraft.selector";
24pub(crate) const ENTITY_SELECTOR_ADVANCED_PERMISSION_KEY: &str = "minecraft.selector.advanced";
25
26pub(crate) fn entity_selector_permission_expr() -> Result<PermissionExpr, PermissionKeyError> {
27    PermissionKey::parse(ENTITY_SELECTOR_PERMISSION_KEY).map(PermissionExpr::key)
28}
29
30pub(crate) fn entity_selector_advanced_permission_expr()
31-> Result<PermissionExpr, PermissionKeyError> {
32    PermissionKey::parse(ENTITY_SELECTOR_ADVANCED_PERMISSION_KEY).map(PermissionExpr::key)
33}
34
35/// One complete command tree and its stable owner identity.
36pub(crate) struct CommandRegistration<S>
37where
38    S: CommandPermissionSource,
39{
40    id: Identifier,
41    aliases: Vec<Box<str>>,
42    permission: Option<PermissionExpr>,
43    subcommand_permissions: Vec<Vec<Box<str>>>,
44    default_access: bool,
45    factory: Box<CommandFactory<S>>,
46}
47
48impl<S> CommandRegistration<S>
49where
50    S: CommandPermissionSource,
51{
52    /// Declares a command whose factory receives the target dispatcher's root.
53    pub(crate) fn new(
54        id: Identifier,
55        factory: impl FnOnce(NodeId) -> CommandNodeBuilder<S, SteelCommandRuntime> + Send + 'static,
56    ) -> Self {
57        Self {
58            id,
59            aliases: Vec::new(),
60            permission: None,
61            subcommand_permissions: Vec::new(),
62            default_access: false,
63            factory: Box::new(factory),
64        }
65    }
66
67    /// Adds a fixed unqualified alias owned by this command.
68    #[must_use]
69    pub(crate) fn alias(mut self, alias: impl Into<Box<str>>) -> Self {
70        self.aliases.push(alias.into());
71        self
72    }
73
74    /// Allows an unset root permission while still respecting an explicit deny.
75    #[must_use]
76    pub(crate) const fn default_access(mut self) -> Self {
77        self.default_access = true;
78        self
79    }
80
81    /// Replaces the permission expression derived from this command's ID.
82    #[must_use]
83    pub(crate) fn permission(mut self, permission: PermissionExpr) -> Self {
84        self.permission = Some(permission);
85        self
86    }
87
88    /// Allows a literal path through a permission derived from the command ID.
89    ///
90    /// The root permission remains a fallback grant. For example,
91    /// `minecraft.command.tick.freeze` permits only `/tick freeze`, while
92    /// `minecraft.command.tick` permits every tick subcommand.
93    #[must_use]
94    pub(crate) fn subcommand_permission<I, T>(mut self, path: I) -> Self
95    where
96        I: IntoIterator<Item = T>,
97        T: Into<Box<str>>,
98    {
99        self.subcommand_permissions
100            .push(path.into_iter().map(Into::into).collect());
101        self
102    }
103
104    fn validate(&self) -> Result<(), CommandRegistrationError> {
105        if self.id.namespace.is_empty()
106            || self.id.path.is_empty()
107            || !Identifier::validate(&self.id.namespace, &self.id.path)
108        {
109            return Err(CommandRegistrationError::InvalidCommandId(self.id.clone()));
110        }
111
112        let mut roots = FxHashSet::default();
113        roots.insert(self.id.path.as_ref());
114        for alias in &self.aliases {
115            validate_alias(alias)?;
116            if !roots.insert(alias.as_ref()) {
117                return Err(CommandRegistrationError::DuplicateOwnedRoot {
118                    id: self.id.clone(),
119                    root: alias.clone(),
120                });
121            }
122        }
123        if self.permission.is_some() && !self.subcommand_permissions.is_empty() {
124            return Err(
125                CommandRegistrationError::SubcommandPermissionsRequireDerivedRoot {
126                    id: self.id.clone(),
127                },
128            );
129        }
130        let mut permission_paths = FxHashSet::default();
131        for path in &self.subcommand_permissions {
132            if path.is_empty() {
133                return Err(CommandRegistrationError::EmptySubcommandPermissionPath {
134                    id: self.id.clone(),
135                });
136            }
137            for segment in path {
138                PermissionSegment::parse(segment.to_string()).map_err(|source| {
139                    CommandRegistrationError::InvalidSubcommandPermissionPath {
140                        id: self.id.clone(),
141                        path: display_permission_path(path),
142                        source,
143                    }
144                })?;
145            }
146            let path = display_permission_path(path);
147            if !permission_paths.insert(path.clone()) {
148                return Err(
149                    CommandRegistrationError::DuplicateSubcommandPermissionPath {
150                        id: self.id.clone(),
151                        path,
152                    },
153                );
154            }
155        }
156        Ok(())
157    }
158}
159
160/// Collects declarations before atomically constructing a dispatcher.
161pub(crate) struct CommandDispatcherBuilder<S>
162where
163    S: CommandPermissionSource,
164{
165    registrations: Vec<CommandRegistration<S>>,
166    ids: FxHashSet<Identifier>,
167    declared_permissions: BTreeSet<PermissionKey>,
168}
169
170/// Built dispatcher and its discovery-only permission declarations.
171pub(crate) struct RegisteredCommandDispatcher<S>
172where
173    S: CommandPermissionSource,
174{
175    pub(crate) dispatcher: CommandDispatcher<S, SteelCommandRuntime>,
176    pub(crate) permissions: Vec<PermissionKey>,
177}
178
179impl<S> CommandDispatcherBuilder<S>
180where
181    S: CommandPermissionSource,
182{
183    pub(crate) fn new() -> Self {
184        Self {
185            registrations: Vec::new(),
186            ids: FxHashSet::default(),
187            declared_permissions: BTreeSet::new(),
188        }
189    }
190
191    /// Declares a non-command permission for discovery and autocomplete.
192    pub(crate) fn declare_permission(
193        &mut self,
194        permission: impl Into<String>,
195    ) -> Result<(), CommandRegistrationError> {
196        let value = permission.into();
197        let permission = PermissionKey::parse(value.clone()).map_err(|source| {
198            CommandRegistrationError::InvalidInternalPermission { value, source }
199        })?;
200        self.declared_permissions.insert(permission);
201        Ok(())
202    }
203
204    /// Adds one declaration. Earlier declarations win unqualified collisions.
205    pub(crate) fn register(
206        &mut self,
207        registration: CommandRegistration<S>,
208    ) -> Result<(), CommandRegistrationError> {
209        registration.validate()?;
210        if !self.ids.insert(registration.id.clone()) {
211            return Err(CommandRegistrationError::DuplicateCommandId(
212                registration.id,
213            ));
214        }
215        self.registrations.push(registration);
216        Ok(())
217    }
218
219    pub(crate) fn extend(&mut self, other: Self) -> Result<(), CommandRegistrationError> {
220        let Self {
221            registrations,
222            declared_permissions,
223            ..
224        } = other;
225        self.declared_permissions.extend(declared_permissions);
226        for registration in registrations {
227            self.register(registration)?;
228        }
229        Ok(())
230    }
231
232    /// Builds the complete graph without exposing a partially registered dispatcher.
233    #[cfg(test)]
234    pub(crate) fn build(
235        self,
236    ) -> Result<CommandDispatcher<S, SteelCommandRuntime>, CommandRegistrationError> {
237        self.build_with_permissions()
238            .map(|registered| registered.dispatcher)
239    }
240
241    /// Builds the graph and retains permission keys for command autocomplete.
242    pub(crate) fn build_with_permissions(
243        self,
244    ) -> Result<RegisteredCommandDispatcher<S>, CommandRegistrationError> {
245        let mut dispatcher = CommandDispatcher::new();
246        let dispatcher_root = dispatcher.root();
247        let mut resolved = Vec::with_capacity(self.registrations.len());
248        let mut permissions = self.declared_permissions;
249
250        for registration in self.registrations {
251            let CommandRegistration {
252                id,
253                aliases,
254                permission,
255                subcommand_permissions,
256                default_access,
257                factory,
258            } = registration;
259            let (root_permission, derived_root) = if let Some(permission) = permission {
260                (permission, None)
261            } else {
262                let key = derived_command_permission_key(&id)?;
263                (PermissionExpr::key(key.clone()), Some(key))
264            };
265            collect_permission_keys(&root_permission, &mut permissions);
266            let root = apply_registration_requirements(
267                factory(dispatcher_root),
268                &id,
269                root_permission,
270                derived_root.as_ref(),
271                &subcommand_permissions,
272                default_access,
273                &mut permissions,
274            )?;
275            let Some(root_name) = root.literal_name() else {
276                return Err(CommandRegistrationError::RootMustBeLiteral { id });
277            };
278            if root_name != id.path {
279                return Err(CommandRegistrationError::RootDoesNotMatchId {
280                    id,
281                    root: root_name.into(),
282                });
283            }
284            resolved.push(ResolvedCommand { id, aliases, root });
285        }
286
287        let mut claim_counts = FxHashMap::<Box<str>, usize>::default();
288        for command in &resolved {
289            for root in command.roots() {
290                *claim_counts.entry(root.into()).or_default() += 1;
291            }
292        }
293
294        let mut claimed_roots = FxHashSet::<Box<str>>::default();
295        for command in &resolved {
296            for root in command.roots() {
297                if !claimed_roots.insert(root.into()) {
298                    continue;
299                }
300                register_renamed_root(&mut dispatcher, &command.root, root)?;
301            }
302        }
303
304        for command in &resolved {
305            let collided = command
306                .roots()
307                .any(|root| claim_counts.get(root).is_some_and(|count| *count > 1));
308            if collided {
309                register_renamed_root(&mut dispatcher, &command.root, command.id.to_string())?;
310            }
311        }
312
313        Ok(RegisteredCommandDispatcher {
314            dispatcher,
315            permissions: permissions.into_iter().collect(),
316        })
317    }
318}
319
320fn apply_registration_requirements<S>(
321    mut root: CommandNodeBuilder<S, SteelCommandRuntime>,
322    id: &Identifier,
323    root_permission: PermissionExpr,
324    derived_root: Option<&PermissionKey>,
325    subcommand_permissions: &[Vec<Box<str>>],
326    default_access: bool,
327    permissions: &mut BTreeSet<PermissionKey>,
328) -> Result<CommandNodeBuilder<S, SteelCommandRuntime>, CommandRegistrationError>
329where
330    S: CommandPermissionSource,
331{
332    if subcommand_permissions.is_empty() {
333        return Ok(root.also_requires(root_permission_requirement(
334            root_permission,
335            Vec::new(),
336            default_access,
337        )));
338    }
339    let Some(derived_root) = derived_root else {
340        return Err(
341            CommandRegistrationError::SubcommandPermissionsRequireDerivedRoot { id: id.clone() },
342        );
343    };
344
345    let mut scoped_permissions = Vec::with_capacity(subcommand_permissions.len());
346    for path in subcommand_permissions {
347        let permission = derived_subcommand_permission(id, derived_root, path)?;
348        permissions.insert(permission.clone());
349        match root.literal_path_match_count(path) {
350            1 => scoped_permissions.push(permission),
351            0 => {
352                return Err(CommandRegistrationError::MissingSubcommandPermissionPath {
353                    id: id.clone(),
354                    path: display_permission_path(path),
355                });
356            }
357            matches => {
358                return Err(
359                    CommandRegistrationError::AmbiguousSubcommandPermissionPath {
360                        id: id.clone(),
361                        path: display_permission_path(path),
362                        matches,
363                    },
364                );
365            }
366        }
367    }
368    root.apply_scoped_requirements(
369        subcommand_permissions,
370        |governing_scope, descendant_scopes| {
371            let descendants = descendant_scopes
372                .iter()
373                .map(|index| scoped_permissions[*index].clone())
374                .collect::<Vec<_>>();
375            let traversal = if let Some(index) = governing_scope {
376                scoped_permission_requirement(derived_root, &scoped_permissions[index], descendants)
377            } else {
378                root_permission_requirement(root_permission.clone(), descendants, default_access)
379            };
380            let execution = if let Some(index) = governing_scope {
381                scoped_permission_requirement(derived_root, &scoped_permissions[index], Vec::new())
382            } else {
383                root_permission_requirement(root_permission.clone(), Vec::new(), default_access)
384            };
385            CommandRequirementRoute::new(traversal, execution)
386        },
387    );
388    Ok(root)
389}
390
391impl<S> Default for CommandDispatcherBuilder<S>
392where
393    S: CommandPermissionSource,
394{
395    fn default() -> Self {
396        Self::new()
397    }
398}
399
400struct ResolvedCommand<S>
401where
402    S: CommandPermissionSource,
403{
404    id: Identifier,
405    aliases: Vec<Box<str>>,
406    root: CommandNodeBuilder<S, SteelCommandRuntime>,
407}
408
409impl<S> ResolvedCommand<S>
410where
411    S: CommandPermissionSource,
412{
413    fn roots(&self) -> impl Iterator<Item = &str> {
414        once(self.id.path.as_ref()).chain(self.aliases.iter().map(AsRef::as_ref))
415    }
416}
417
418fn derived_command_permission_key(
419    id: &Identifier,
420) -> Result<PermissionKey, CommandRegistrationError> {
421    PermissionKey::parse(format!("{}.command.{}", id.namespace, id.path)).map_err(|source| {
422        CommandRegistrationError::InvalidDerivedPermission {
423            id: id.clone(),
424            source,
425        }
426    })
427}
428
429fn derived_subcommand_permission(
430    id: &Identifier,
431    root: &PermissionKey,
432    path: &[Box<str>],
433) -> Result<PermissionKey, CommandRegistrationError> {
434    let mut permission = root.clone();
435    for segment in path {
436        let segment = PermissionSegment::parse(segment.to_string()).map_err(|source| {
437            CommandRegistrationError::InvalidSubcommandPermissionPath {
438                id: id.clone(),
439                path: display_permission_path(path),
440                source,
441            }
442        })?;
443        permission = permission.child(&segment).map_err(|source| {
444            CommandRegistrationError::InvalidSubcommandPermissionPath {
445                id: id.clone(),
446                path: display_permission_path(path),
447                source,
448            }
449        })?;
450    }
451    Ok(permission)
452}
453
454fn root_permission_requirement<S>(
455    root: PermissionExpr,
456    alternatives: Vec<PermissionKey>,
457    default_access: bool,
458) -> CommandRequirement<S>
459where
460    S: CommandPermissionSource,
461{
462    if default_access {
463        let alternatives = if alternatives.is_empty() {
464            None
465        } else {
466            Some(PermissionExpr::Any(
467                alternatives.into_iter().map(PermissionExpr::key).collect(),
468            ))
469        };
470        return CommandRequirement::contextual(move |source: &S| {
471            source.permission_state(&root) != Some(PermissionState::Deny)
472                || alternatives.as_ref().is_some_and(|alternatives| {
473                    source.permission_state(alternatives) == Some(PermissionState::Allow)
474                })
475        });
476    }
477
478    let permission = alternatives
479        .into_iter()
480        .fold(root, |permission, alternative| {
481            permission | PermissionExpr::key(alternative)
482        });
483    permission_requirement(permission)
484}
485
486fn permission_requirement<S>(permission: PermissionExpr) -> CommandRequirement<S>
487where
488    S: CommandPermissionSource,
489{
490    CommandRequirement::authorization(move |source: &S| {
491        source.permission_state(&permission) == Some(PermissionState::Allow)
492    })
493}
494
495fn scoped_permission_requirement<S>(
496    root: &PermissionKey,
497    scoped: &PermissionKey,
498    alternatives: Vec<PermissionKey>,
499) -> CommandRequirement<S>
500where
501    S: CommandPermissionSource,
502{
503    let permission = alternatives.into_iter().fold(
504        PermissionExpr::scoped_key(root.clone(), scoped.clone()),
505        |permission, alternative| permission | PermissionExpr::key(alternative),
506    );
507    permission_requirement(permission)
508}
509
510fn collect_permission_keys(expression: &PermissionExpr, keys: &mut BTreeSet<PermissionKey>) {
511    match expression {
512        PermissionExpr::Key(key) => {
513            keys.insert(key.clone());
514        }
515        PermissionExpr::ScopedKey { parent, key } => {
516            keys.insert(parent.clone());
517            keys.insert(key.clone());
518        }
519        PermissionExpr::All(expressions) | PermissionExpr::Any(expressions) => {
520            for expression in expressions {
521                collect_permission_keys(expression, keys);
522            }
523        }
524    }
525}
526
527fn display_permission_path(path: &[Box<str>]) -> String {
528    path.iter()
529        .map(AsRef::as_ref)
530        .collect::<Vec<&str>>()
531        .join(".")
532}
533
534fn register_renamed_root<S>(
535    dispatcher: &mut CommandDispatcher<S, SteelCommandRuntime>,
536    root: &CommandNodeBuilder<S, SteelCommandRuntime>,
537    name: impl Into<Box<str>>,
538) -> Result<(), CommandRegistrationError>
539where
540    S: CommandPermissionSource,
541{
542    let renamed = root
543        .clone()
544        .with_literal_name(name)
545        .ok_or(CommandRegistrationError::UnexpectedArgumentRoot)?;
546    dispatcher.register(renamed)?;
547    Ok(())
548}
549
550fn validate_alias(alias: &str) -> Result<(), CommandRegistrationError> {
551    if alias.is_empty() {
552        return Err(CommandRegistrationError::EmptyAlias);
553    }
554    if alias.chars().any(char::is_whitespace) {
555        return Err(CommandRegistrationError::AliasContainsWhitespace(
556            alias.into(),
557        ));
558    }
559    if alias.contains(':') {
560        return Err(CommandRegistrationError::NamespacedAlias(alias.into()));
561    }
562    Ok(())
563}
564
565/// A command declaration or its resulting Brigadier graph was invalid.
566#[derive(Debug, Error)]
567pub(crate) enum CommandRegistrationError {
568    #[error("invalid command id '{0}'")]
569    InvalidCommandId(Identifier),
570    #[error("command id '{0}' is already registered")]
571    DuplicateCommandId(Identifier),
572    #[error("command '{id}' claims root '{root}' more than once")]
573    DuplicateOwnedRoot { id: Identifier, root: Box<str> },
574    #[error("command '{id}' must produce a literal root")]
575    RootMustBeLiteral { id: Identifier },
576    #[error("command '{id}' produced root '{root}' instead of its id path")]
577    RootDoesNotMatchId { id: Identifier, root: Box<str> },
578    #[error("command alias cannot be empty")]
579    EmptyAlias,
580    #[error("command alias '{0}' cannot contain whitespace")]
581    AliasContainsWhitespace(Box<str>),
582    #[error("command alias '{0}' cannot be namespaced")]
583    NamespacedAlias(Box<str>),
584    #[error("command '{id}' cannot derive a permission from its id: {source}")]
585    InvalidDerivedPermission {
586        id: Identifier,
587        #[source]
588        source: PermissionKeyError,
589    },
590    #[error("invalid internal permission declaration '{value}': {source}")]
591    InvalidInternalPermission {
592        value: String,
593        #[source]
594        source: PermissionKeyError,
595    },
596    #[error("command '{id}' has an invalid explicit permission: {source}")]
597    InvalidExplicitPermission {
598        id: Identifier,
599        #[source]
600        source: PermissionKeyError,
601    },
602    #[error("command '{id}' cannot combine explicit and derived subcommand permissions")]
603    SubcommandPermissionsRequireDerivedRoot { id: Identifier },
604    #[error("command '{id}' has an empty subcommand permission path")]
605    EmptySubcommandPermissionPath { id: Identifier },
606    #[error("command '{id}' has invalid subcommand permission path '{path}': {source}")]
607    InvalidSubcommandPermissionPath {
608        id: Identifier,
609        path: String,
610        #[source]
611        source: PermissionKeyError,
612    },
613    #[error("command '{id}' declares subcommand permission path '{path}' more than once")]
614    DuplicateSubcommandPermissionPath { id: Identifier, path: String },
615    #[error("command '{id}' has no literal at subcommand permission path '{path}'")]
616    MissingSubcommandPermissionPath { id: Identifier, path: String },
617    #[error("command '{id}' has {matches} literals at subcommand permission path '{path}'")]
618    AmbiguousSubcommandPermissionPath {
619        id: Identifier,
620        path: String,
621        matches: usize,
622    },
623    #[error("a validated command root unexpectedly became an argument")]
624    UnexpectedArgumentRoot,
625    #[error(transparent)]
626    InvalidGraph(#[from] RegistrationError),
627}
628
629#[cfg(test)]
630mod tests;