tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

/// A stable project identity (`[projects.<project-id>]` in `tix.toml`).
///
/// Never blank, never containing whitespace or a TOML-structural character, and
/// **never reused**. It is the registry key and every issue association points at
/// it, so a rename changes the display label only and leaves every association and
/// derived figure intact (C-002, SC-005). The type exists so "this string is an
/// id" is checked once, at the boundary, instead of at every lookup — the same
/// problem [`crate::views::registry::ViewId`] solves for views. The rules
/// themselves are shared with [`EpicId`] through [`StableId`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ProjectId(pub(super) String);

impl StableId for ProjectId {
    const PREFIX: &'static str = "p-";
    const LABEL: &'static str = "project id";
    const ARTICLE: &'static str = "a";

    fn from_validated(raw: String) -> Self {
        Self(raw)
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

impl ProjectId {
    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;

    /// Validate an explicit id; see [`StableId::new`].
    pub fn new(raw: &str) -> Result<Self> {
        <Self as StableId>::new(raw)
    }

    /// Allocate a new id, `p-`-prefixed; see [`StableId::generate`] for the
    /// never-reused guarantee.
    #[must_use]
    pub fn generate() -> Self {
        <Self as StableId>::generate()
    }

    /// The id as written.
    #[must_use]
    pub fn as_str(&self) -> &str {
        <Self as StableId>::as_str(self)
    }
}

/// A stable epic identity (`[epics.<epic-id>]` in `tix.toml`).
///
/// The same rules as [`ProjectId`], shared through [`StableId`], with an `e-`
/// prefix: never blank, never padded, never carrying a TOML-structural character,
/// and **never reused**. It is the registry key and never a display name (C-003), so
/// a rename changes the label only and a deleted epic's id stays spent — a member
/// issue's dangling reference is then diagnosed and repaired, never silently
/// rebound to a new epic (C-005, NFR-002).
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EpicId(pub(super) String);

impl StableId for EpicId {
    const PREFIX: &'static str = "e-";
    const LABEL: &'static str = "epic id";
    const ARTICLE: &'static str = "an";

    fn from_validated(raw: String) -> Self {
        Self(raw)
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

impl EpicId {
    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;

    /// Validate an explicit epic id; see [`StableId::new`].
    pub fn new(raw: &str) -> Result<Self> {
        <Self as StableId>::new(raw)
    }

    /// Allocate a new epic id, `e-`-prefixed; see [`StableId::generate`] for the
    /// never-reused guarantee.
    #[must_use]
    pub fn generate() -> Self {
        <Self as StableId>::generate()
    }

    /// The id as written.
    #[must_use]
    pub fn as_str(&self) -> &str {
        <Self as StableId>::as_str(self)
    }
}

impl std::fmt::Display for ProjectId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl TryFrom<String> for ProjectId {
    type Error = TixError;

    fn try_from(raw: String) -> Result<Self> {
        Self::new(&raw)
    }
}

impl std::fmt::Display for EpicId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl TryFrom<String> for EpicId {
    type Error = TixError;

    fn try_from(raw: String) -> Result<Self> {
        Self::new(&raw)
    }
}

/// A stable initiative identity (`[initiatives.<initiative-id>]` in `tix.toml`).
///
/// The third [`StableId`], after [`ProjectId`] and [`EpicId`], with an `i-` prefix
/// and the same rules through the same trait: never blank, never padded, never
/// carrying a TOML-structural character, and **never reused**. It is the registry
/// key and an initiative's associations and derived values hang off it, so a rename
/// changes the display label only (C-002). A deleted initiative's id stays spent, so
/// nothing is silently rebound to a later record.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct InitiativeId(pub(super) String);

impl StableId for InitiativeId {
    const PREFIX: &'static str = "i-";
    const LABEL: &'static str = "initiative id";
    const ARTICLE: &'static str = "an";

    fn from_validated(raw: String) -> Self {
        Self(raw)
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

impl InitiativeId {
    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;

    /// Validate an explicit id; see [`StableId::new`].
    pub fn new(raw: &str) -> Result<Self> {
        <Self as StableId>::new(raw)
    }

    /// Allocate a new id, `i-`-prefixed; see [`StableId::generate`] for the
    /// never-reused guarantee.
    #[must_use]
    pub fn generate() -> Self {
        <Self as StableId>::generate()
    }

    /// The id as written.
    #[must_use]
    pub fn as_str(&self) -> &str {
        <Self as StableId>::as_str(self)
    }
}

impl std::fmt::Display for InitiativeId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl TryFrom<String> for InitiativeId {
    type Error = TixError;

    fn try_from(raw: String) -> Result<Self> {
        Self::new(&raw)
    }
}

/// Characters that cannot appear in a config table key: whitespace, and the
/// TOML/table syntax characters an id would otherwise be able to break out of.
pub(super) fn is_structural_key_char(c: char) -> bool {
    c.is_whitespace()
        || matches!(c, '.' | '"' | '\'' | '[' | ']' | '#' | '=' | '\\')
        || c.is_control()
}