omicron_zone_package/config/
identifier.rs1use std::{borrow::Cow, fmt, str::FromStr};
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10macro_rules! ident_newtype {
11 ($id:ident) => {
12 impl $id {
13 pub fn new<S: Into<String>>(s: S) -> Result<Self, InvalidConfigIdent> {
15 ConfigIdent::new(s).map(Self)
16 }
17
18 pub fn new_static(s: &'static str) -> Result<Self, InvalidConfigIdent> {
20 ConfigIdent::new_static(s).map(Self)
21 }
22
23 pub const fn new_const(s: &'static str) -> Self {
26 Self(ConfigIdent::new_const(s))
27 }
28
29 #[inline]
31 pub fn as_str(&self) -> &str {
32 self.0.as_str()
33 }
34
35 #[inline]
36 #[allow(dead_code)]
37 pub(crate) fn as_ident(&self) -> &ConfigIdent {
38 &self.0
39 }
40 }
41
42 impl AsRef<str> for $id {
43 #[inline]
44 fn as_ref(&self) -> &str {
45 self.0.as_ref()
46 }
47 }
48
49 impl std::fmt::Display for $id {
50 #[inline]
51 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
52 self.0.fmt(f)
53 }
54 }
55
56 impl FromStr for $id {
57 type Err = InvalidConfigIdent;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 ConfigIdent::new(s).map(Self)
61 }
62 }
63 };
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
77#[serde(transparent)]
78pub struct PackageName(ConfigIdent);
79ident_newtype!(PackageName);
80
81#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
90#[serde(transparent)]
91pub struct ServiceName(ConfigIdent);
92ident_newtype!(ServiceName);
93
94#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
103#[serde(transparent)]
104pub struct PresetName(ConfigIdent);
105ident_newtype!(PresetName);
106
107#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
108#[serde(transparent)]
109pub(crate) struct ConfigIdent(Cow<'static, str>);
110
111impl ConfigIdent {
112 pub fn new<S: Into<String>>(s: S) -> Result<Self, InvalidConfigIdent> {
114 let s = s.into();
115 Self::validate(&s)?;
116 Ok(Self(Cow::Owned(s)))
117 }
118
119 pub fn new_static(s: &'static str) -> Result<Self, InvalidConfigIdent> {
121 Self::validate(s)?;
122 Ok(Self(Cow::Borrowed(s)))
123 }
124
125 pub const fn new_const(s: &'static str) -> Self {
128 match Self::validate(s) {
129 Ok(_) => Self(Cow::Borrowed(s)),
130 Err(error) => panic!("{}", error.as_static_str()),
131 }
132 }
133
134 const fn validate(id: &str) -> Result<(), InvalidConfigIdent> {
135 if id.is_empty() {
136 return Err(InvalidConfigIdent::Empty);
137 }
138
139 let bytes = id.as_bytes();
140 if !bytes[0].is_ascii_alphabetic() {
141 return Err(InvalidConfigIdent::StartsWithNonLetter);
142 }
143
144 let mut bytes = match bytes {
145 [_, rest @ ..] => rest,
146 [] => panic!("already checked that it's non-empty"),
147 };
148 while let [next, rest @ ..] = &bytes {
149 if !(next.is_ascii_alphanumeric() || *next == b'_' || *next == b'-') {
150 break;
151 }
152 bytes = rest;
153 }
154
155 if !bytes.is_empty() {
156 return Err(InvalidConfigIdent::ContainsInvalidCharacters);
157 }
158
159 Ok(())
160 }
161
162 #[inline]
164 pub fn as_str(&self) -> &str {
165 &self.0
166 }
167}
168
169impl FromStr for ConfigIdent {
170 type Err = InvalidConfigIdent;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
173 Self::new(s)
174 }
175}
176
177impl<'de> Deserialize<'de> for ConfigIdent {
182 fn deserialize<D>(deserializer: D) -> Result<ConfigIdent, D::Error>
183 where
184 D: serde::Deserializer<'de>,
185 {
186 let s = String::deserialize(deserializer)?;
187 Self::new(s).map_err(serde::de::Error::custom)
188 }
189}
190
191impl AsRef<str> for ConfigIdent {
192 #[inline]
193 fn as_ref(&self) -> &str {
194 &self.0
195 }
196}
197
198impl std::fmt::Display for ConfigIdent {
199 #[inline]
200 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
201 self.0.fmt(f)
202 }
203}
204
205#[derive(Clone, Debug, Error)]
207pub enum InvalidConfigIdent {
208 Empty,
209 NonAsciiPrintable,
210 StartsWithNonLetter,
211 ContainsInvalidCharacters,
212}
213
214impl InvalidConfigIdent {
215 pub const fn as_static_str(&self) -> &'static str {
216 match self {
217 Self::Empty => "config identifier must be non-empty",
218 Self::NonAsciiPrintable => "config identifier must be ASCII printable",
219 Self::StartsWithNonLetter => "config identifier must start with a letter",
220 Self::ContainsInvalidCharacters => {
221 "config identifier must contain only letters, numbers, underscores, and hyphens"
222 }
223 }
224 }
225}
226
227impl fmt::Display for InvalidConfigIdent {
228 #[inline]
229 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230 self.as_static_str().fmt(f)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use serde_json::json;
238 use test_strategy::proptest;
239
240 static IDENT_REGEX: &str = r"[a-zA-Z][a-zA-Z0-9_-]*";
241
242 #[test]
243 fn valid_identifiers() {
244 let valid = [
245 "a", "ab", "a1", "a_", "a-", "a_b", "a-b", "a1_", "a1-", "a1_b", "a1-b",
246 ];
247 for &id in &valid {
248 ConfigIdent::new(id).unwrap_or_else(|error| {
249 panic!(
250 "ConfigIdent::new for {} should have succeeded, but failed with: {:?}",
251 id, error
252 );
253 });
254 PackageName::new(id).unwrap_or_else(|error| {
255 panic!(
256 "PackageName::new for {} should have succeeded, but failed with: {:?}",
257 id, error
258 );
259 });
260 ServiceName::new(id).unwrap_or_else(|error| {
261 panic!(
262 "ServiceName::new for {} should have succeeded, but failed with: {:?}",
263 id, error
264 );
265 });
266 PresetName::new(id).unwrap_or_else(|error| {
267 panic!(
268 "PresetName::new for {} should have succeeded, but failed with: {:?}",
269 id, error
270 );
271 });
272 }
273 }
274
275 #[test]
276 fn invalid_identifiers() {
277 let invalid = [
278 "", "1", "_", "-", "1_", "-a", "_a", "a!", "a ", "a\n", "a\t", "a\r", "a\x7F", "aɑ",
279 ];
280 for &id in &invalid {
281 ConfigIdent::new(id)
282 .expect_err(&format!("ConfigIdent::new for {} should have failed", id));
283 PackageName::new(id)
284 .expect_err(&format!("PackageName::new for {} should have failed", id));
285 ServiceName::new(id)
286 .expect_err(&format!("ServiceName::new for {} should have failed", id));
287 PresetName::new(id)
288 .expect_err(&format!("PresetName::new for {} should have failed", id));
289
290 let json = json!(id);
292 serde_json::from_value::<ConfigIdent>(json.clone()).expect_err(&format!(
293 "ConfigIdent deserialization for {} should have failed",
294 id
295 ));
296 serde_json::from_value::<PackageName>(json.clone()).expect_err(&format!(
297 "PackageName deserialization for {} should have failed",
298 id
299 ));
300 serde_json::from_value::<ServiceName>(json.clone()).expect_err(&format!(
301 "ServiceName deserialization for {} should have failed",
302 id
303 ));
304 serde_json::from_value::<PresetName>(json.clone()).expect_err(&format!(
305 "PresetName deserialization for {} should have failed",
306 id
307 ));
308 }
309 }
310
311 #[proptest]
312 fn valid_identifiers_proptest(#[strategy(IDENT_REGEX)] id: String) {
313 ConfigIdent::new(&id).unwrap_or_else(|error| {
314 panic!(
315 "ConfigIdent::new for {} should have succeeded, but failed with: {:?}",
316 id, error
317 );
318 });
319 PackageName::new(&id).unwrap_or_else(|error| {
320 panic!(
321 "PackageName::new for {} should have succeeded, but failed with: {:?}",
322 id, error
323 );
324 });
325 ServiceName::new(&id).unwrap_or_else(|error| {
326 panic!(
327 "ServiceName::new for {} should have succeeded, but failed with: {:?}",
328 id, error
329 );
330 });
331 PresetName::new(&id).unwrap_or_else(|error| {
332 panic!(
333 "PresetName::new for {} should have succeeded, but failed with: {:?}",
334 id, error
335 );
336 });
337 }
338
339 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
340 struct AllIdentifiers {
341 config: ConfigIdent,
342 package: PackageName,
343 service: ServiceName,
344 preset: PresetName,
345 }
346
347 #[proptest]
348 fn valid_identifiers_proptest_serde(#[strategy(IDENT_REGEX)] id: String) {
349 let all = AllIdentifiers {
350 config: ConfigIdent::new(&id).unwrap(),
351 package: PackageName::new(&id).unwrap(),
352 service: ServiceName::new(&id).unwrap(),
353 preset: PresetName::new(&id).unwrap(),
354 };
355
356 let json = serde_json::to_value(&all).unwrap();
357 let deserialized: AllIdentifiers = serde_json::from_value(json).unwrap();
358 assert_eq!(all, deserialized);
359 }
360}