1use graphforge_core::{PropId, TypeId};
4use serde::{Deserialize, Serialize};
5
6pub const TYPE_LOCAL_ID_LIMIT: u32 = 1 << 30;
8const ENTITY_TAG: u32 = 1 << 30;
9const RELATION_TAG: u32 = 1 << 31;
10const TAG_MASK: u32 = ENTITY_TAG | RELATION_TAG;
11const LOCAL_MASK: u32 = TYPE_LOCAL_ID_LIMIT - 1;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum TypeIdKind {
16 Ontology,
18 RuntimeEntity,
20 RuntimeRelation,
22 RuntimeProperty,
24 OntologyProperty,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
30pub enum CatalogIdError {
31 #[error("{kind:?} local ID {value} exceeds its supported range")]
33 OutOfRange {
34 kind: TypeIdKind,
36 value: u32,
38 },
39 #[error("type ID {encoded} has conflicting runtime entity and relation tags")]
41 ConflictingTags {
42 encoded: u32,
44 },
45 #[error("expected {expected:?} type domain, found {actual:?}")]
47 WrongDomain {
48 expected: TypeIdKind,
50 actual: TypeIdKind,
52 },
53}
54
55macro_rules! runtime_id {
56 ($name:ident, $kind:ident, $limit:expr, $doc:literal) => {
57 #[doc = $doc]
58 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59 #[serde(try_from = "u32", into = "u32")]
60 pub struct $name(u32);
61
62 impl $name {
63 pub const fn new(value: u32) -> Result<Self, CatalogIdError> {
65 if value < $limit {
66 Ok(Self(value))
67 } else {
68 Err(CatalogIdError::OutOfRange {
69 kind: TypeIdKind::$kind,
70 value,
71 })
72 }
73 }
74
75 #[must_use]
77 pub const fn get(self) -> u32 {
78 self.0
79 }
80 }
81
82 impl TryFrom<u32> for $name {
83 type Error = CatalogIdError;
84
85 fn try_from(value: u32) -> Result<Self, Self::Error> {
86 Self::new(value)
87 }
88 }
89
90 impl From<$name> for u32 {
91 fn from(value: $name) -> Self {
92 value.get()
93 }
94 }
95 };
96}
97
98runtime_id!(
99 RuntimeEntityId,
100 RuntimeEntity,
101 TYPE_LOCAL_ID_LIMIT,
102 "Checked runtime-catalog entity identity; distinct from relation and ontology IDs."
103);
104runtime_id!(
105 RuntimeRelationId,
106 RuntimeRelation,
107 TYPE_LOCAL_ID_LIMIT,
108 "Checked runtime-catalog relation identity; distinct from entity and ontology IDs."
109);
110runtime_id!(
112 RuntimePropId,
113 RuntimeProperty,
114 u32::MAX,
115 "Checked owner-scoped runtime property identity, retaining its untagged integer encoding."
116);
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119enum TypeIdentity {
120 Ontology(TypeId),
121 RuntimeEntity(RuntimeEntityId),
122 RuntimeRelation(RuntimeRelationId),
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
130#[serde(try_from = "u32", into = "u32")]
131pub struct TaggedTypeId(TypeIdentity);
132
133impl TaggedTypeId {
134 pub const fn ontology(id: TypeId) -> Result<Self, CatalogIdError> {
136 if id.0 < TYPE_LOCAL_ID_LIMIT {
137 Ok(Self(TypeIdentity::Ontology(id)))
138 } else {
139 Err(CatalogIdError::OutOfRange {
140 kind: TypeIdKind::Ontology,
141 value: id.0,
142 })
143 }
144 }
145
146 pub const fn decode(encoded: u32) -> Result<Self, CatalogIdError> {
148 let local = encoded & LOCAL_MASK;
149 match encoded & TAG_MASK {
150 0 => Ok(Self(TypeIdentity::Ontology(TypeId(local)))),
151 ENTITY_TAG => Ok(Self(TypeIdentity::RuntimeEntity(RuntimeEntityId(local)))),
152 RELATION_TAG => Ok(Self(TypeIdentity::RuntimeRelation(RuntimeRelationId(
153 local,
154 )))),
155 _ => Err(CatalogIdError::ConflictingTags { encoded }),
156 }
157 }
158
159 #[must_use]
161 pub const fn encode(self) -> u32 {
162 match self.0 {
163 TypeIdentity::Ontology(id) => id.0,
164 TypeIdentity::RuntimeEntity(id) => ENTITY_TAG | id.get(),
165 TypeIdentity::RuntimeRelation(id) => RELATION_TAG | id.get(),
166 }
167 }
168
169 #[must_use]
171 pub const fn kind(self) -> TypeIdKind {
172 match self.0 {
173 TypeIdentity::Ontology(_) => TypeIdKind::Ontology,
174 TypeIdentity::RuntimeEntity(_) => TypeIdKind::RuntimeEntity,
175 TypeIdentity::RuntimeRelation(_) => TypeIdKind::RuntimeRelation,
176 }
177 }
178
179 #[must_use]
181 pub const fn ontology_id(self) -> Option<TypeId> {
182 match self.0 {
183 TypeIdentity::Ontology(id) => Some(id),
184 _ => None,
185 }
186 }
187
188 #[must_use]
190 pub const fn runtime_entity_id(self) -> Option<RuntimeEntityId> {
191 match self.0 {
192 TypeIdentity::RuntimeEntity(id) => Some(id),
193 _ => None,
194 }
195 }
196
197 #[must_use]
199 pub const fn runtime_relation_id(self) -> Option<RuntimeRelationId> {
200 match self.0 {
201 TypeIdentity::RuntimeRelation(id) => Some(id),
202 _ => None,
203 }
204 }
205}
206
207impl TryFrom<u32> for TaggedTypeId {
208 type Error = CatalogIdError;
209 fn try_from(value: u32) -> Result<Self, Self::Error> {
210 Self::decode(value)
211 }
212}
213
214impl From<TaggedTypeId> for u32 {
215 fn from(value: TaggedTypeId) -> Self {
216 value.encode()
217 }
218}
219
220macro_rules! contextual_id {
221 ($name:ident, $runtime:ident, $variant:ident, $kind:ident, $other:ident, $doc:literal) => {
222 #[doc = $doc]
223 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
224 #[serde(try_from = "u32", into = "u32")]
225 pub struct $name(TaggedTypeId);
226
227 impl $name {
228 pub const fn ontology(id: TypeId) -> Result<Self, CatalogIdError> {
230 match TaggedTypeId::ontology(id) {
231 Ok(id) => Ok(Self(id)),
232 Err(error) => Err(error),
233 }
234 }
235
236 #[must_use]
238 pub const fn runtime(id: $runtime) -> Self {
239 Self(TaggedTypeId(TypeIdentity::$variant(id)))
240 }
241
242 pub const fn decode(encoded: u32) -> Result<Self, CatalogIdError> {
244 let decoded = match TaggedTypeId::decode(encoded) {
245 Ok(id) => id,
246 Err(error) => return Err(error),
247 };
248 match decoded.kind() {
249 TypeIdKind::$other => Err(CatalogIdError::WrongDomain {
250 expected: TypeIdKind::$kind,
251 actual: TypeIdKind::$other,
252 }),
253 _ => Ok(Self(decoded)),
254 }
255 }
256
257 #[must_use]
259 pub const fn encode(self) -> u32 {
260 self.0.encode()
261 }
262
263 #[must_use]
265 pub const fn tagged(self) -> TaggedTypeId {
266 self.0
267 }
268 }
269
270 impl TryFrom<u32> for $name {
271 type Error = CatalogIdError;
272 fn try_from(value: u32) -> Result<Self, Self::Error> {
273 Self::decode(value)
274 }
275 }
276 impl From<$name> for u32 {
277 fn from(value: $name) -> Self {
278 value.encode()
279 }
280 }
281 };
282}
283
284contextual_id!(
285 EntityTypeId,
286 RuntimeEntityId,
287 RuntimeEntity,
288 RuntimeEntity,
289 RuntimeRelation,
290 "Checked entity-label carrier, which cannot contain a runtime relation ID."
291);
292contextual_id!(
293 RelationTypeId,
294 RuntimeRelationId,
295 RuntimeRelation,
296 RuntimeRelation,
297 RuntimeEntity,
298 "Checked relation-type carrier, which cannot contain a runtime entity ID."
299);
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
303pub enum EntityTypeSelection {
304 All,
306 Known(EntityTypeId),
308 Missing,
310}
311
312impl EntityTypeSelection {
313 #[must_use]
315 pub const fn known_id(self) -> Option<EntityTypeId> {
316 match self {
317 Self::Known(id) => Some(id),
318 Self::All | Self::Missing => None,
319 }
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
325pub enum RelationTypeSelection {
326 All,
328 Known(RelationTypeId),
330 Missing,
332}
333
334impl RelationTypeSelection {
335 #[must_use]
337 pub const fn known_id(self) -> Option<RelationTypeId> {
338 match self {
339 Self::Known(id) => Some(id),
340 Self::All | Self::Missing => None,
341 }
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
350#[serde(try_from = "u32", into = "u32")]
351pub struct PrimaryEntityTypeId(Option<EntityTypeId>);
352
353impl PrimaryEntityTypeId {
354 #[must_use]
356 pub const fn absent() -> Self {
357 Self(None)
358 }
359
360 #[must_use]
362 pub const fn known(id: EntityTypeId) -> Self {
363 Self(Some(id))
364 }
365
366 pub const fn decode(encoded: u32) -> Result<Self, CatalogIdError> {
368 if encoded == u32::MAX {
369 Ok(Self::absent())
370 } else {
371 match EntityTypeId::decode(encoded) {
372 Ok(id) => Ok(Self::known(id)),
373 Err(error) => Err(error),
374 }
375 }
376 }
377
378 #[must_use]
380 pub const fn encode(self) -> u32 {
381 match self.0 {
382 Some(id) => id.encode(),
383 None => u32::MAX,
384 }
385 }
386
387 #[must_use]
389 pub const fn label(self) -> Option<EntityTypeId> {
390 self.0
391 }
392}
393
394impl TryFrom<u32> for PrimaryEntityTypeId {
395 type Error = CatalogIdError;
396
397 fn try_from(value: u32) -> Result<Self, Self::Error> {
398 Self::decode(value)
399 }
400}
401
402impl From<PrimaryEntityTypeId> for u32 {
403 fn from(value: PrimaryEntityTypeId) -> Self {
404 value.encode()
405 }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
409enum PropertyIdentity {
410 Ontology(PropId),
411 Runtime(RuntimePropId),
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
420#[serde(try_from = "PropertyWire", into = "PropertyWire")]
421pub struct PropertyId(PropertyIdentity);
422
423impl PropertyId {
424 pub const fn ontology(id: PropId) -> Result<Self, CatalogIdError> {
426 if id.0 == u32::MAX {
427 Err(CatalogIdError::OutOfRange {
428 kind: TypeIdKind::OntologyProperty,
429 value: id.0,
430 })
431 } else {
432 Ok(Self(PropertyIdentity::Ontology(id)))
433 }
434 }
435
436 #[must_use]
438 pub const fn runtime(id: RuntimePropId) -> Self {
439 Self(PropertyIdentity::Runtime(id))
440 }
441
442 #[must_use]
444 pub const fn ontology_id(self) -> Option<PropId> {
445 match self.0 {
446 PropertyIdentity::Ontology(id) => Some(id),
447 PropertyIdentity::Runtime(_) => None,
448 }
449 }
450
451 #[must_use]
453 pub const fn runtime_id(self) -> Option<RuntimePropId> {
454 match self.0 {
455 PropertyIdentity::Runtime(id) => Some(id),
456 PropertyIdentity::Ontology(_) => None,
457 }
458 }
459}
460
461impl std::fmt::Display for PropertyId {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 match self.0 {
464 PropertyIdentity::Ontology(id) => write!(f, "ontology_{}", id.0),
465 PropertyIdentity::Runtime(id) => write!(f, "runtime_{}", id.get()),
466 }
467 }
468}
469
470#[derive(Serialize, Deserialize)]
471enum PropertyWire {
472 Ontology(u32),
473 Runtime(u32),
474}
475
476impl TryFrom<PropertyWire> for PropertyId {
477 type Error = CatalogIdError;
478
479 fn try_from(value: PropertyWire) -> Result<Self, Self::Error> {
480 match value {
481 PropertyWire::Ontology(id) => Self::ontology(PropId(id)),
482 PropertyWire::Runtime(id) => RuntimePropId::new(id).map(Self::runtime),
483 }
484 }
485}
486
487impl From<PropertyId> for PropertyWire {
488 fn from(value: PropertyId) -> Self {
489 match value.0 {
490 PropertyIdentity::Ontology(id) => Self::Ontology(id.0),
491 PropertyIdentity::Runtime(id) => Self::Runtime(id.get()),
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use std::collections::HashSet;
500
501 #[test]
502 fn original_integer_and_serde_golden_vectors() {
503 let fixture: serde_json::Value =
504 serde_json::from_str(include_str!("../tests/fixtures/catalog_ids_v1.json")).unwrap();
505 let mut unique = HashSet::new();
506 for vector in fixture["valid"].as_array().unwrap() {
507 let raw = u32::try_from(vector["encoded"].as_u64().unwrap()).unwrap();
508 let local = u32::try_from(vector["local"].as_u64().unwrap()).unwrap();
509 let decoded = TaggedTypeId::decode(raw).unwrap();
510 let expected = match vector["kind"].as_str().unwrap() {
511 "ontology" => TaggedTypeId::ontology(TypeId(local)).unwrap(),
512 "runtime_entity" => {
513 EntityTypeId::runtime(RuntimeEntityId::new(local).unwrap()).tagged()
514 }
515 "runtime_relation" => {
516 RelationTypeId::runtime(RuntimeRelationId::new(local).unwrap()).tagged()
517 }
518 other => panic!("unknown frozen fixture domain {other}"),
519 };
520 assert_eq!(decoded, expected);
521 assert_eq!(decoded.encode(), raw);
522 assert_eq!(serde_json::to_value(decoded).unwrap(), vector["encoded"]);
523 assert_eq!(
524 serde_json::from_value::<TaggedTypeId>(vector["encoded"].clone()).unwrap(),
525 decoded
526 );
527 assert_eq!(
528 serde_json::to_value(raw.to_le_bytes()).unwrap(),
529 vector["little_endian"]
530 );
531 assert!(unique.insert(decoded.encode()));
532 }
533 for raw in fixture["invalid_tagged"].as_array().unwrap() {
534 let encoded = u32::try_from(raw.as_u64().unwrap()).unwrap();
535 assert!(TaggedTypeId::decode(encoded).is_err());
536 assert!(EntityTypeId::decode(encoded).is_err());
537 assert!(RelationTypeId::decode(encoded).is_err());
538 assert!(serde_json::from_value::<TaggedTypeId>(raw.clone()).is_err());
539 }
540 for raw in fixture["invalid_type_locals"].as_array().unwrap() {
541 let local = u32::try_from(raw.as_u64().unwrap()).unwrap();
542 assert!(TaggedTypeId::ontology(TypeId(local)).is_err());
543 assert!(RuntimeEntityId::new(local).is_err());
544 assert!(RuntimeRelationId::new(local).is_err());
545 }
546 }
547
548 #[test]
549 fn contextual_types_reject_the_opposite_runtime_domain() {
550 let entity = EntityTypeId::runtime(RuntimeEntityId::new(0).unwrap());
551 let relation = RelationTypeId::runtime(RuntimeRelationId::new(0).unwrap());
552 assert!(EntityTypeId::decode(relation.encode()).is_err());
553 assert!(RelationTypeId::decode(entity.encode()).is_err());
554 assert!(
555 serde_json::from_value::<EntityTypeId>(serde_json::json!(relation.encode())).is_err()
556 );
557 assert!(
558 serde_json::from_value::<RelationTypeId>(serde_json::json!(entity.encode())).is_err()
559 );
560 assert_eq!(entity.tagged().runtime_relation_id(), None);
561 assert_eq!(relation.tagged().runtime_entity_id(), None);
562 assert_eq!(entity.tagged().ontology_id(), None);
563 assert_eq!(relation.tagged().ontology_id(), None);
564 }
565
566 #[test]
567 fn primary_absence_does_not_admit_a_membership_or_relation_sentinel() {
568 let primary = PrimaryEntityTypeId::decode(u32::MAX).unwrap();
569 assert_eq!(primary, PrimaryEntityTypeId::absent());
570 assert_eq!(primary.label(), None);
571 assert_eq!(
572 serde_json::to_value(primary).unwrap(),
573 serde_json::json!(4294967295_u32)
574 );
575 assert!(EntityTypeId::decode(u32::MAX).is_err());
576 assert!(RelationTypeId::decode(u32::MAX).is_err());
577 let membership = EntityTypeId::runtime(RuntimeEntityId::new(7).unwrap());
578 assert_eq!(PrimaryEntityTypeId::known(membership).encode(), 1073741831);
579 assert!(PrimaryEntityTypeId::decode(2147483648).is_err());
580 assert!(PrimaryEntityTypeId::decode(3221225472).is_err());
581 }
582
583 #[test]
584 fn property_ir_domains_do_not_collide_or_accept_ambiguous_numeric_input() {
585 let declared = PropertyId::ontology(PropId(7)).unwrap();
586 let runtime = PropertyId::runtime(RuntimePropId::new(7).unwrap());
587 assert_ne!(declared, runtime);
588 assert_eq!(HashSet::from([declared, runtime]).len(), 2);
589 assert_eq!(declared.runtime_id(), None);
590 assert_eq!(runtime.ontology_id(), None);
591 for (id, wire) in [
592 (declared, serde_json::json!({"Ontology": 7})),
593 (runtime, serde_json::json!({"Runtime": 7})),
594 ] {
595 assert_eq!(serde_json::to_value(id).unwrap(), wire);
596 assert_eq!(serde_json::from_value::<PropertyId>(wire).unwrap(), id);
597 }
598 for invalid in [
599 serde_json::json!(7),
600 serde_json::json!({"Runtime": u32::MAX}),
601 serde_json::json!({"Ontology": u32::MAX}),
602 ] {
603 assert!(serde_json::from_value::<PropertyId>(invalid).is_err());
604 }
605 }
606
607 #[test]
608 fn missing_and_all_are_distinct_wire_selections() {
609 assert_ne!(EntityTypeSelection::All, EntityTypeSelection::Missing);
610 assert_ne!(RelationTypeSelection::All, RelationTypeSelection::Missing);
611 assert_eq!(
612 serde_json::to_value(EntityTypeSelection::All).unwrap(),
613 serde_json::json!("All")
614 );
615 assert_eq!(
616 serde_json::to_value(EntityTypeSelection::Missing).unwrap(),
617 serde_json::json!("Missing")
618 );
619 assert_eq!(
620 serde_json::from_value::<EntityTypeSelection>(serde_json::json!("Missing")).unwrap(),
621 EntityTypeSelection::Missing
622 );
623 assert!(
624 serde_json::from_value::<EntityTypeSelection>(serde_json::json!({"Known": u32::MAX}))
625 .is_err()
626 );
627 }
628
629 #[test]
630 fn property_ids_remain_untagged_and_counter_range_checked() {
631 for raw in [0, TYPE_LOCAL_ID_LIMIT, u32::MAX - 1] {
632 let id = RuntimePropId::new(raw).unwrap();
633 assert_eq!(id.get(), raw);
634 assert_eq!(serde_json::to_value(id).unwrap(), serde_json::json!(raw));
635 assert_eq!(
636 serde_json::from_value::<RuntimePropId>(serde_json::json!(raw)).unwrap(),
637 id
638 );
639 }
640 assert!(RuntimePropId::new(u32::MAX).is_err());
641 }
642}