Skip to main content

spin_serde/
id.rs

1//! ID (de)serialization
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// An ID is a non-empty string containing one or more component model
7/// `word`s separated by a delimiter char.
8#[derive(
9    Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
10)]
11#[serde(into = "String", try_from = "String")]
12pub struct Id<const DELIM: char, const LOWER: bool>(String);
13
14impl<const DELIM: char, const LOWER: bool> std::fmt::Display for Id<DELIM, LOWER> {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        write!(f, "{}", self.0)
17    }
18}
19
20impl<const DELIM: char, const LOWER: bool> AsRef<str> for Id<DELIM, LOWER> {
21    fn as_ref(&self) -> &str {
22        &self.0
23    }
24}
25
26impl<const DELIM: char, const LOWER: bool> From<Id<DELIM, LOWER>> for String {
27    fn from(value: Id<DELIM, LOWER>) -> Self {
28        value.0
29    }
30}
31
32impl<const DELIM: char, const LOWER: bool> TryFrom<String> for Id<DELIM, LOWER> {
33    type Error = String;
34
35    fn try_from(id: String) -> Result<Self, Self::Error> {
36        if id.is_empty() {
37            return Err("empty".into());
38        }
39        // Special-case common "wrong separator" errors
40        if let Some(wrong) = wrong_delim::<DELIM>()
41            && id.contains(wrong)
42        {
43            return Err(format!(
44                "words must be separated with {DELIM:?}, not {wrong:?}"
45            ));
46        }
47        for word in id.split(DELIM) {
48            if word.is_empty() {
49                return Err(format!("{DELIM:?}-separated words must not be empty"));
50            }
51            let mut chars = word.chars();
52            let first = chars.next().unwrap();
53            if !first.is_ascii_alphabetic() {
54                return Err(format!(
55                    "{DELIM:?}-separated words must start with an ASCII letter; got {first:?}"
56                ));
57            }
58            let word_is_uppercase = first.is_ascii_uppercase();
59            for ch in chars {
60                if ch.is_ascii_digit() {
61                } else if !ch.is_ascii_alphanumeric() {
62                    return Err(format!(
63                        "{DELIM:?}-separated words may only contain alphanumeric ASCII; got {ch:?}"
64                    ));
65                } else if ch.is_ascii_uppercase() != word_is_uppercase {
66                    return Err(format!(
67                        "{DELIM:?}-separated words must be all lowercase or all UPPERCASE; got {word:?}"
68                    ));
69                }
70            }
71            if LOWER && word_is_uppercase {
72                return Err(format!(
73                    "Lower-case identifiers must be all lowercase; got {id:?}"
74                ));
75            }
76        }
77        Ok(Self(id))
78    }
79}
80
81#[cfg(test)]
82impl<const DELIM: char, const LOWER: bool> indexmap::Equivalent<Id<DELIM, LOWER>> for &str {
83    fn equivalent(&self, key: &Id<DELIM, LOWER>) -> bool {
84        key.as_ref() == *self
85    }
86}
87
88impl<const DELIM: char, const LOWER: bool> std::borrow::Borrow<str> for Id<DELIM, LOWER> {
89    fn borrow(&self) -> &str {
90        self.as_ref()
91    }
92}
93
94const fn wrong_delim<const DELIM: char>() -> Option<char> {
95    match DELIM {
96        '_' => Some('-'),
97        '-' => Some('_'),
98        _ => None,
99    }
100}