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 if id.contains(wrong) {
42 return Err(format!(
43 "words must be separated with {DELIM:?}, not {wrong:?}"
44 ));
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!("{DELIM:?}-separated words must be all lowercase or all UPPERCASE; got {word:?}"));
67 }
68 }
69 if LOWER && word_is_uppercase {
70 return Err(format!(
71 "Lower-case identifiers must be all lowercase; got {id:?}"
72 ));
73 }
74 }
75 Ok(Self(id))
76 }
77}
78
79const fn wrong_delim<const DELIM: char>() -> Option<char> {
80 match DELIM {
81 '_' => Some('-'),
82 '-' => Some('_'),
83 _ => None,
84 }
85}