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 #[doc = concat!("Stable numeric identifier for [`", stringify!($name), "`].")]
27 pub struct $name(pub u64);
28
29 impl $name {
30 #[must_use]
32 pub const fn new(value: u64) -> Self {
33 Self(value)
34 }
35
36 #[must_use]
38 pub const fn get(self) -> u64 {
39 self.0
40 }
41 }
42
43 impl Display for $name {
44 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
45 Display::fmt(&self.0, formatter)
46 }
47 }
48 };
49}
50
51define_id!(ArmyId);
52define_id!(BoundaryId);
53define_id!(CommandAttemptId);
54define_id!(CommandId);
55define_id!(CommandRequestId);
56define_id!(DecisionRequestId);
57define_id!(DecisionTicketId);
58define_id!(DecisionTraceId);
59define_id!(EventId);
60define_id!(GovernmentId);
61define_id!(IngressId);
62define_id!(HolderKnowledgeRecordId);
63define_id!(LetterId);
64define_id!(OrganizationId);
65define_id!(PersonId);
66define_id!(RandomDrawId);
67define_id!(KnowledgeRecordId);
68define_id!(ResourceId);
69define_id!(RouteId);
70define_id!(TerritoryId);
71
72#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
82#[serde(rename_all = "snake_case")]
83pub enum SimulationGranularity {
84 Aggregate,
85 Group,
86 Actor,
87}
88
89impl SimulationGranularity {
90 #[must_use]
92 pub const fn as_str(self) -> &'static str {
93 match self {
94 Self::Aggregate => "aggregate",
95 Self::Group => "group",
96 Self::Actor => "actor",
97 }
98 }
99}
100
101#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
104pub struct DomainRecordKind {
105 pub namespace: String,
106 pub name: String,
107}
108
109impl DomainRecordKind {
110 #[must_use]
111 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
112 Self {
113 namespace: namespace.into(),
114 name: name.into(),
115 }
116 }
117
118 #[must_use]
119 pub fn for_type<T: DomainRecordType>() -> Self {
120 Self::new(T::NAMESPACE, T::NAME)
121 }
122
123 #[must_use]
124 pub fn matches_type<T: DomainRecordType>(&self) -> bool {
125 self.namespace == T::NAMESPACE && self.name == T::NAME
126 }
127}
128
129impl Display for DomainRecordKind {
130 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
131 write!(formatter, "{}.{}", self.namespace, self.name)
132 }
133}
134
135#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
137pub struct DomainRecordRef {
138 pub kind: DomainRecordKind,
139 pub id: String,
140}
141
142#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
145#[serde(tag = "type", rename_all = "snake_case")]
146pub enum DomainRecordVersionSource {
147 InitialScenario,
148 BoundaryChange {
149 boundary: BoundaryId,
150 change_index: u64,
151 },
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
156pub struct DomainRecordVersionRef {
157 pub record: DomainRecordRef,
158 pub version: u64,
159 pub established_by: DomainRecordVersionSource,
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
165#[serde(tag = "type", content = "value", rename_all = "snake_case")]
166pub enum EvidenceRef {
167 Command(CommandId),
168 CommandAttempt(CommandAttemptId),
169 Event(EventId),
170 Ingress(IngressId),
171 Boundary(BoundaryId),
172 RandomDraw(RandomDrawId),
173 DomainRecordVersion(DomainRecordVersionRef),
174}
175
176#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
178pub struct KnowledgeRecordKind {
179 pub namespace: String,
180 pub name: String,
181}
182
183impl KnowledgeRecordKind {
184 #[must_use]
185 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
186 Self {
187 namespace: namespace.into(),
188 name: name.into(),
189 }
190 }
191}
192
193impl Display for KnowledgeRecordKind {
194 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
195 write!(formatter, "{}.{}", self.namespace, self.name)
196 }
197}
198
199#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
201pub struct KnowledgeSchemaId {
202 pub kind: KnowledgeRecordKind,
203 pub version: u32,
204}
205
206impl KnowledgeSchemaId {
207 #[must_use]
208 pub fn new(kind: KnowledgeRecordKind, version: u32) -> Self {
209 Self { kind, version }
210 }
211}
212
213#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
215#[serde(tag = "type", content = "value", rename_all = "snake_case")]
216pub enum KnowledgeHolderRef {
217 Person(PersonId),
218 Entity(EntityRef),
219}
220
221impl KnowledgeHolderRef {
222 #[must_use]
223 pub const fn is_person_entity(&self) -> bool {
224 matches!(self, Self::Entity(EntityRef::Person(_)))
225 }
226}
227
228#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(rename_all = "snake_case")]
231pub enum KnowledgeHolderPolicy {
232 #[default]
233 Disallowed,
234 Allowed,
235}
236
237pub trait KnowledgeRecordType {
239 type Payload;
240
241 const NAMESPACE: &'static str;
242 const NAME: &'static str;
243 const SCHEMA_VERSION: u32;
244}
245
246impl DomainRecordRef {
247 #[must_use]
248 pub fn new(
249 namespace: impl Into<String>,
250 kind: impl Into<String>,
251 id: impl Into<String>,
252 ) -> Self {
253 Self {
254 kind: DomainRecordKind::new(namespace, kind),
255 id: id.into(),
256 }
257 }
258}
259
260impl Display for DomainRecordRef {
261 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
262 write!(formatter, "{}:{}", self.kind, self.id)
263 }
264}
265
266pub trait DomainRecordType {
272 type Payload;
273 type Class: DomainKindClass;
274
275 const NAMESPACE: &'static str;
276 const NAME: &'static str;
277}
278
279mod domain_kind_class {
280 pub trait Sealed {}
281}
282
283pub trait DomainKindClass: domain_kind_class::Sealed {
285 const IS_ENTITY: bool;
286}
287
288pub enum DomainEntityKindClass {}
290
291impl domain_kind_class::Sealed for DomainEntityKindClass {}
292
293impl DomainKindClass for DomainEntityKindClass {
294 const IS_ENTITY: bool = true;
295}
296
297pub enum DomainValueKindClass {}
299
300impl domain_kind_class::Sealed for DomainValueKindClass {}
301
302impl DomainKindClass for DomainValueKindClass {
303 const IS_ENTITY: bool = false;
304}
305
306pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
308
309impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
310
311pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
313
314impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
315
316#[derive(Serialize)]
321#[serde(transparent, bound = "")]
322pub struct TypedDomainRecordRef<T: DomainRecordType> {
323 reference: DomainRecordRef,
324 #[serde(skip)]
325 marker: PhantomData<fn() -> T>,
326}
327
328impl<T: DomainRecordType> Clone for TypedDomainRecordRef<T> {
329 fn clone(&self) -> Self {
330 Self {
331 reference: self.reference.clone(),
332 marker: PhantomData,
333 }
334 }
335}
336
337impl<T: DomainRecordType> std::fmt::Debug for TypedDomainRecordRef<T> {
338 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
339 formatter
340 .debug_tuple("TypedDomainRecordRef")
341 .field(&self.reference)
342 .finish()
343 }
344}
345
346impl<T: DomainRecordType> PartialEq for TypedDomainRecordRef<T> {
347 fn eq(&self, other: &Self) -> bool {
348 self.reference == other.reference
349 }
350}
351
352impl<T: DomainRecordType> Eq for TypedDomainRecordRef<T> {}
353
354impl<T: DomainRecordType> PartialOrd for TypedDomainRecordRef<T> {
355 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
356 Some(self.cmp(other))
357 }
358}
359
360impl<T: DomainRecordType> Ord for TypedDomainRecordRef<T> {
361 fn cmp(&self, other: &Self) -> Ordering {
362 self.reference.cmp(&other.reference)
363 }
364}
365
366impl<T: DomainRecordType> Hash for TypedDomainRecordRef<T> {
367 fn hash<H: Hasher>(&self, state: &mut H) {
368 self.reference.hash(state);
369 }
370}
371
372impl<'de, T: DomainRecordType> Deserialize<'de> for TypedDomainRecordRef<T> {
373 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
374 where
375 D: serde::Deserializer<'de>,
376 {
377 let reference = DomainRecordRef::deserialize(deserializer)?;
378 Self::from_untyped(reference).map_err(|reference| {
379 serde::de::Error::custom(format!(
380 "domain record reference {reference} does not match typed kind {}",
381 DomainRecordKind::for_type::<T>()
382 ))
383 })
384 }
385}
386
387impl<T: DomainRecordType> TypedDomainRecordRef<T> {
388 #[must_use]
389 pub fn new(id: impl Into<String>) -> Self {
390 Self {
391 reference: DomainRecordRef {
392 kind: DomainRecordKind::for_type::<T>(),
393 id: id.into(),
394 },
395 marker: PhantomData,
396 }
397 }
398
399 #[must_use]
400 pub const fn as_untyped(&self) -> &DomainRecordRef {
401 &self.reference
402 }
403
404 #[must_use]
405 pub fn into_untyped(self) -> DomainRecordRef {
406 self.reference
407 }
408
409 pub fn from_untyped(reference: DomainRecordRef) -> Result<Self, DomainRecordRef> {
415 if !reference.kind.matches_type::<T>() {
416 return Err(reference);
417 }
418 Ok(Self {
419 reference,
420 marker: PhantomData,
421 })
422 }
423
424 #[must_use]
425 pub fn id(&self) -> &str {
426 &self.reference.id
427 }
428}
429
430impl<T: DomainRecordType> Display for TypedDomainRecordRef<T> {
431 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
432 Display::fmt(&self.reference, formatter)
433 }
434}
435
436impl<T: DomainRecordType> From<TypedDomainRecordRef<T>> for DomainRecordRef {
437 fn from(reference: TypedDomainRecordRef<T>) -> Self {
438 reference.into_untyped()
439 }
440}
441
442impl<T: DomainEntityType> From<TypedDomainRecordRef<T>> for EntityRef {
443 fn from(reference: TypedDomainRecordRef<T>) -> Self {
444 Self::Domain(reference.into_untyped())
445 }
446}
447
448#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
449#[serde(rename_all = "snake_case")]
450pub enum CoreEntityKind {
451 Army,
452 Government,
453 Organization,
454 Person,
455 Resource,
456 Route,
457 Territory,
458}
459
460#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
462#[serde(tag = "type", content = "id", rename_all = "snake_case")]
463pub enum EntityRef {
464 Army(ArmyId),
465 Domain(DomainRecordRef),
466 Government(GovernmentId),
467 Organization(OrganizationId),
468 Person(PersonId),
469 Resource(ResourceId),
470 Route(RouteId),
471 Territory(TerritoryId),
472}
473
474impl Display for EntityRef {
475 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
476 match self {
477 Self::Army(id) => write!(formatter, "army:{id}"),
478 Self::Domain(reference) => write!(formatter, "domain:{reference}"),
479 Self::Government(id) => write!(formatter, "government:{id}"),
480 Self::Organization(id) => write!(formatter, "organization:{id}"),
481 Self::Person(id) => write!(formatter, "person:{id}"),
482 Self::Resource(id) => write!(formatter, "resource:{id}"),
483 Self::Route(id) => write!(formatter, "route:{id}"),
484 Self::Territory(id) => write!(formatter, "territory:{id}"),
485 }
486 }
487}
488
489impl EntityRef {
490 #[must_use]
491 pub const fn core_kind(&self) -> Option<CoreEntityKind> {
492 match self {
493 Self::Army(_) => Some(CoreEntityKind::Army),
494 Self::Domain(_) => None,
495 Self::Government(_) => Some(CoreEntityKind::Government),
496 Self::Organization(_) => Some(CoreEntityKind::Organization),
497 Self::Person(_) => Some(CoreEntityKind::Person),
498 Self::Resource(_) => Some(CoreEntityKind::Resource),
499 Self::Route(_) => Some(CoreEntityKind::Route),
500 Self::Territory(_) => Some(CoreEntityKind::Territory),
501 }
502 }
503}
504
505#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
510pub struct EvaluationTerm {
511 pub term_id: String,
512 pub contribution: i64,
513 pub evidence: Vec<EvidenceRef>,
515}
516
517#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
526pub struct EvaluationTraceRecord {
527 pub rule_id: String,
528 pub rule_version: String,
529 pub subject: EntityRef,
530 pub terms: Vec<EvaluationTerm>,
531 pub result: i64,
532 pub boundary: BoundaryId,
534}
535
536#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
539pub struct DeterministicRng {
540 state: u64,
541}
542
543impl DeterministicRng {
544 const STEP: u64 = 0x9E37_79B9_7F4A_7C15;
545
546 #[must_use]
547 pub const fn from_seed(seed: u64) -> Self {
548 Self { state: seed }
549 }
550
551 #[must_use]
552 pub const fn state(self) -> u64 {
553 self.state
554 }
555
556 #[must_use]
557 pub const fn state_after(seed: u64, draws: u64) -> u64 {
558 seed.wrapping_add(Self::STEP.wrapping_mul(draws))
559 }
560
561 #[must_use]
562 pub const fn seed_before(state: u64, draws: u64) -> u64 {
563 state.wrapping_sub(Self::STEP.wrapping_mul(draws))
564 }
565
566 pub fn next_u64(&mut self) -> u64 {
567 self.state = self.state.wrapping_add(Self::STEP);
568 let mut value = self.state;
569 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
570 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
571 value ^ (value >> 31)
572 }
573
574 pub fn range(&mut self, upper_exclusive: u64) -> u64 {
579 if upper_exclusive == 0 {
580 return 0;
581 }
582 let rejection_threshold = upper_exclusive.wrapping_neg() % upper_exclusive;
583 loop {
584 let value = self.next_u64();
585 if value >= rejection_threshold {
586 return value % upper_exclusive;
587 }
588 }
589 }
590
591 #[must_use]
594 pub fn range_modulo(&mut self, upper_exclusive: u64) -> u64 {
595 if upper_exclusive == 0 {
596 return 0;
597 }
598 self.next_u64() % upper_exclusive
599 }
600}
601
602#[derive(Clone, Debug, Eq, PartialEq)]
604pub enum SchemaRegistryError {
605 DuplicateType(String),
606}
607
608impl Display for SchemaRegistryError {
609 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
610 match self {
611 Self::DuplicateType(type_name) => {
612 write!(formatter, "schema type {type_name} is already registered")
613 }
614 }
615 }
616}
617
618impl std::error::Error for SchemaRegistryError {}
619
620#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
621pub struct FieldSchema {
622 pub name: String,
623 pub value_type: String,
624 pub description: String,
625 pub reference_type: Option<String>,
626 pub writable_via_debug_command: bool,
627}
628
629#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
630pub struct TypeSchema {
631 pub type_name: String,
632 pub description: String,
633 pub fields: Vec<FieldSchema>,
634}
635
636#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
637pub struct SchemaRegistry {
638 types: BTreeMap<String, TypeSchema>,
639}
640
641impl SchemaRegistry {
642 pub fn register(&mut self, schema: TypeSchema) -> Result<(), SchemaRegistryError> {
652 if let Some(existing) = self.types.get(&schema.type_name) {
653 if existing == &schema {
654 return Ok(());
655 }
656 return Err(SchemaRegistryError::DuplicateType(schema.type_name));
657 }
658 self.types.insert(schema.type_name.clone(), schema);
659 Ok(())
660 }
661
662 #[must_use]
663 pub fn get(&self, type_name: &str) -> Option<&TypeSchema> {
664 self.types.get(type_name)
665 }
666
667 pub fn iter(&self) -> impl Iterator<Item = &TypeSchema> {
668 self.types.values()
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 struct Office;
677
678 impl DomainRecordType for Office {
679 type Payload = String;
680 type Class = DomainEntityKindClass;
681
682 const NAMESPACE: &'static str = "fixture.governance";
683 const NAME: &'static str = "office";
684 }
685
686 struct Obligation;
687
688 impl DomainRecordType for Obligation {
689 type Payload = String;
690 type Class = DomainValueKindClass;
691
692 const NAMESPACE: &'static str = "fixture.governance";
693 const NAME: &'static str = "obligation";
694 }
695
696 struct Assessment;
697
698 impl KnowledgeRecordType for Assessment {
699 type Payload = String;
700
701 const NAMESPACE: &'static str = "fixture.knowledge";
702 const NAME: &'static str = "assessment";
703 const SCHEMA_VERSION: u32 = 2;
704 }
705
706 #[test]
707 fn typed_domain_identity_preserves_wire_shape_and_kind_boundary() {
708 let typed = TypedDomainRecordRef::<Office>::new("secretariat");
709 let raw = DomainRecordRef::new("fixture.governance", "office", "secretariat");
710
711 assert_eq!(
712 serde_json::to_value(&typed).expect("typed identity should serialize"),
713 serde_json::to_value(&raw).expect("raw identity should serialize")
714 );
715 let round_trip: TypedDomainRecordRef<Office> = serde_json::from_value(
716 serde_json::to_value(&typed).expect("typed identity should serialize"),
717 )
718 .expect("typed identity should deserialize");
719 assert_eq!(round_trip.as_untyped(), &raw);
720 assert_eq!(EntityRef::from(round_trip), EntityRef::Domain(raw.clone()));
721
722 let wrong_kind =
723 DomainRecordRef::new("fixture.governance", "obligation", "secretariat-duty");
724 assert_eq!(
725 TypedDomainRecordRef::<Office>::from_untyped(wrong_kind.clone()),
726 Err(wrong_kind)
727 );
728 assert!(TypedDomainRecordRef::<Obligation>::from_untyped(raw).is_err());
729 assert!(
730 serde_json::from_value::<TypedDomainRecordRef<Office>>(serde_json::json!({
731 "kind": {
732 "namespace": "fixture.governance",
733 "name": "obligation"
734 },
735 "id": "secretariat-duty"
736 }))
737 .is_err()
738 );
739 }
740
741 #[test]
742 fn knowledge_identity_and_holder_wire_shapes_are_stable() {
743 let kind = KnowledgeRecordKind::new(Assessment::NAMESPACE, Assessment::NAME);
744 let schema = KnowledgeSchemaId::new(kind.clone(), Assessment::SCHEMA_VERSION);
745
746 assert_eq!(kind.to_string(), "fixture.knowledge.assessment");
747 assert_eq!(schema.version, 2);
748 assert_eq!(schema.kind, kind);
749 assert_eq!(
750 KnowledgeHolderPolicy::default(),
751 KnowledgeHolderPolicy::Disallowed
752 );
753
754 assert_eq!(
755 serde_json::to_value(KnowledgeHolderRef::Person(PersonId::new(7)))
756 .expect("person holder should serialize"),
757 serde_json::json!({ "type": "person", "value": 7 })
758 );
759 let invalid_shape = KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(7)));
760 assert!(invalid_shape.is_person_entity());
761 let institution =
762 KnowledgeHolderRef::Entity(EntityRef::Organization(OrganizationId::new(3)));
763 assert!(!institution.is_person_entity());
764 assert_eq!(
765 serde_json::to_value(institution).expect("institution holder should serialize"),
766 serde_json::json!({
767 "type": "entity",
768 "value": { "type": "organization", "id": 3 }
769 })
770 );
771 }
772
773 #[test]
774 fn exact_domain_record_evidence_has_a_stable_wire_identity() {
775 let evidence = EvidenceRef::DomainRecordVersion(DomainRecordVersionRef {
776 record: DomainRecordRef::new("fixture.information", "dispatch", "dispatch-7"),
777 version: 2,
778 established_by: DomainRecordVersionSource::BoundaryChange {
779 boundary: BoundaryId::new(12),
780 change_index: 3,
781 },
782 });
783
784 assert_eq!(
785 serde_json::to_value(evidence).expect("evidence should serialize"),
786 serde_json::json!({
787 "type": "domain_record_version",
788 "value": {
789 "record": {
790 "kind": {
791 "namespace": "fixture.information",
792 "name": "dispatch"
793 },
794 "id": "dispatch-7"
795 },
796 "version": 2,
797 "established_by": {
798 "type": "boundary_change",
799 "boundary": 12,
800 "change_index": 3
801 }
802 }
803 })
804 );
805 }
806
807 #[test]
808 fn deterministic_range_stays_bounded_for_non_power_of_two_limits() {
809 for seed in 0..32 {
810 let mut rng = DeterministicRng::from_seed(seed);
811 assert_eq!(rng.range(0), 0);
812 for _ in 0..128 {
813 assert!(rng.range(7) < 7);
814 }
815 }
816 }
817
818 #[test]
819 fn schema_registration_is_idempotent_but_rejects_definition_replacement() {
820 let schema = TypeSchema {
821 type_name: "fixture.office".to_owned(),
822 description: "An office".to_owned(),
823 fields: vec![FieldSchema {
824 name: "name".to_owned(),
825 value_type: "string".to_owned(),
826 description: "The office name".to_owned(),
827 reference_type: None,
828 writable_via_debug_command: false,
829 }],
830 };
831 let mut registry = SchemaRegistry::default();
832 registry
833 .register(schema.clone())
834 .expect("the first schema registration should succeed");
835 registry
836 .register(schema.clone())
837 .expect("an identical schema registration should be idempotent");
838
839 let mut conflicting = schema;
840 conflicting.description = "A different office".to_owned();
841 assert_eq!(
842 registry
843 .register(conflicting)
844 .expect_err("a different definition must not replace the registered schema"),
845 SchemaRegistryError::DuplicateType("fixture.office".to_owned())
846 );
847 }
848}