1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6#[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 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}