//! The command registry and command signatures.
//!
//! A [`CommandSignature`] owns the pipeline-carrier contract and stable
//! documentation an internal command declares: accepted input carriers, its
//! output carrier, positional/option validation, invocation spelling, and prose. A
//! [`CommandRegistry`] owns a validated namespace manifest. Core entries own
//! signatures, aliases borrow the signature of one canonical core entry, and
//! reserved names own no executable signature. The registry remains empty by
//! default so focused callers can inject signatures without constructing the
//! standard manifest.
use std::collections::{BTreeMap, BTreeSet};
use crate::documentation::CommandDocumentation;
/// The language major whose namespace contract is being frozen for Flash v1.
pub const V1_LANGUAGE_MAJOR: u16 = 1;
/// One pipeline-edge carrier.
///
/// `Empty`, `ByteStream`, `Value`, and `ValueStream` are distinct payload states
/// that the planner never substitutes for one another.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Carrier {
/// No payload, distinct from `null` and from an empty stream.
Empty,
/// One ordered logical sequence of bytes in arbitrary chunks.
ByteStream,
/// Exactly one runtime value.
Value,
/// An ordered sequence of zero or more runtime values.
ValueStream,
}
/// How an internal command determines its output carrier.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CommandOutput {
/// The command always produces one fixed carrier.
Fixed(Carrier),
/// The command forwards whichever carrier it accepted unchanged.
SameAsInput,
}
impl CommandOutput {
/// Resolve this contract for an actual input carrier.
#[must_use]
pub const fn resolve(self, input: Carrier) -> Carrier {
match self {
Self::Fixed(output) => output,
Self::SameAsInput => input,
}
}
}
/// An internal command's signature: its name, pipeline contract, flags, and prose.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandSignature {
name: String,
inputs: BTreeSet<Carrier>,
output: CommandOutput,
arguments: CommandArgumentSchema,
documentation: CommandDocumentation,
}
/// The source/runtime form accepted by one positional slot.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandArgumentKind {
/// One native command word.
Word,
/// One typed closure value.
Closure,
/// Either a word or a typed closure value.
Any,
}
/// How static checking treats an argument spread whose final length is unknown.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandDynamicTail {
/// Defer the final arity and option answer to expanded planning.
DeferredToRuntime,
/// Reject a dynamic tail because the command requires a fixed source shape.
Rejected,
}
/// How a command treats the conventional `--` option terminator.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandOptionTerminator {
/// Remove the first `--` before execution and treat later words as positionals.
Accepted,
/// Treat `--` as an ordinary positional word.
Literal,
}
/// One declarative long-option contract.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandOptionSchema {
name: String,
value_arity: usize,
repeatable: bool,
conflicts: BTreeSet<String>,
}
impl CommandOptionSchema {
/// A non-repeatable flag that consumes no following value.
#[must_use]
pub fn flag(name: impl Into<String>) -> Self {
Self {
name: name.into(),
value_arity: 0,
repeatable: false,
conflicts: BTreeSet::new(),
}
}
/// Sets the exact number of following words consumed by this option.
#[must_use]
pub const fn with_value_arity(mut self, value_arity: usize) -> Self {
self.value_arity = value_arity;
self
}
/// Allows the option to occur more than once.
#[must_use]
pub const fn repeatable(mut self) -> Self {
self.repeatable = true;
self
}
/// Declares mutually exclusive option spellings.
#[must_use]
pub fn conflicts_with(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.conflicts.extend(names.into_iter().map(Into::into));
self
}
/// The exact option spelling.
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
/// The exact count of following value words.
#[must_use]
pub const fn value_arity(&self) -> usize {
self.value_arity
}
/// Whether the option may repeat.
#[must_use]
pub const fn is_repeatable(&self) -> bool {
self.repeatable
}
/// Mutually exclusive option spellings, in deterministic order.
pub fn conflicts(&self) -> impl Iterator<Item = &str> {
self.conflicts.iter().map(String::as_str)
}
}
/// Complete positional, option, terminator, and dynamic-tail metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandArgumentSchema {
minimum: usize,
maximum: Option<usize>,
positional_kinds: Vec<CommandArgumentKind>,
variadic_kind: CommandArgumentKind,
options: BTreeMap<String, CommandOptionSchema>,
options_before_positionals: bool,
terminator: CommandOptionTerminator,
dynamic_tail: CommandDynamicTail,
}
impl Default for CommandArgumentSchema {
fn default() -> Self {
Self {
minimum: 0,
maximum: None,
positional_kinds: Vec::new(),
variadic_kind: CommandArgumentKind::Any,
options: BTreeMap::new(),
options_before_positionals: false,
terminator: CommandOptionTerminator::Accepted,
dynamic_tail: CommandDynamicTail::DeferredToRuntime,
}
}
}
impl CommandArgumentSchema {
/// An exact positional range with word-valued slots by default.
#[must_use]
pub fn positional(minimum: usize, maximum: Option<usize>) -> Self {
Self {
minimum,
maximum,
positional_kinds: Vec::new(),
variadic_kind: CommandArgumentKind::Word,
..Self::default()
}
}
/// Assigns exact kinds to the leading positional slots.
#[must_use]
pub fn with_positional_kinds(
mut self,
kinds: impl IntoIterator<Item = CommandArgumentKind>,
) -> Self {
self.positional_kinds = kinds.into_iter().collect();
self
}
/// Assigns the kind used after the explicitly described leading slots.
#[must_use]
pub const fn with_variadic_kind(mut self, kind: CommandArgumentKind) -> Self {
self.variadic_kind = kind;
self
}
/// Adds declarative long-option schemas.
#[must_use]
pub fn with_options(mut self, options: impl IntoIterator<Item = CommandOptionSchema>) -> Self {
for option in options {
self.options.insert(option.name.clone(), option);
}
self
}
/// Requires options to precede every positional argument.
#[must_use]
pub const fn options_before_positionals(mut self) -> Self {
self.options_before_positionals = true;
self
}
/// Selects whether `--` is an option terminator or a literal positional.
#[must_use]
pub const fn with_terminator(mut self, terminator: CommandOptionTerminator) -> Self {
self.terminator = terminator;
self
}
/// Selects how an unresolved spread is handled by static checking.
#[must_use]
pub const fn with_dynamic_tail(mut self, dynamic_tail: CommandDynamicTail) -> Self {
self.dynamic_tail = dynamic_tail;
self
}
/// Minimum positional count after option parsing.
#[must_use]
pub const fn minimum(&self) -> usize {
self.minimum
}
/// Maximum positional count after option parsing.
#[must_use]
pub const fn maximum(&self) -> Option<usize> {
self.maximum
}
/// Expected form for one zero-based positional slot.
#[must_use]
pub fn positional_kind(&self, position: usize) -> CommandArgumentKind {
self.positional_kinds
.get(position)
.copied()
.unwrap_or(self.variadic_kind)
}
/// One exact option contract.
#[must_use]
pub fn option(&self, name: &str) -> Option<&CommandOptionSchema> {
self.options.get(name)
}
/// Options in deterministic spelling order.
pub fn options(&self) -> impl Iterator<Item = &CommandOptionSchema> {
self.options.values()
}
/// Whether options must occur before every positional.
#[must_use]
pub const fn require_options_before_positionals(&self) -> bool {
self.options_before_positionals
}
/// The command's `--` policy.
#[must_use]
pub const fn terminator(&self) -> CommandOptionTerminator {
self.terminator
}
/// The command's unresolved-spread policy.
#[must_use]
pub const fn dynamic_tail(&self) -> CommandDynamicTail {
self.dynamic_tail
}
/// Validate one source or expanded argument sequence against this schema.
///
/// Static callers represent interpolation-dependent words with `Word(None)`
/// and unresolved spreads with `DynamicTail`; only facts independent of
/// those values are reported. Expanded planning supplies exact word bytes.
#[must_use]
pub fn validate(&self, inputs: &[CommandArgumentInput]) -> Vec<CommandArgumentFault> {
let mut faults = Vec::new();
let mut options_active = true;
let mut positional_started = false;
let mut definite_positionals = 0usize;
let mut definite_positional_indices = Vec::new();
let mut possible_positionals = 0usize;
let mut unbounded_positionals = false;
let mut uncertain_option_classification = false;
let mut seen = BTreeMap::<String, usize>::new();
let mut index = 0usize;
while index < inputs.len() {
match &inputs[index] {
CommandArgumentInput::DynamicTail => {
if self.dynamic_tail == CommandDynamicTail::Rejected {
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::DynamicTail,
));
} else {
unbounded_positionals = true;
uncertain_option_classification = true;
}
index += 1;
}
CommandArgumentInput::Word(Some(bytes))
if options_active
&& self.terminator == CommandOptionTerminator::Accepted
&& bytes.as_slice() == b"--" =>
{
options_active = false;
index += 1;
}
CommandArgumentInput::Word(Some(bytes))
if options_active
&& bytes.starts_with(b"--")
&& !(self.terminator == CommandOptionTerminator::Literal
&& bytes.as_slice() == b"--") =>
{
let option_name = String::from_utf8_lossy(bytes).into_owned();
let Some(option) = self.options.get(&option_name) else {
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::UnknownOption {
option: option_name,
},
));
index += 1;
continue;
};
if self.options_before_positionals && positional_started {
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::OptionAfterPositional {
option: option_name.clone(),
},
));
}
if !option.repeatable && seen.contains_key(&option_name) {
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::RepeatedOption {
option: option_name.clone(),
},
));
}
if let Some(conflict) = option
.conflicts
.iter()
.find(|conflict| seen.contains_key(*conflict))
{
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::ConflictingOptions {
option: option_name.clone(),
conflict: conflict.clone(),
},
));
}
seen.insert(option_name.clone(), index);
let available = inputs.len().saturating_sub(index + 1);
if available < option.value_arity {
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::MissingOptionValues {
option: option_name,
expected: option.value_arity,
actual: available,
},
));
break;
}
index += option.value_arity + 1;
}
CommandArgumentInput::Word(None) if options_active => {
possible_positionals += 1;
uncertain_option_classification = true;
index += 1;
}
input => {
positional_started = true;
let position = definite_positionals;
definite_positionals += 1;
definite_positional_indices.push(index);
possible_positionals += 1;
let actual = match input {
CommandArgumentInput::Word(_) => CommandArgumentKind::Word,
CommandArgumentInput::Closure => CommandArgumentKind::Closure,
CommandArgumentInput::DynamicTail => unreachable!("handled above"),
};
let expected = self.positional_kind(position);
if !uncertain_option_classification
&& expected != CommandArgumentKind::Any
&& expected != actual
{
faults.push(CommandArgumentFault::new(
Some(index),
CommandArgumentFaultKind::UnexpectedKind {
position,
expected,
actual,
},
));
}
index += 1;
}
}
}
let too_few = !unbounded_positionals
&& !uncertain_option_classification
&& possible_positionals < self.minimum;
let too_many = self
.maximum
.is_some_and(|maximum| definite_positionals > maximum);
if too_few || too_many {
faults.push(CommandArgumentFault::new(
self.maximum
.filter(|_| too_many)
.and_then(|maximum| definite_positional_indices.get(maximum).copied()),
CommandArgumentFaultKind::Arity {
minimum: self.minimum,
maximum: self.maximum,
actual: definite_positionals,
},
));
}
faults
}
}
/// One argument shape presented to shared source/runtime validation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandArgumentInput {
/// A word with exact expanded bytes, or an interpolation-dependent static word.
Word(Option<Vec<u8>>),
/// A typed closure syntax item.
Closure,
/// A spread whose final number of words is unknown to static checking.
DynamicTail,
}
/// One shared command-schema validation failure.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandArgumentFault {
argument_index: Option<usize>,
kind: CommandArgumentFaultKind,
}
impl CommandArgumentFault {
fn new(argument_index: Option<usize>, kind: CommandArgumentFaultKind) -> Self {
Self {
argument_index,
kind,
}
}
/// Zero-based source/runtime argument index, when one argument owns the fault.
#[must_use]
pub const fn argument_index(&self) -> Option<usize> {
self.argument_index
}
/// The exact schema rule that failed.
#[must_use]
pub const fn kind(&self) -> &CommandArgumentFaultKind {
&self.kind
}
}
/// Stable classes of shared command-schema validation failure.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandArgumentFaultKind {
Arity {
minimum: usize,
maximum: Option<usize>,
actual: usize,
},
UnknownOption {
option: String,
},
MissingOptionValues {
option: String,
expected: usize,
actual: usize,
},
RepeatedOption {
option: String,
},
ConflictingOptions {
option: String,
conflict: String,
},
OptionAfterPositional {
option: String,
},
UnexpectedKind {
position: usize,
expected: CommandArgumentKind,
actual: CommandArgumentKind,
},
DynamicTail,
}
/// Compatibility metadata shared by invocable namespace entries.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandLifecycle {
introduced_major: u16,
deprecated_since: Option<String>,
replacement: Option<String>,
}
impl CommandLifecycle {
/// Metadata for an entry introduced in `language_major`.
#[must_use]
pub const fn introduced(language_major: u16) -> Self {
Self {
introduced_major: language_major,
deprecated_since: None,
replacement: None,
}
}
/// Marks the entry deprecated since one product release.
#[must_use]
pub fn deprecated_since(mut self, release: impl Into<String>) -> Self {
self.deprecated_since = Some(release.into());
self
}
/// Adds a canonical replacement spelling.
#[must_use]
pub fn with_replacement(mut self, replacement: impl Into<String>) -> Self {
self.replacement = Some(replacement.into());
self
}
/// The language major in which the entry first existed or was reserved.
#[must_use]
pub const fn introduced_major(&self) -> u16 {
self.introduced_major
}
/// The product release that first deprecated the entry, if any.
#[must_use]
pub fn deprecated_since_release(&self) -> Option<&str> {
self.deprecated_since.as_deref()
}
/// The canonical replacement spelling, if one is published.
#[must_use]
pub fn replacement(&self) -> Option<&str> {
self.replacement.as_deref()
}
}
/// One declarative command-namespace manifest entry.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandNamespaceEntry {
/// One canonical executable built-in.
Core {
/// The canonical signature and documentation.
signature: CommandSignature,
/// Compatibility lifecycle metadata.
lifecycle: CommandLifecycle,
},
/// One executable migration spelling targeting a canonical core entry.
Alias {
/// The source spelling.
name: String,
/// The canonical core name.
target: String,
/// Compatibility lifecycle metadata.
lifecycle: CommandLifecycle,
},
/// One unavailable spelling protected from implicit external fallback.
Reserved {
/// The protected source spelling.
name: String,
/// The language major in which the reservation first existed.
introduced_major: u16,
/// Stable user-facing reason for the reservation.
purpose: String,
/// Optional canonical migration target.
replacement: Option<String>,
},
}
impl CommandNamespaceEntry {
/// A canonical core entry.
#[must_use]
pub const fn core(signature: CommandSignature, lifecycle: CommandLifecycle) -> Self {
Self::Core {
signature,
lifecycle,
}
}
/// An alias that reuses one canonical core entry.
#[must_use]
pub fn alias(
name: impl Into<String>,
target: impl Into<String>,
lifecycle: CommandLifecycle,
) -> Self {
Self::Alias {
name: name.into(),
target: target.into(),
lifecycle,
}
}
/// A name protected from implicit external fallback.
#[must_use]
pub fn reserved(
name: impl Into<String>,
introduced_major: u16,
purpose: impl Into<String>,
replacement: Option<&str>,
) -> Self {
Self::Reserved {
name: name.into(),
introduced_major,
purpose: purpose.into(),
replacement: replacement.map(str::to_owned),
}
}
fn name(&self) -> &str {
match self {
Self::Core { signature, .. } => signature.name(),
Self::Alias { name, .. } | Self::Reserved { name, .. } => name,
}
}
}
/// The stable class of one namespace spelling.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NamespaceClass {
/// A canonical executable built-in.
Core,
/// An executable migration spelling.
Alias,
/// A spelling protected from external fallback.
Reserved,
}
/// The classification result for one command spelling.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CommandClassification<'a> {
/// The spelling has no namespace entry.
Unknown,
/// A canonical executable built-in.
Core {
/// The canonical signature.
signature: &'a CommandSignature,
/// Compatibility lifecycle metadata.
lifecycle: &'a CommandLifecycle,
},
/// An executable spelling that reuses one canonical core entry.
Alias {
/// The canonical executor and signature name.
canonical_name: &'a str,
/// The canonical signature.
signature: &'a CommandSignature,
/// Compatibility lifecycle metadata for the alias spelling.
lifecycle: &'a CommandLifecycle,
},
/// A spelling protected from implicit external fallback.
Reserved {
/// Stable user-facing reason for the reservation.
purpose: &'a str,
/// Optional canonical migration target.
replacement: Option<&'a str>,
/// The language major in which the reservation first existed.
introduced_major: u16,
},
}
/// A borrowed entry from deterministic namespace iteration.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NamespaceEntryRef<'a> {
name: &'a str,
class: NamespaceClass,
}
impl<'a> NamespaceEntryRef<'a> {
/// The source spelling.
#[must_use]
pub const fn name(self) -> &'a str {
self.name
}
/// The entry's stable namespace class.
#[must_use]
pub const fn class(self) -> NamespaceClass {
self.class
}
}
/// A manifest validation failure.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandRegistryError {
/// The registry language major must be positive.
InvalidLanguageMajor { language_major: u16 },
/// Namespace spellings cannot be empty.
EmptyName,
/// One spelling occurred in more than one manifest entry.
DuplicateName { name: String },
/// An entry claims introduction outside the registry's language-major range.
InvalidIntroducedMajor {
name: String,
introduced_major: u16,
language_major: u16,
},
/// A deprecation release spelling was empty.
EmptyDeprecation { name: String },
/// Replacement guidance requires an explicit deprecation.
ReplacementWithoutDeprecation { name: String },
/// A reservation must explain why the spelling is unavailable.
EmptyReservationPurpose { name: String },
/// An alias targeted itself.
SelfAlias { name: String },
/// An alias target had no manifest entry.
MissingAliasTarget { name: String, target: String },
/// An alias target was another alias or a reservation rather than a core.
AliasTargetNotCore { name: String, target: String },
/// Replacement guidance did not name a different canonical core entry.
InvalidReplacementTarget { name: String, replacement: String },
}
impl std::fmt::Display for CommandRegistryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "invalid command namespace manifest: {self:?}")
}
}
impl std::error::Error for CommandRegistryError {}
#[derive(Clone, Debug, Eq, PartialEq)]
enum StoredNamespaceEntry {
Core {
signature: CommandSignature,
lifecycle: CommandLifecycle,
},
Alias {
target: String,
lifecycle: CommandLifecycle,
},
Reserved {
introduced_major: u16,
purpose: String,
replacement: Option<String>,
},
}
impl CommandSignature {
/// Builds a signature from a name, the input carriers it accepts, and the
/// carrier it produces. Duplicate input carriers collapse to one.
pub fn new(
name: impl Into<String>,
inputs: impl IntoIterator<Item = Carrier>,
output: Carrier,
) -> Self {
Self {
name: name.into(),
inputs: inputs.into_iter().collect(),
output: CommandOutput::Fixed(output),
arguments: CommandArgumentSchema::default(),
documentation: CommandDocumentation::default(),
}
}
/// Builds a signature that forwards its accepted input carrier unchanged.
pub fn passthrough(name: impl Into<String>, inputs: impl IntoIterator<Item = Carrier>) -> Self {
Self {
name: name.into(),
inputs: inputs.into_iter().collect(),
output: CommandOutput::SameAsInput,
arguments: CommandArgumentSchema::default(),
documentation: CommandDocumentation::default(),
}
}
/// Adds the flags this command accepts. Duplicate spellings collapse to one.
#[must_use]
pub fn with_flags(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.arguments = self
.arguments
.with_options(flags.into_iter().map(CommandOptionSchema::flag));
self
}
/// Attaches the canonical positional and option schema.
#[must_use]
pub fn with_arguments(mut self, arguments: CommandArgumentSchema) -> Self {
self.arguments = arguments;
self
}
/// Attaches the command's stable invocation spelling and prose.
#[must_use]
pub fn with_documentation(mut self, documentation: CommandDocumentation) -> Self {
self.documentation = documentation;
self
}
/// The command name.
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
/// Whether the command accepts `carrier` as input.
#[must_use]
pub fn accepts(&self, carrier: Carrier) -> bool {
self.inputs.contains(&carrier)
}
/// The accepted input carriers, in a deterministic order.
pub fn inputs(&self) -> impl Iterator<Item = Carrier> + '_ {
self.inputs.iter().copied()
}
/// The carrier the command produces.
#[must_use]
pub fn output(&self) -> CommandOutput {
self.output
}
/// The advertised flags, in sorted order.
pub fn flags(&self) -> impl Iterator<Item = &str> {
self.arguments.options().map(CommandOptionSchema::name)
}
/// The canonical positional and option schema.
#[must_use]
pub const fn arguments(&self) -> &CommandArgumentSchema {
&self.arguments
}
/// Required user-facing invocation and prose metadata.
#[must_use]
pub const fn documentation(&self) -> &CommandDocumentation {
&self.documentation
}
}
/// A validated command namespace. Empty at language major one by default.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommandRegistry {
language_major: u16,
entries: BTreeMap<String, StoredNamespaceEntry>,
}
impl Default for CommandRegistry {
fn default() -> Self {
Self {
language_major: V1_LANGUAGE_MAJOR,
entries: BTreeMap::new(),
}
}
}
impl CommandRegistry {
/// An empty registry.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Builds and validates one complete namespace manifest.
pub fn try_from_entries(
language_major: u16,
entries: impl IntoIterator<Item = CommandNamespaceEntry>,
) -> Result<Self, CommandRegistryError> {
if language_major == 0 {
return Err(CommandRegistryError::InvalidLanguageMajor { language_major });
}
let mut registry = Self {
language_major,
entries: BTreeMap::new(),
};
for entry in entries {
let name = entry.name().to_owned();
if name.is_empty() {
return Err(CommandRegistryError::EmptyName);
}
if registry.entries.contains_key(&name) {
return Err(CommandRegistryError::DuplicateName { name });
}
let stored = match entry {
CommandNamespaceEntry::Core {
signature,
lifecycle,
} => {
validate_lifecycle(&name, &lifecycle, language_major)?;
StoredNamespaceEntry::Core {
signature,
lifecycle,
}
}
CommandNamespaceEntry::Alias {
target, lifecycle, ..
} => {
validate_lifecycle(&name, &lifecycle, language_major)?;
StoredNamespaceEntry::Alias { target, lifecycle }
}
CommandNamespaceEntry::Reserved {
introduced_major,
purpose,
replacement,
..
} => {
validate_introduced_major(&name, introduced_major, language_major)?;
if purpose.is_empty() {
return Err(CommandRegistryError::EmptyReservationPurpose { name });
}
StoredNamespaceEntry::Reserved {
introduced_major,
purpose,
replacement,
}
}
};
registry.entries.insert(name, stored);
}
registry.validate_targets()?;
Ok(registry)
}
/// Registers `signature` under its name. Returns `true` if it was inserted, or
/// `false` if a command of that name already exists, in which case the earlier
/// signature is kept unchanged.
pub fn register(&mut self, signature: CommandSignature) -> bool {
if signature.name().is_empty() || self.entries.contains_key(signature.name()) {
return false;
}
self.entries.insert(
signature.name.clone(),
StoredNamespaceEntry::Core {
signature,
lifecycle: CommandLifecycle::introduced(self.language_major),
},
);
true
}
/// The signature registered under `name`, if any.
#[must_use]
pub fn lookup(&self, name: &str) -> Option<&CommandSignature> {
match self.entries.get(name)? {
StoredNamespaceEntry::Core { signature, .. } => Some(signature),
StoredNamespaceEntry::Alias { target, .. } => match self.entries.get(target) {
Some(StoredNamespaceEntry::Core { signature, .. }) => Some(signature),
_ => unreachable!("validated aliases target core entries"),
},
StoredNamespaceEntry::Reserved { .. } => None,
}
}
/// Whether a command of `name` is registered.
#[must_use]
pub fn contains(&self, name: &str) -> bool {
matches!(
self.entries.get(name),
Some(StoredNamespaceEntry::Core { .. } | StoredNamespaceEntry::Alias { .. })
)
}
/// The number of registered commands.
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
/// Whether the registry has no commands.
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// The registered command names, in sorted order.
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries.keys().map(String::as_str)
}
/// Registered signatures in name order.
pub fn signatures(&self) -> impl Iterator<Item = &CommandSignature> {
self.entries.values().filter_map(|entry| match entry {
StoredNamespaceEntry::Core { signature, .. } => Some(signature),
StoredNamespaceEntry::Alias { .. } | StoredNamespaceEntry::Reserved { .. } => None,
})
}
/// The language major whose namespace policy this registry validates.
#[must_use]
pub const fn language_major(&self) -> u16 {
self.language_major
}
/// Classify one source spelling through the shared namespace seam.
#[must_use]
pub fn classify(&self, name: &str) -> CommandClassification<'_> {
match self.entries.get(name) {
None => CommandClassification::Unknown,
Some(StoredNamespaceEntry::Core {
signature,
lifecycle,
}) => CommandClassification::Core {
signature,
lifecycle,
},
Some(StoredNamespaceEntry::Alias { target, lifecycle }) => {
let Some(StoredNamespaceEntry::Core { signature, .. }) = self.entries.get(target)
else {
unreachable!("validated aliases target core entries");
};
CommandClassification::Alias {
canonical_name: target,
signature,
lifecycle,
}
}
Some(StoredNamespaceEntry::Reserved {
introduced_major,
purpose,
replacement,
}) => CommandClassification::Reserved {
purpose,
replacement: replacement.as_deref(),
introduced_major: *introduced_major,
},
}
}
/// Every namespace entry in spelling order.
pub fn namespace_entries(&self) -> impl Iterator<Item = NamespaceEntryRef<'_>> {
self.entries.iter().map(|(name, entry)| NamespaceEntryRef {
name,
class: match entry {
StoredNamespaceEntry::Core { .. } => NamespaceClass::Core,
StoredNamespaceEntry::Alias { .. } => NamespaceClass::Alias,
StoredNamespaceEntry::Reserved { .. } => NamespaceClass::Reserved,
},
})
}
/// Canonical core names in spelling order.
pub fn core_names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().filter_map(|(name, entry)| {
matches!(entry, StoredNamespaceEntry::Core { .. }).then_some(name.as_str())
})
}
/// Alias names in spelling order.
pub fn alias_names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().filter_map(|(name, entry)| {
matches!(entry, StoredNamespaceEntry::Alias { .. }).then_some(name.as_str())
})
}
/// Reserved names in spelling order.
pub fn reserved_names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().filter_map(|(name, entry)| {
matches!(entry, StoredNamespaceEntry::Reserved { .. }).then_some(name.as_str())
})
}
fn validate_targets(&self) -> Result<(), CommandRegistryError> {
for (name, entry) in &self.entries {
match entry {
StoredNamespaceEntry::Core { lifecycle, .. } => {
self.validate_replacement(name, lifecycle.replacement())?;
}
StoredNamespaceEntry::Alias { target, lifecycle } => {
if target == name {
return Err(CommandRegistryError::SelfAlias { name: name.clone() });
}
match self.entries.get(target) {
None => {
return Err(CommandRegistryError::MissingAliasTarget {
name: name.clone(),
target: target.clone(),
});
}
Some(StoredNamespaceEntry::Core { .. }) => {}
Some(
StoredNamespaceEntry::Alias { .. }
| StoredNamespaceEntry::Reserved { .. },
) => {
return Err(CommandRegistryError::AliasTargetNotCore {
name: name.clone(),
target: target.clone(),
});
}
}
self.validate_replacement(name, lifecycle.replacement())?;
}
StoredNamespaceEntry::Reserved { replacement, .. } => {
self.validate_replacement(name, replacement.as_deref())?;
}
}
}
Ok(())
}
fn validate_replacement(
&self,
name: &str,
replacement: Option<&str>,
) -> Result<(), CommandRegistryError> {
let Some(replacement) = replacement else {
return Ok(());
};
if replacement == name
|| !matches!(
self.entries.get(replacement),
Some(StoredNamespaceEntry::Core { .. })
)
{
return Err(CommandRegistryError::InvalidReplacementTarget {
name: name.to_owned(),
replacement: replacement.to_owned(),
});
}
Ok(())
}
}
fn validate_lifecycle(
name: &str,
lifecycle: &CommandLifecycle,
language_major: u16,
) -> Result<(), CommandRegistryError> {
validate_introduced_major(name, lifecycle.introduced_major, language_major)?;
if lifecycle
.deprecated_since
.as_ref()
.is_some_and(String::is_empty)
{
return Err(CommandRegistryError::EmptyDeprecation {
name: name.to_owned(),
});
}
if lifecycle.replacement.is_some() && lifecycle.deprecated_since.is_none() {
return Err(CommandRegistryError::ReplacementWithoutDeprecation {
name: name.to_owned(),
});
}
Ok(())
}
fn validate_introduced_major(
name: &str,
introduced_major: u16,
language_major: u16,
) -> Result<(), CommandRegistryError> {
if introduced_major == 0 || introduced_major > language_major {
return Err(CommandRegistryError::InvalidIntroducedMajor {
name: name.to_owned(),
introduced_major,
language_major,
});
}
Ok(())
}