use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
pub struct Namespace(pub String);
impl Namespace {
pub fn new(raw: &str) -> Result<Self, String> {
let normalized = Self::normalize(raw);
if normalized.is_empty() {
return Err("namespace cannot be empty".to_string());
}
if !normalized.chars().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
{
return Err("namespace must be lowercase kebab-case".to_string());
}
if normalized.starts_with('-') || normalized.ends_with('-') || normalized.contains("--") {
return Err(
"namespace cannot start/end with '-' or contain consecutive '-'".to_string()
);
}
Ok(Self(normalized))
}
#[must_use]
pub fn normalize(raw: &str) -> String {
raw.trim()
.to_ascii_lowercase()
.replace(['_', ' ', '/'], "-")
.split('-')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("-")
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandPath {
pub segments: Vec<Namespace>,
}
impl CommandPath {
pub fn new(raw_segments: &[&str]) -> Result<Self, String> {
let mut segments = Vec::with_capacity(raw_segments.len());
for value in raw_segments {
segments.push(Namespace::new(value)?);
}
if segments.is_empty() {
return Err("command path requires at least one segment".to_string());
}
Ok(Self { segments })
}
pub fn parse(raw: &str) -> Result<Self, String> {
let input = raw.trim();
if input.is_empty() {
return Err("command path cannot be empty".to_string());
}
let segments: Vec<&str> = input
.split(|ch: char| ch.is_ascii_whitespace() || ch == '/')
.filter(|segment| !segment.is_empty())
.collect();
Self::new(&segments)
}
#[must_use]
pub fn to_command_string(&self) -> String {
self.segments.iter().map(Namespace::as_str).collect::<Vec<_>>().join(" ")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandMetadata {
pub path: CommandPath,
pub summary: String,
pub hidden: bool,
pub aliases: Vec<CommandPath>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct NamespaceMetadata {
pub name: Namespace,
pub reserved: bool,
pub owner: String,
}