1use core::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::DocError;
8
9macro_rules! id_type {
10 ($(#[$m:meta])* $name:ident, $prefix:literal) => {
11 $(#[$m])*
12 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13 #[serde(transparent)]
14 pub struct $name(pub u64);
15
16 impl fmt::Display for $name {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 write!(f, concat!($prefix, "{}"), self.0)
19 }
20 }
21 };
22}
23
24id_type!(
25 EntityId,
28 "e"
29);
30id_type!(
31 ConstraintId,
33 "c"
34);
35id_type!(
36 LayerId,
38 "l"
39);
40id_type!(
41 GroupId,
43 "g"
44);
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51#[serde(try_from = "String", into = "String")]
52pub struct TypeId(pub(crate) String);
53
54impl TypeId {
55 pub const MAX_LEN: usize = 128;
57
58 pub fn new(s: impl Into<String>) -> Result<Self, DocError> {
60 let s = s.into();
61 if Self::is_valid(&s) { Ok(Self(s)) } else { Err(DocError::InvalidTypeId(s)) }
62 }
63
64 #[must_use]
66 pub fn is_valid(s: &str) -> bool {
67 if s.is_empty() || s.len() > Self::MAX_LEN {
68 return false;
69 }
70 let mut segments = 0;
71 for seg in s.split('.') {
72 segments += 1;
73 let mut chars = seg.chars();
74 match chars.next() {
75 Some(c) if c.is_ascii_lowercase() => {}
76 _ => return false,
77 }
78 if !chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-') {
79 return false;
80 }
81 }
82 segments >= 2
83 }
84
85 #[must_use]
87 pub fn namespace(&self) -> &str {
88 self.0.split('.').next().unwrap_or("")
89 }
90
91 #[must_use]
93 pub fn as_str(&self) -> &str {
94 &self.0
95 }
96}
97
98impl TryFrom<String> for TypeId {
99 type Error = DocError;
100 fn try_from(s: String) -> Result<Self, DocError> {
101 Self::new(s)
102 }
103}
104
105impl From<TypeId> for String {
106 fn from(t: TypeId) -> Self {
107 t.0
108 }
109}
110
111impl fmt::Display for TypeId {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.write_str(&self.0)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn type_id_grammar() {
123 assert!(TypeId::new("dotloom.line").is_ok());
124 assert!(TypeId::new("acme.floor-plan.wall_2").is_ok());
125 for bad in ["line", "Dotloom.line", "acme..wall", "acme.1wall", "", "acme.wa ll", "acme.wall."] {
126 assert!(TypeId::new(bad).is_err(), "{bad}");
127 }
128 assert!(TypeId::new(format!("a.{}", "b".repeat(200))).is_err());
129 assert_eq!(TypeId::new("acme.wall").unwrap().namespace(), "acme");
130 }
131
132 #[test]
133 fn ids_serialize_as_numbers() {
134 assert_eq!(serde_json::to_string(&EntityId(7)).unwrap(), "7");
135 assert_eq!(EntityId(7).to_string(), "e7");
136 let t: Result<TypeId, _> = serde_json::from_str("\"Bad\"");
137 assert!(t.is_err());
138 }
139}