Skip to main content

tix/config/
ids.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4/// A stable project identity (`[projects.<project-id>]` in `tix.toml`).
5///
6/// Never blank, never containing whitespace or a TOML-structural character, and
7/// **never reused**. It is the registry key and every issue association points at
8/// it, so a rename changes the display label only and leaves every association and
9/// derived figure intact (C-002, SC-005). The type exists so "this string is an
10/// id" is checked once, at the boundary, instead of at every lookup — the same
11/// problem [`crate::views::registry::ViewId`] solves for views. The rules
12/// themselves are shared with [`EpicId`] through [`StableId`].
13#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(try_from = "String", into = "String")]
15pub struct ProjectId(pub(super) String);
16
17impl StableId for ProjectId {
18    const PREFIX: &'static str = "p-";
19    const LABEL: &'static str = "project id";
20    const ARTICLE: &'static str = "a";
21
22    fn from_validated(raw: String) -> Self {
23        Self(raw)
24    }
25
26    fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31impl ProjectId {
32    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
33    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;
34
35    /// Validate an explicit id; see [`StableId::new`].
36    pub fn new(raw: &str) -> Result<Self> {
37        <Self as StableId>::new(raw)
38    }
39
40    /// Allocate a new id, `p-`-prefixed; see [`StableId::generate`] for the
41    /// never-reused guarantee.
42    #[must_use]
43    pub fn generate() -> Self {
44        <Self as StableId>::generate()
45    }
46
47    /// The id as written.
48    #[must_use]
49    pub fn as_str(&self) -> &str {
50        <Self as StableId>::as_str(self)
51    }
52}
53
54/// A stable epic identity (`[epics.<epic-id>]` in `tix.toml`).
55///
56/// The same rules as [`ProjectId`], shared through [`StableId`], with an `e-`
57/// prefix: never blank, never padded, never carrying a TOML-structural character,
58/// and **never reused**. It is the registry key and never a display name (C-003), so
59/// a rename changes the label only and a deleted epic's id stays spent — a member
60/// issue's dangling reference is then diagnosed and repaired, never silently
61/// rebound to a new epic (C-005, NFR-002).
62#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
63#[serde(try_from = "String", into = "String")]
64pub struct EpicId(pub(super) String);
65
66impl StableId for EpicId {
67    const PREFIX: &'static str = "e-";
68    const LABEL: &'static str = "epic id";
69    const ARTICLE: &'static str = "an";
70
71    fn from_validated(raw: String) -> Self {
72        Self(raw)
73    }
74
75    fn as_str(&self) -> &str {
76        &self.0
77    }
78}
79
80impl EpicId {
81    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
82    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;
83
84    /// Validate an explicit epic id; see [`StableId::new`].
85    pub fn new(raw: &str) -> Result<Self> {
86        <Self as StableId>::new(raw)
87    }
88
89    /// Allocate a new epic id, `e-`-prefixed; see [`StableId::generate`] for the
90    /// never-reused guarantee.
91    #[must_use]
92    pub fn generate() -> Self {
93        <Self as StableId>::generate()
94    }
95
96    /// The id as written.
97    #[must_use]
98    pub fn as_str(&self) -> &str {
99        <Self as StableId>::as_str(self)
100    }
101}
102
103impl std::fmt::Display for ProjectId {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.write_str(&self.0)
106    }
107}
108
109impl TryFrom<String> for ProjectId {
110    type Error = TixError;
111
112    fn try_from(raw: String) -> Result<Self> {
113        Self::new(&raw)
114    }
115}
116
117impl std::fmt::Display for EpicId {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.write_str(&self.0)
120    }
121}
122
123impl TryFrom<String> for EpicId {
124    type Error = TixError;
125
126    fn try_from(raw: String) -> Result<Self> {
127        Self::new(&raw)
128    }
129}
130
131/// A stable initiative identity (`[initiatives.<initiative-id>]` in `tix.toml`).
132///
133/// The third [`StableId`], after [`ProjectId`] and [`EpicId`], with an `i-` prefix
134/// and the same rules through the same trait: never blank, never padded, never
135/// carrying a TOML-structural character, and **never reused**. It is the registry
136/// key and an initiative's associations and derived values hang off it, so a rename
137/// changes the display label only (C-002). A deleted initiative's id stays spent, so
138/// nothing is silently rebound to a later record.
139#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
140#[serde(try_from = "String", into = "String")]
141pub struct InitiativeId(pub(super) String);
142
143impl StableId for InitiativeId {
144    const PREFIX: &'static str = "i-";
145    const LABEL: &'static str = "initiative id";
146    const ARTICLE: &'static str = "an";
147
148    fn from_validated(raw: String) -> Self {
149        Self(raw)
150    }
151
152    fn as_str(&self) -> &str {
153        &self.0
154    }
155}
156
157impl InitiativeId {
158    /// The longest id the registry accepts (see [`StableId::MAX_LEN`]).
159    pub const MAX_LEN: usize = <Self as StableId>::MAX_LEN;
160
161    /// Validate an explicit id; see [`StableId::new`].
162    pub fn new(raw: &str) -> Result<Self> {
163        <Self as StableId>::new(raw)
164    }
165
166    /// Allocate a new id, `i-`-prefixed; see [`StableId::generate`] for the
167    /// never-reused guarantee.
168    #[must_use]
169    pub fn generate() -> Self {
170        <Self as StableId>::generate()
171    }
172
173    /// The id as written.
174    #[must_use]
175    pub fn as_str(&self) -> &str {
176        <Self as StableId>::as_str(self)
177    }
178}
179
180impl std::fmt::Display for InitiativeId {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.write_str(&self.0)
183    }
184}
185
186impl TryFrom<String> for InitiativeId {
187    type Error = TixError;
188
189    fn try_from(raw: String) -> Result<Self> {
190        Self::new(&raw)
191    }
192}
193
194/// Characters that cannot appear in a config table key: whitespace, and the
195/// TOML/table syntax characters an id would otherwise be able to break out of.
196pub(super) fn is_structural_key_char(c: char) -> bool {
197    c.is_whitespace()
198        || matches!(c, '.' | '"' | '\'' | '[' | ']' | '#' | '=' | '\\')
199        || c.is_control()
200}