1use std::fmt;
4
5use serde::de::Error as _;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use unicode_ident::{is_xid_continue, is_xid_start};
8
9use crate::diagnostic::{Diagnostic, DiagnosticCategory};
10
11pub const MAX_LABEL_BYTES: usize = 255;
13
14pub const MAX_THING_IID_HEX_DIGITS: usize = 256;
16
17#[must_use]
22pub fn is_canonical_thing_iid(value: &str) -> bool {
23 value.strip_prefix("0x").is_some_and(|digits| {
24 !digits.is_empty()
25 && digits.len() <= MAX_THING_IID_HEX_DIGITS
26 && digits.bytes().all(|byte| byte.is_ascii_hexdigit())
27 })
28}
29
30const TYPEQL_3_12_1_RESERVED_LABELS: [&str; 42] = [
34 "with",
35 "given",
36 "match",
37 "fetch",
38 "update",
39 "define",
40 "undefine",
41 "redefine",
42 "insert",
43 "put",
44 "delete",
45 "end",
46 "entity",
47 "relation",
48 "attribute",
49 "role",
50 "asc",
51 "desc",
52 "struct",
53 "fun",
54 "return",
55 "alias",
56 "sub",
57 "owns",
58 "as",
59 "plays",
60 "relates",
61 "iid",
62 "isa",
63 "links",
64 "has",
65 "is",
66 "or",
67 "not",
68 "try",
69 "in",
70 "true",
71 "false",
72 "of",
73 "from",
74 "first",
75 "last",
76];
77
78const TYPEQL_3_12_BUILTIN_FUNCTION_NAMES: [&str; 9] = [
83 "abs", "ceil", "floor", "iid", "label", "len", "max", "min", "round",
84];
85
86fn is_typeql_3_12_1_reserved_label(value: &str) -> bool {
87 TYPEQL_3_12_1_RESERVED_LABELS.contains(&value)
88}
89
90#[must_use]
97pub fn is_typeql_3_12_builtin_function_name(value: &str) -> bool {
98 TYPEQL_3_12_BUILTIN_FUNCTION_NAMES.contains(&value)
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct Label(String);
104
105impl Label {
106 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
108 let value = value.into();
109 let mut chars = value.chars();
110 let valid = value.len() <= MAX_LABEL_BYTES
111 && chars.next().is_some_and(|ch| ch == '_' || is_xid_start(ch))
112 && chars.all(|ch| ch == '-' || is_xid_continue(ch))
113 && !is_typeql_3_12_1_reserved_label(&value);
114 if valid {
115 Ok(Self(value))
116 } else {
117 Err(Diagnostic::stable(
118 DiagnosticCategory::InvalidContract,
119 "malformed_id",
120 "identifier label is empty, oversized, reserved, or contains invalid characters",
121 )
122 .with_detail(
123 "maximum_bytes",
124 i64::try_from(MAX_LABEL_BYTES).unwrap_or(i64::MAX),
125 ))
126 }
127 }
128 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132}
133
134impl fmt::Display for Label {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter.write_str(self.as_str())
137 }
138}
139impl Serialize for Label {
140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141 where
142 S: Serializer,
143 {
144 serializer.serialize_str(self.as_str())
145 }
146}
147impl<'de> Deserialize<'de> for Label {
148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149 where
150 D: Deserializer<'de>,
151 {
152 Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum TypeKind {
160 Entity,
162 Relation,
164 Attribute,
166 Struct,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
172pub struct TypeId {
173 kind: TypeKind,
174 label: Label,
175}
176
177impl TypeId {
178 pub fn new(kind: TypeKind, label: impl Into<String>) -> Result<Self, Diagnostic> {
180 Ok(Self {
181 kind,
182 label: Label::new(label)?,
183 })
184 }
185 pub const fn kind(&self) -> TypeKind {
187 self.kind
188 }
189 pub fn label(&self) -> &Label {
191 &self.label
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
197pub struct RoleId {
198 declaring_relation: Label,
199 label: Label,
200}
201
202impl RoleId {
203 pub fn new(
205 declaring_relation: impl Into<String>,
206 label: impl Into<String>,
207 ) -> Result<Self, Diagnostic> {
208 Ok(Self {
209 declaring_relation: Label::new(declaring_relation)?,
210 label: Label::new(label)?,
211 })
212 }
213 pub fn declaring_relation(&self) -> &Label {
215 &self.declaring_relation
216 }
217 pub fn label(&self) -> &Label {
219 &self.label
220 }
221}
222
223macro_rules! label_id {
224 ($name:ident, $doc:literal) => {
225 #[doc = $doc]
226 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
227 #[serde(transparent)]
228 pub struct $name(Label);
229 impl $name {
230 pub fn new(label: impl Into<String>) -> Result<Self, Diagnostic> {
232 Ok(Self(Label::new(label)?))
233 }
234 pub fn label(&self) -> &Label {
236 &self.0
237 }
238 }
239 impl fmt::Display for $name {
240 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241 self.0.fmt(formatter)
242 }
243 }
244 };
245}
246
247label_id!(AttributeId, "A typed attribute identity.");
248label_id!(FunctionId, "A typed function identity.");
249label_id!(StructId, "A typed struct identity.");
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn labels_reject_malformed_input_during_deserialization() {
257 for value in ["", "9person", "person name", "person.", "a²"] {
258 assert_eq!(
259 Label::new(value).unwrap_err().code().as_str(),
260 "malformed_id"
261 );
262 }
263 assert!(serde_json::from_str::<Label>(r#""person name""#).is_err());
264 }
265
266 #[test]
267 fn labels_follow_typeql_unicode_xid_grammar() {
268 for value in ["_", "type-with-hyphens", "a·b", "a\u{301}", "℘x"] {
269 assert_eq!(Label::new(value).unwrap().as_str(), value);
270 }
271 }
272
273 #[test]
274 fn labels_reject_the_typeql_3_12_1_reserved_vocabulary() {
275 for value in TYPEQL_3_12_1_RESERVED_LABELS {
276 assert_eq!(
277 Label::new(value).unwrap_err().code().as_str(),
278 "malformed_id",
279 "reserved TypeQL word {value:?} must not cross the identifier boundary",
280 );
281 }
282 assert!(Label::new("matching").is_ok());
283 assert!(Label::new("entity-type").is_ok());
284 }
285
286 #[test]
287 fn builtin_function_names_remain_contextual_identifiers() {
288 for value in TYPEQL_3_12_BUILTIN_FUNCTION_NAMES {
289 assert!(
290 is_typeql_3_12_builtin_function_name(value),
291 "missing TypeQL built-in function {value:?}",
292 );
293 }
294 for value in ["absolute", "length", "person_name_length"] {
295 assert!(!is_typeql_3_12_builtin_function_name(value));
296 }
297
298 assert!(FunctionId::new("abs").is_ok());
299 assert!(FunctionId::new("label").is_ok());
300 assert!(FunctionId::new("iid").is_err());
301 }
302
303 #[test]
304 fn thing_iids_preserve_the_released_bounded_hexadecimal_grammar() {
305 assert!(is_canonical_thing_iid("0x0"));
306 assert!(is_canonical_thing_iid("0xAbCdEf"));
307 assert!(is_canonical_thing_iid(&format!(
308 "0x{}",
309 "a".repeat(MAX_THING_IID_HEX_DIGITS)
310 )));
311
312 for malformed in ["", "0x", "0X1", "01", "0x1g", "0x1; delete $x;"] {
313 assert!(!is_canonical_thing_iid(malformed), "{malformed:?}");
314 }
315 assert!(!is_canonical_thing_iid(&format!(
316 "0x{}",
317 "a".repeat(MAX_THING_IID_HEX_DIGITS + 1)
318 )));
319 }
320
321 #[test]
322 fn malformed_typed_id_wires_fail_closed() {
323 assert!(serde_json::from_str::<TypeId>(r#"{"kind":"entity","label":"9person"}"#).is_err());
324 assert!(serde_json::from_str::<TypeId>(r#"{"kind":"future","label":"person"}"#).is_err());
325 assert!(
326 serde_json::from_str::<RoleId>(
327 r#"{"declaring_relation":"9employment","label":"employee"}"#
328 )
329 .is_err()
330 );
331 }
332
333 #[test]
334 fn role_identity_includes_the_declaring_relation() {
335 let employee = RoleId::new("employment", "employee").unwrap();
336 let membership = RoleId::new("membership", "employee").unwrap();
337 assert_ne!(employee, membership);
338 }
339}