ppoppo-sigil-parser 0.1.0

Parser for the ppoppo `@@/` chat sigil grammar — entity references, verbs, scopes, modifiers
Documentation
use core::fmt;

/// A parsed sigil command.
///
/// ```text
/// @@/<verb> <target> [args...]
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SigilCommand {
    pub verb: Verb,
    pub target: Option<SigilRef>,
    pub args: Vec<String>,
}

/// Phase A + B verbs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verb {
    // Phase A — read-only
    Ls,
    Cat,
    Find,
    Grep,
    Man,
    // Phase A+ — save to MyInfo
    Save,
    // Phase B — write
    Set,
    Rm,
    Lbl,
}

impl Verb {
    /// Whether the verb requires a sigil reference as target.
    /// `man` and `save` do not — `man` takes an optional verb name,
    /// `save` accepts either a positional ref OR literal text args.
    pub fn requires_target(self) -> bool {
        !matches!(self, Self::Man | Self::Save)
    }
}

impl fmt::Display for Verb {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Ls => "ls",
            Self::Cat => "cat",
            Self::Find => "find",
            Self::Grep => "grep",
            Self::Man => "man",
            Self::Save => "save",
            Self::Set => "set",
            Self::Rm => "rm",
            Self::Lbl => "lbl",
        };
        f.write_str(s)
    }
}

/// The 4 reference systems for sigil targets.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SigilRef {
    /// Scope sigils: `@~`, `@*`, `@name`, `#~`, `#*`, `#name`
    Scope(ScopeRef),
    /// Positional reference: `^` (depth=1), `^^` (depth=2), etc.
    Positional(std::num::NonZeroU8),
    /// ID reference: bare ULID (26-char alphanumeric, auto-detected)
    Id(String),
    /// Named reference: `:label`, `:*`
    Named(NamedRef),
}

impl SigilRef {
    pub fn is_collection(&self) -> bool {
        matches!(
            self,
            Self::Scope(ScopeRef { modifier: Modifier::All, .. }) | Self::Named(NamedRef::All)
        )
    }

    pub fn is_instance(&self) -> bool {
        !self.is_collection()
    }
}

impl fmt::Display for SigilRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Scope(s) => write!(f, "{s}"),
            Self::Positional(d) => {
                for _ in 0..d.get() {
                    f.write_str("^")?;
                }
                Ok(())
            }
            Self::Id(id) => f.write_str(id),
            Self::Named(n) => write!(f, "{n}"),
        }
    }
}

/// Scope sigil: `<prefix><modifier>`
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScopeRef {
    pub scope: Scope,
    pub modifier: Modifier,
}

impl fmt::Display for ScopeRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let prefix = match self.scope {
            Scope::User => '@',
            Scope::Channel => '#',
        };
        match &self.modifier {
            Modifier::Current => write!(f, "{prefix}~"),
            Modifier::All => write!(f, "{prefix}*"),
            Modifier::Name(n) => write!(f, "{prefix}{n}"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
    User,
    Channel,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Modifier {
    /// `~` — self (user scope) or here (channel scope)
    Current,
    /// `*` — all in scope
    All,
    /// Named entity
    Name(String),
}

/// Named reference: `:label` or `:*`
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NamedRef {
    /// `:label` — a specific labeled message
    Label(String),
    /// `:*` — all labels (collection)
    All,
}

impl fmt::Display for NamedRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Label(l) => write!(f, ":{l}"),
            Self::All => f.write_str(":*"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
    /// Input does not start with `@@/`.
    NotSigil,
    /// No verb found after `@@/`.
    MissingVerb,
    /// Verb is not recognized.
    UnknownVerb(String),
    /// Verb requires a sigil target but none was provided.
    MissingTarget { verb: Verb },
    /// Target token is syntactically invalid.
    InvalidTarget { got: String },
    /// A deprecated v1 sigil was used.
    DeprecatedSigil(String),
    /// `find` was called without a search term.
    MissingSearchTerm { verb: Verb },
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotSigil => write!(f, "Not a sigil command (must start with @@/)"),
            Self::MissingVerb => write!(f, "Missing verb after @@/"),
            Self::UnknownVerb(v) => write!(f, "Unknown verb '{v}'"),
            Self::MissingTarget { verb } => {
                write!(f, "'{verb}' requires a target")
            }
            Self::InvalidTarget { got } => {
                write!(f, "Invalid target '{got}'")
            }
            Self::DeprecatedSigil(msg) => write!(f, "{msg}"),
            Self::MissingSearchTerm { verb } => {
                write!(f, "'{verb}' requires a search term. Did you mean 'ls'?")
            }
        }
    }
}