slash-lang 0.1.0

Parser and AST for the slash-command language
Documentation
/// The root of a parsed slash-command program.
///
/// A program is a sequence of [`Pipeline`]s connected by `&&` / `||` operators.
/// The operator stored on each pipeline indicates how it connects to the *next* one.
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
    pub pipelines: Vec<Pipeline>,
}

/// A sequence of [`Command`]s connected by `|` / `|&` operators within one logical unit.
///
/// `operator` indicates how this pipeline connects to the next one in the [`Program`]:
/// - `Some(Op::And)` — next pipeline runs only if this one succeeds (`&&`)
/// - `Some(Op::Or)` — next pipeline runs only if this one fails (`||`)
/// - `None` — this is the last pipeline
#[derive(Debug, Clone, PartialEq)]
pub struct Pipeline {
    pub commands: Vec<Command>,
    pub operator: Option<Op>,
}

/// An operator connecting commands or pipelines.
#[derive(Debug, Clone, PartialEq)]
pub enum Op {
    /// `&&` — run next pipeline only on success
    And,
    /// `||` — run next pipeline only on failure
    Or,
    /// `|` — pipe stdout to the next command
    Pipe,
    /// `|&` — pipe both stdout and stderr to the next command
    PipeErr,
}

/// A single builder-chain argument on a command.
///
/// Parsed from dotted segments after the command name:
/// `/cmd.flag` → `Arg { name: "flag", value: None }`
/// `/cmd.key(val)` → `Arg { name: "key", value: Some("val") }`
#[derive(Debug, Clone, PartialEq)]
pub struct Arg {
    pub name: String,
    pub value: Option<String>,
}

/// A single parsed slash command.
///
/// # Priority
///
/// Inferred from the **shape** of the raw token before normalization:
///
/// | Token shape      | Priority |
/// |------------------|----------|
/// | `ALL_CAPS`       | Max      |
/// | `TitleCase`      | High     |
/// | `camelCase`      | Medium   |
/// | `kebab-case`     | Low      |
/// | `snake_case`     | Lowest   |
///
/// # Suffixes (applied outer → inner)
///
/// ```text
/// /TitleCase+++???!  →  invalid (double ? is an error)
/// /Deploy?!          →  name="deploy", optional, urgency=Low
/// /ALERT!!!          →  urgency=High
/// /verbose+++        →  verbosity=+3
/// /quiet--           →  verbosity=-2
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
    /// The original token as it appeared in the input (e.g. `/Build.flag(v)!`).
    pub raw: String,
    /// Normalized lowercase name (e.g. `build`).
    pub name: String,
    /// Primary argument from `/cmd(value)` syntax.
    pub primary: Option<String>,
    /// Builder-chain arguments (e.g. `.flag(val).other`).
    pub args: Vec<Arg>,
    /// Priority inferred from the raw token's casing/separators.
    pub priority: Priority,
    /// Urgency from trailing `!` / `!!` / `!!!`.
    pub urgency: Urgency,
    /// Verbosity level from trailing `+` (positive) or `-` (negative) markers.
    pub verbosity: i8,
    /// Whether the command is optional (`?` suffix) — its return value may be absent.
    pub optional: bool,
    /// Numeric test identifier for `/test`-family commands (e.g. `/test3` → `Some(3)`).
    pub test_id: Option<u32>,
    /// Output redirection, if present. Closes the pipeline — no further `|` is allowed.
    pub redirect: Option<Redirection>,
    /// Pipe operator connecting this command to the next one within the same pipeline.
    pub pipe: Option<Op>,
}

/// Priority level inferred from the command token's shape.
#[derive(Debug, Clone, PartialEq)]
pub enum Priority {
    /// `ALL_CAPS` token
    Max,
    /// `TitleCase` token
    High,
    /// `camelCase` token
    Medium,
    /// `kebab-case` or plain lowercase token
    Low,
    /// `snake_case` token
    Lowest,
}

/// Urgency level from trailing `!` markers.
#[derive(Debug, Clone, PartialEq)]
pub enum Urgency {
    None,
    /// `!`
    Low,
    /// `!!`
    Medium,
    /// `!!!`
    High,
}

/// Output redirection target.
#[derive(Debug, Clone, PartialEq)]
pub enum Redirection {
    /// `> file` — overwrite
    Truncate(String),
    /// `>> file` — append
    Append(String),
}

impl Command {
    pub fn new(name: String, priority: Priority) -> Self {
        Self {
            raw: String::new(),
            name,
            primary: None,
            args: vec![],
            priority,
            urgency: Urgency::None,
            verbosity: 0,
            optional: false,
            test_id: None,
            redirect: None,
            pipe: None,
        }
    }
}