Skip to main content

wyvern/extensions/
ids.rs

1//! Validated identifier newtypes for the extension registry.
2
3/// Validated, non-empty extension identifier.
4///
5/// Constructed via `serde` `try_from` — guaranteed non-empty after trim.
6#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
7pub struct ExtensionId(String);
8
9impl ExtensionId {
10    /// Returns the id as a string slice.
11    #[must_use]
12    pub fn as_str(&self) -> &str {
13        &self.0
14    }
15}
16
17/// Failure from [`ExtensionId::try_from`].
18///
19/// A string newtype because this conversion is used exclusively via
20/// `serde::Deserialize`, where `de::Error::custom` wraps the message.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ExtensionIdError(String);
23
24impl std::fmt::Display for ExtensionIdError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        self.0.fmt(f)
27    }
28}
29
30impl std::error::Error for ExtensionIdError {}
31
32impl TryFrom<String> for ExtensionId {
33    type Error = ExtensionIdError;
34    fn try_from(s: String) -> Result<Self, Self::Error> {
35        let trimmed = s.trim();
36        if trimmed.is_empty() {
37            return Err(ExtensionIdError(
38                "extension id must not be empty or whitespace".into(),
39            ));
40        }
41        Ok(Self(trimmed.to_owned()))
42    }
43}
44
45impl std::fmt::Display for ExtensionId {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        self.0.fmt(f)
48    }
49}
50
51impl AsRef<str> for ExtensionId {
52    fn as_ref(&self) -> &str {
53        &self.0
54    }
55}
56
57impl PartialEq<str> for ExtensionId {
58    fn eq(&self, other: &str) -> bool {
59        self.0 == other
60    }
61}
62
63impl PartialEq<&str> for ExtensionId {
64    fn eq(&self, other: &&str) -> bool {
65        self.0 == *other
66    }
67}
68
69/// Declared `{arg:name}` flag name (no leading dashes).
70#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
71pub struct ArgName(String);
72
73impl ArgName {
74    /// Wrap a non-empty flag name after trim.
75    ///
76    /// Returns `None` when `name` is empty or whitespace.
77    #[must_use]
78    pub fn new(name: impl Into<String>) -> Option<Self> {
79        Self::try_from(name.into()).ok()
80    }
81
82    /// Returns the flag name as a string slice.
83    #[must_use]
84    pub fn as_str(&self) -> &str {
85        &self.0
86    }
87}
88
89impl TryFrom<String> for ArgName {
90    type Error = ExtensionIdError;
91    fn try_from(s: String) -> Result<Self, Self::Error> {
92        let trimmed = s.trim();
93        if trimmed.is_empty() {
94            return Err(ExtensionIdError(
95                "arg name must not be empty or whitespace".into(),
96            ));
97        }
98        Ok(Self(trimmed.to_owned()))
99    }
100}
101
102impl std::fmt::Display for ArgName {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        self.0.fmt(f)
105    }
106}
107
108impl AsRef<str> for ArgName {
109    fn as_ref(&self) -> &str {
110        &self.0
111    }
112}
113
114impl std::borrow::Borrow<str> for ArgName {
115    fn borrow(&self) -> &str {
116        &self.0
117    }
118}
119
120impl PartialEq<str> for ArgName {
121    fn eq(&self, other: &str) -> bool {
122        self.0 == other
123    }
124}
125
126impl PartialEq<&str> for ArgName {
127    fn eq(&self, other: &&str) -> bool {
128        self.0 == *other
129    }
130}
131
132/// Bare PATH binary name (non-empty, no path separators).
133///
134/// Constructed via `serde` `try_from` at registry load.
135#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
136pub struct BinaryName(String);
137
138impl BinaryName {
139    /// Returns the binary name as a string slice.
140    #[must_use]
141    pub fn as_str(&self) -> &str {
142        &self.0
143    }
144}
145
146impl std::fmt::Display for BinaryName {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        self.0.fmt(f)
149    }
150}
151
152impl AsRef<str> for BinaryName {
153    fn as_ref(&self) -> &str {
154        &self.0
155    }
156}
157
158impl std::borrow::Borrow<str> for BinaryName {
159    fn borrow(&self) -> &str {
160        &self.0
161    }
162}
163
164impl PartialEq<str> for BinaryName {
165    fn eq(&self, other: &str) -> bool {
166        self.0 == other
167    }
168}
169
170impl PartialEq<&str> for BinaryName {
171    fn eq(&self, other: &&str) -> bool {
172        self.0 == *other
173    }
174}
175
176impl TryFrom<String> for BinaryName {
177    type Error = ExtensionIdError;
178    fn try_from(s: String) -> Result<Self, Self::Error> {
179        let trimmed = s.trim();
180        if trimmed.is_empty() {
181            return Err(ExtensionIdError(
182                "binary name must not be empty or whitespace".into(),
183            ));
184        }
185        if trimmed.contains('/') || trimmed.contains('\\') {
186            return Err(ExtensionIdError(format!(
187                "'{trimmed}' looks like a path; use bare binary name"
188            )));
189        }
190        Ok(Self(trimmed.to_owned()))
191    }
192}
193
194impl<'de> serde::Deserialize<'de> for BinaryName {
195    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
196        let s = String::deserialize(d)?;
197        Self::try_from(s).map_err(serde::de::Error::custom)
198    }
199}
200
201/// Non-empty match token (suffix, filename, or argv prefix element).
202#[derive(Debug, Clone, PartialEq, Eq, Hash)]
203pub struct MatchToken(String);
204
205impl MatchToken {
206    /// Returns the token as a string slice.
207    #[must_use]
208    pub fn as_str(&self) -> &str {
209        &self.0
210    }
211}
212
213impl std::fmt::Display for MatchToken {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        self.0.fmt(f)
216    }
217}
218
219impl AsRef<str> for MatchToken {
220    fn as_ref(&self) -> &str {
221        &self.0
222    }
223}
224
225impl TryFrom<String> for MatchToken {
226    type Error = ExtensionIdError;
227    fn try_from(s: String) -> Result<Self, Self::Error> {
228        let trimmed = s.trim();
229        if trimmed.is_empty() {
230            return Err(ExtensionIdError(
231                "match token must not be empty or whitespace".into(),
232            ));
233        }
234        Ok(Self(trimmed.to_owned()))
235    }
236}
237
238impl<'de> serde::Deserialize<'de> for MatchToken {
239    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
240        let s = String::deserialize(d)?;
241        Self::try_from(s).map_err(serde::de::Error::custom)
242    }
243}
244
245impl<'de> serde::Deserialize<'de> for ExtensionId {
246    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
247        let s = String::deserialize(d)?;
248        Self::try_from(s).map_err(serde::de::Error::custom)
249    }
250}