omicron_zone_package/config/
identifier.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use 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            /// Creates a new identifier at runtime.
14            pub fn new<S: Into<String>>(s: S) -> Result<Self, InvalidConfigIdent> {
15                ConfigIdent::new(s).map(Self)
16            }
17
18            /// Creates a new identifier from a static string.
19            pub fn new_static(s: &'static str) -> Result<Self, InvalidConfigIdent> {
20                ConfigIdent::new_static(s).map(Self)
21            }
22
23            /// Creates a new identifier at compile time, panicking if it is
24            /// invalid.
25            pub const fn new_const(s: &'static str) -> Self {
26                Self(ConfigIdent::new_const(s))
27            }
28
29            /// Returns the identifier as a string.
30            #[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/// A unique identifier for a package name.
67///
68/// Package names must be:
69///
70/// * non-empty
71/// * ASCII printable
72/// * first character must be a letter
73/// * contain only letters, numbers, underscores, and hyphens
74///
75/// These generally match the rules of Rust package names.
76#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
77#[serde(transparent)]
78pub struct PackageName(ConfigIdent);
79ident_newtype!(PackageName);
80
81/// A unique identifier for a service name.
82///
83/// Package names must be:
84///
85/// * non-empty
86/// * ASCII printable
87/// * first character must be a letter
88/// * contain only letters, numbers, underscores, and hyphens
89#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
90#[serde(transparent)]
91pub struct ServiceName(ConfigIdent);
92ident_newtype!(ServiceName);
93
94/// A unique identifier for a target preset.
95///
96/// Package names must be:
97///
98/// * non-empty
99/// * ASCII printable
100/// * first character must be a letter
101/// * contain only letters, numbers, underscores, and hyphens
102#[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    /// Creates a new config identifier at runtime.
113    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    /// Creates a new config identifier from a static string.
120    pub fn new_static(s: &'static str) -> Result<Self, InvalidConfigIdent> {
121        Self::validate(s)?;
122        Ok(Self(Cow::Borrowed(s)))
123    }
124
125    /// Creates a new config identifier at compile time, panicking if the
126    /// identifier is invalid.
127    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    /// Returns the identifier as a string.
163    #[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
177// The `Deserialize` implementation for `ConfigIdent` must be manually
178// implemented, because it must go through validation. The `Serialize`
179// implementation can be derived because `ConfigIdent` serializes as a regular
180// string.
181impl<'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/// Errors that can occur when creating a `ConfigIdent`.
206#[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            // Also ensure that deserialization fails.
291            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}