1use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::collections::BTreeMap;
6use std::fmt::{Display, Formatter};
7use std::hash::{Hash, Hasher};
8use std::marker::PhantomData;
9
10macro_rules! define_id {
11 ($name:ident) => {
12 #[derive(
13 Clone,
14 Copy,
15 Debug,
16 Default,
17 Deserialize,
18 Eq,
19 Hash,
20 Ord,
21 PartialEq,
22 PartialOrd,
23 Serialize,
24 )]
25 #[serde(transparent)]
26 pub struct $name(pub u64);
27
28 impl $name {
29 #[must_use]
30 pub const fn new(value: u64) -> Self {
31 Self(value)
32 }
33
34 #[must_use]
35 pub const fn get(self) -> u64 {
36 self.0
37 }
38 }
39
40 impl Display for $name {
41 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
42 Display::fmt(&self.0, formatter)
43 }
44 }
45 };
46}
47
48define_id!(ArmyId);
49define_id!(BoundaryId);
50define_id!(CommandAttemptId);
51define_id!(CommandId);
52define_id!(CommandRequestId);
53define_id!(DecisionRequestId);
54define_id!(DecisionTicketId);
55define_id!(DecisionTraceId);
56define_id!(EventId);
57define_id!(GovernmentId);
58define_id!(IngressId);
59define_id!(HolderKnowledgeRecordId);
60define_id!(LetterId);
61define_id!(OrganizationId);
62define_id!(PersonId);
63define_id!(RandomDrawId);
64define_id!(KnowledgeRecordId);
65define_id!(ResourceId);
66define_id!(RouteId);
67define_id!(TerritoryId);
68
69#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
72pub struct DomainRecordKind {
73 pub namespace: String,
74 pub name: String,
75}
76
77impl DomainRecordKind {
78 #[must_use]
79 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
80 Self {
81 namespace: namespace.into(),
82 name: name.into(),
83 }
84 }
85
86 #[must_use]
87 pub fn for_type<T: DomainRecordType>() -> Self {
88 Self::new(T::NAMESPACE, T::NAME)
89 }
90
91 #[must_use]
92 pub fn matches_type<T: DomainRecordType>(&self) -> bool {
93 self.namespace == T::NAMESPACE && self.name == T::NAME
94 }
95}
96
97impl Display for DomainRecordKind {
98 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
99 write!(formatter, "{}.{}", self.namespace, self.name)
100 }
101}
102
103#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
105pub struct DomainRecordRef {
106 pub kind: DomainRecordKind,
107 pub id: String,
108}
109
110#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
113#[serde(tag = "type", rename_all = "snake_case")]
114pub enum DomainRecordVersionSource {
115 InitialScenario,
116 BoundaryChange {
117 boundary: BoundaryId,
118 change_index: u64,
119 },
120}
121
122#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
124pub struct DomainRecordVersionRef {
125 pub record: DomainRecordRef,
126 pub version: u64,
127 pub established_by: DomainRecordVersionSource,
128}
129
130#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
133#[serde(tag = "type", content = "value", rename_all = "snake_case")]
134pub enum EvidenceRef {
135 Command(CommandId),
136 CommandAttempt(CommandAttemptId),
137 Event(EventId),
138 Ingress(IngressId),
139 Boundary(BoundaryId),
140 RandomDraw(RandomDrawId),
141 DomainRecordVersion(DomainRecordVersionRef),
142}
143
144#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
146pub struct KnowledgeRecordKind {
147 pub namespace: String,
148 pub name: String,
149}
150
151impl KnowledgeRecordKind {
152 #[must_use]
153 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
154 Self {
155 namespace: namespace.into(),
156 name: name.into(),
157 }
158 }
159}
160
161impl Display for KnowledgeRecordKind {
162 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
163 write!(formatter, "{}.{}", self.namespace, self.name)
164 }
165}
166
167#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
169pub struct KnowledgeSchemaId {
170 pub kind: KnowledgeRecordKind,
171 pub version: u32,
172}
173
174impl KnowledgeSchemaId {
175 #[must_use]
176 pub fn new(kind: KnowledgeRecordKind, version: u32) -> Self {
177 Self { kind, version }
178 }
179}
180
181#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
183#[serde(tag = "type", content = "value", rename_all = "snake_case")]
184pub enum KnowledgeHolderRef {
185 Person(PersonId),
186 Entity(EntityRef),
187}
188
189impl KnowledgeHolderRef {
190 #[must_use]
191 pub fn is_person_entity(&self) -> bool {
192 matches!(self, Self::Entity(EntityRef::Person(_)))
193 }
194}
195
196#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
198#[serde(rename_all = "snake_case")]
199pub enum KnowledgeHolderPolicy {
200 #[default]
201 Disallowed,
202 Allowed,
203}
204
205pub trait KnowledgeRecordType {
207 type Payload;
208
209 const NAMESPACE: &'static str;
210 const NAME: &'static str;
211 const SCHEMA_VERSION: u32;
212}
213
214impl DomainRecordRef {
215 #[must_use]
216 pub fn new(
217 namespace: impl Into<String>,
218 kind: impl Into<String>,
219 id: impl Into<String>,
220 ) -> Self {
221 Self {
222 kind: DomainRecordKind::new(namespace, kind),
223 id: id.into(),
224 }
225 }
226}
227
228impl Display for DomainRecordRef {
229 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
230 write!(formatter, "{}:{}", self.kind, self.id)
231 }
232}
233
234pub trait DomainRecordType {
240 type Payload;
241 type Class: DomainKindClass;
242
243 const NAMESPACE: &'static str;
244 const NAME: &'static str;
245}
246
247mod domain_kind_class {
248 pub trait Sealed {}
249}
250
251pub trait DomainKindClass: domain_kind_class::Sealed {
253 const IS_ENTITY: bool;
254}
255
256pub enum DomainEntityKindClass {}
258
259impl domain_kind_class::Sealed for DomainEntityKindClass {}
260
261impl DomainKindClass for DomainEntityKindClass {
262 const IS_ENTITY: bool = true;
263}
264
265pub enum DomainValueKindClass {}
267
268impl domain_kind_class::Sealed for DomainValueKindClass {}
269
270impl DomainKindClass for DomainValueKindClass {
271 const IS_ENTITY: bool = false;
272}
273
274pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
276
277impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
278
279pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
281
282impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
283
284#[derive(Serialize)]
289#[serde(transparent, bound = "")]
290pub struct TypedDomainRecordRef<T: DomainRecordType> {
291 reference: DomainRecordRef,
292 #[serde(skip)]
293 marker: PhantomData<fn() -> T>,
294}
295
296impl<T: DomainRecordType> Clone for TypedDomainRecordRef<T> {
297 fn clone(&self) -> Self {
298 Self {
299 reference: self.reference.clone(),
300 marker: PhantomData,
301 }
302 }
303}
304
305impl<T: DomainRecordType> std::fmt::Debug for TypedDomainRecordRef<T> {
306 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
307 formatter
308 .debug_tuple("TypedDomainRecordRef")
309 .field(&self.reference)
310 .finish()
311 }
312}
313
314impl<T: DomainRecordType> PartialEq for TypedDomainRecordRef<T> {
315 fn eq(&self, other: &Self) -> bool {
316 self.reference == other.reference
317 }
318}
319
320impl<T: DomainRecordType> Eq for TypedDomainRecordRef<T> {}
321
322impl<T: DomainRecordType> PartialOrd for TypedDomainRecordRef<T> {
323 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
324 Some(self.cmp(other))
325 }
326}
327
328impl<T: DomainRecordType> Ord for TypedDomainRecordRef<T> {
329 fn cmp(&self, other: &Self) -> Ordering {
330 self.reference.cmp(&other.reference)
331 }
332}
333
334impl<T: DomainRecordType> Hash for TypedDomainRecordRef<T> {
335 fn hash<H: Hasher>(&self, state: &mut H) {
336 self.reference.hash(state);
337 }
338}
339
340impl<'de, T: DomainRecordType> Deserialize<'de> for TypedDomainRecordRef<T> {
341 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
342 where
343 D: serde::Deserializer<'de>,
344 {
345 let reference = DomainRecordRef::deserialize(deserializer)?;
346 Self::from_untyped(reference).map_err(|reference| {
347 serde::de::Error::custom(format!(
348 "domain record reference {reference} does not match typed kind {}",
349 DomainRecordKind::for_type::<T>()
350 ))
351 })
352 }
353}
354
355impl<T: DomainRecordType> TypedDomainRecordRef<T> {
356 #[must_use]
357 pub fn new(id: impl Into<String>) -> Self {
358 Self {
359 reference: DomainRecordRef {
360 kind: DomainRecordKind::for_type::<T>(),
361 id: id.into(),
362 },
363 marker: PhantomData,
364 }
365 }
366
367 #[must_use]
368 pub const fn as_untyped(&self) -> &DomainRecordRef {
369 &self.reference
370 }
371
372 #[must_use]
373 pub fn into_untyped(self) -> DomainRecordRef {
374 self.reference
375 }
376
377 pub fn from_untyped(reference: DomainRecordRef) -> Result<Self, DomainRecordRef> {
383 if !reference.kind.matches_type::<T>() {
384 return Err(reference);
385 }
386 Ok(Self {
387 reference,
388 marker: PhantomData,
389 })
390 }
391
392 #[must_use]
393 pub fn id(&self) -> &str {
394 &self.reference.id
395 }
396}
397
398impl<T: DomainRecordType> Display for TypedDomainRecordRef<T> {
399 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
400 Display::fmt(&self.reference, formatter)
401 }
402}
403
404impl<T: DomainRecordType> From<TypedDomainRecordRef<T>> for DomainRecordRef {
405 fn from(reference: TypedDomainRecordRef<T>) -> Self {
406 reference.into_untyped()
407 }
408}
409
410impl<T: DomainEntityType> From<TypedDomainRecordRef<T>> for EntityRef {
411 fn from(reference: TypedDomainRecordRef<T>) -> Self {
412 Self::Domain(reference.into_untyped())
413 }
414}
415
416#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
417#[serde(rename_all = "snake_case")]
418pub enum CoreEntityKind {
419 Army,
420 Government,
421 Organization,
422 Person,
423 Resource,
424 Route,
425 Territory,
426}
427
428#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
430#[serde(tag = "type", content = "id", rename_all = "snake_case")]
431pub enum EntityRef {
432 Army(ArmyId),
433 Domain(DomainRecordRef),
434 Government(GovernmentId),
435 Organization(OrganizationId),
436 Person(PersonId),
437 Resource(ResourceId),
438 Route(RouteId),
439 Territory(TerritoryId),
440}
441
442impl Display for EntityRef {
443 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
444 match self {
445 Self::Army(id) => write!(formatter, "army:{id}"),
446 Self::Domain(reference) => write!(formatter, "domain:{reference}"),
447 Self::Government(id) => write!(formatter, "government:{id}"),
448 Self::Organization(id) => write!(formatter, "organization:{id}"),
449 Self::Person(id) => write!(formatter, "person:{id}"),
450 Self::Resource(id) => write!(formatter, "resource:{id}"),
451 Self::Route(id) => write!(formatter, "route:{id}"),
452 Self::Territory(id) => write!(formatter, "territory:{id}"),
453 }
454 }
455}
456
457impl EntityRef {
458 #[must_use]
459 pub const fn core_kind(&self) -> Option<CoreEntityKind> {
460 match self {
461 Self::Army(_) => Some(CoreEntityKind::Army),
462 Self::Domain(_) => None,
463 Self::Government(_) => Some(CoreEntityKind::Government),
464 Self::Organization(_) => Some(CoreEntityKind::Organization),
465 Self::Person(_) => Some(CoreEntityKind::Person),
466 Self::Resource(_) => Some(CoreEntityKind::Resource),
467 Self::Route(_) => Some(CoreEntityKind::Route),
468 Self::Territory(_) => Some(CoreEntityKind::Territory),
469 }
470 }
471}
472
473#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
476pub struct DeterministicRng {
477 state: u64,
478}
479
480impl DeterministicRng {
481 const STEP: u64 = 0x9E37_79B9_7F4A_7C15;
482
483 #[must_use]
484 pub const fn from_seed(seed: u64) -> Self {
485 Self { state: seed }
486 }
487
488 #[must_use]
489 pub const fn state(self) -> u64 {
490 self.state
491 }
492
493 #[must_use]
494 pub const fn state_after(seed: u64, draws: u64) -> u64 {
495 seed.wrapping_add(Self::STEP.wrapping_mul(draws))
496 }
497
498 #[must_use]
499 pub const fn seed_before(state: u64, draws: u64) -> u64 {
500 state.wrapping_sub(Self::STEP.wrapping_mul(draws))
501 }
502
503 pub fn next_u64(&mut self) -> u64 {
504 self.state = self.state.wrapping_add(Self::STEP);
505 let mut value = self.state;
506 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
507 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
508 value ^ (value >> 31)
509 }
510
511 pub fn range(&mut self, upper_exclusive: u64) -> u64 {
513 if upper_exclusive == 0 {
514 return 0;
515 }
516 self.next_u64() % upper_exclusive
517 }
518}
519
520#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
521pub struct FieldSchema {
522 pub name: String,
523 pub value_type: String,
524 pub description: String,
525 pub reference_type: Option<String>,
526 pub writable_via_debug_command: bool,
527}
528
529#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
530pub struct TypeSchema {
531 pub type_name: String,
532 pub description: String,
533 pub fields: Vec<FieldSchema>,
534}
535
536#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
537pub struct SchemaRegistry {
538 types: BTreeMap<String, TypeSchema>,
539}
540
541impl SchemaRegistry {
542 pub fn register(&mut self, schema: TypeSchema) {
543 self.types.insert(schema.type_name.clone(), schema);
544 }
545
546 #[must_use]
547 pub fn get(&self, type_name: &str) -> Option<&TypeSchema> {
548 self.types.get(type_name)
549 }
550
551 pub fn iter(&self) -> impl Iterator<Item = &TypeSchema> {
552 self.types.values()
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 struct Office;
561
562 impl DomainRecordType for Office {
563 type Payload = String;
564 type Class = DomainEntityKindClass;
565
566 const NAMESPACE: &'static str = "fixture.governance";
567 const NAME: &'static str = "office";
568 }
569
570 struct Obligation;
571
572 impl DomainRecordType for Obligation {
573 type Payload = String;
574 type Class = DomainValueKindClass;
575
576 const NAMESPACE: &'static str = "fixture.governance";
577 const NAME: &'static str = "obligation";
578 }
579
580 struct Assessment;
581
582 impl KnowledgeRecordType for Assessment {
583 type Payload = String;
584
585 const NAMESPACE: &'static str = "fixture.knowledge";
586 const NAME: &'static str = "assessment";
587 const SCHEMA_VERSION: u32 = 2;
588 }
589
590 #[test]
591 fn typed_domain_identity_preserves_wire_shape_and_kind_boundary() {
592 let typed = TypedDomainRecordRef::<Office>::new("secretariat");
593 let raw = DomainRecordRef::new("fixture.governance", "office", "secretariat");
594
595 assert_eq!(
596 serde_json::to_value(&typed).expect("typed identity should serialize"),
597 serde_json::to_value(&raw).expect("raw identity should serialize")
598 );
599 let round_trip: TypedDomainRecordRef<Office> = serde_json::from_value(
600 serde_json::to_value(&typed).expect("typed identity should serialize"),
601 )
602 .expect("typed identity should deserialize");
603 assert_eq!(round_trip.as_untyped(), &raw);
604 assert_eq!(EntityRef::from(round_trip), EntityRef::Domain(raw.clone()));
605
606 let wrong_kind =
607 DomainRecordRef::new("fixture.governance", "obligation", "secretariat-duty");
608 assert_eq!(
609 TypedDomainRecordRef::<Office>::from_untyped(wrong_kind.clone()),
610 Err(wrong_kind)
611 );
612 assert!(TypedDomainRecordRef::<Obligation>::from_untyped(raw).is_err());
613 assert!(
614 serde_json::from_value::<TypedDomainRecordRef<Office>>(serde_json::json!({
615 "kind": {
616 "namespace": "fixture.governance",
617 "name": "obligation"
618 },
619 "id": "secretariat-duty"
620 }))
621 .is_err()
622 );
623 }
624
625 #[test]
626 fn knowledge_identity_and_holder_wire_shapes_are_stable() {
627 let kind = KnowledgeRecordKind::new(Assessment::NAMESPACE, Assessment::NAME);
628 let schema = KnowledgeSchemaId::new(kind.clone(), Assessment::SCHEMA_VERSION);
629
630 assert_eq!(kind.to_string(), "fixture.knowledge.assessment");
631 assert_eq!(schema.version, 2);
632 assert_eq!(schema.kind, kind);
633 assert_eq!(
634 KnowledgeHolderPolicy::default(),
635 KnowledgeHolderPolicy::Disallowed
636 );
637
638 assert_eq!(
639 serde_json::to_value(KnowledgeHolderRef::Person(PersonId::new(7)))
640 .expect("person holder should serialize"),
641 serde_json::json!({ "type": "person", "value": 7 })
642 );
643 let invalid_shape = KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(7)));
644 assert!(invalid_shape.is_person_entity());
645 let institution =
646 KnowledgeHolderRef::Entity(EntityRef::Organization(OrganizationId::new(3)));
647 assert!(!institution.is_person_entity());
648 assert_eq!(
649 serde_json::to_value(institution).expect("institution holder should serialize"),
650 serde_json::json!({
651 "type": "entity",
652 "value": { "type": "organization", "id": 3 }
653 })
654 );
655 }
656
657 #[test]
658 fn exact_domain_record_evidence_has_a_stable_wire_identity() {
659 let evidence = EvidenceRef::DomainRecordVersion(DomainRecordVersionRef {
660 record: DomainRecordRef::new("fixture.information", "dispatch", "dispatch-7"),
661 version: 2,
662 established_by: DomainRecordVersionSource::BoundaryChange {
663 boundary: BoundaryId::new(12),
664 change_index: 3,
665 },
666 });
667
668 assert_eq!(
669 serde_json::to_value(evidence).expect("evidence should serialize"),
670 serde_json::json!({
671 "type": "domain_record_version",
672 "value": {
673 "record": {
674 "kind": {
675 "namespace": "fixture.information",
676 "name": "dispatch"
677 },
678 "id": "dispatch-7"
679 },
680 "version": 2,
681 "established_by": {
682 "type": "boundary_change",
683 "boundary": 12,
684 "change_index": 3
685 }
686 }
687 })
688 );
689 }
690}