Skip to main content

canwu_core/
lib.rs

1//! Stable identifiers, deterministic utilities, and lightweight schema metadata.
2
3use 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!(EventId);
54define_id!(GovernmentId);
55define_id!(IngressId);
56define_id!(OrganizationId);
57define_id!(PersonId);
58define_id!(RandomDrawId);
59define_id!(ResourceId);
60define_id!(RouteId);
61define_id!(TerritoryId);
62
63/// Stable application-defined record kind. Namespaces and names are validated
64/// by the simulation package registry before authoritative use.
65#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
66pub struct DomainRecordKind {
67    pub namespace: String,
68    pub name: String,
69}
70
71impl DomainRecordKind {
72    #[must_use]
73    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
74        Self {
75            namespace: namespace.into(),
76            name: name.into(),
77        }
78    }
79
80    #[must_use]
81    pub fn for_type<T: DomainRecordType>() -> Self {
82        Self::new(T::NAMESPACE, T::NAME)
83    }
84
85    #[must_use]
86    pub fn matches_type<T: DomainRecordType>(&self) -> bool {
87        self.namespace == T::NAMESPACE && self.name == T::NAME
88    }
89}
90
91impl Display for DomainRecordKind {
92    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
93        write!(formatter, "{}.{}", self.namespace, self.name)
94    }
95}
96
97/// Stable string identity for an application-defined entity or record.
98#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
99pub struct DomainRecordRef {
100    pub kind: DomainRecordKind,
101    pub id: String,
102}
103
104impl DomainRecordRef {
105    #[must_use]
106    pub fn new(
107        namespace: impl Into<String>,
108        kind: impl Into<String>,
109        id: impl Into<String>,
110    ) -> Self {
111        Self {
112            kind: DomainRecordKind::new(namespace, kind),
113            id: id.into(),
114        }
115    }
116}
117
118impl Display for DomainRecordRef {
119    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
120        write!(formatter, "{}:{}", self.kind, self.id)
121    }
122}
123
124/// Compile-time identity for one namespaced application-defined record kind.
125///
126/// The associated payload stays outside the kernel's type graph. Domain
127/// packages use this trait to bind stable identities and payload codecs while
128/// Canwu persists the existing schema-validated [`DomainRecordRef`] shape.
129pub trait DomainRecordType {
130    type Payload;
131    type Class: DomainKindClass;
132
133    const NAMESPACE: &'static str;
134    const NAME: &'static str;
135}
136
137mod domain_kind_class {
138    pub trait Sealed {}
139}
140
141/// Sealed type-level classification for application-defined record kinds.
142pub trait DomainKindClass: domain_kind_class::Sealed {
143    const IS_ENTITY: bool;
144}
145
146/// Type-level class for domain kinds whose instances are entity identities.
147pub enum DomainEntityKindClass {}
148
149impl domain_kind_class::Sealed for DomainEntityKindClass {}
150
151impl DomainKindClass for DomainEntityKindClass {
152    const IS_ENTITY: bool = true;
153}
154
155/// Type-level class for domain kinds whose instances are non-entity records.
156pub enum DomainValueKindClass {}
157
158impl domain_kind_class::Sealed for DomainValueKindClass {}
159
160impl DomainKindClass for DomainValueKindClass {
161    const IS_ENTITY: bool = false;
162}
163
164/// Marker implemented automatically for entity-class domain record types.
165pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
166
167impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
168
169/// Marker implemented automatically for non-entity domain record types.
170pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
171
172impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
173
174/// Typed façade over a stable application-defined record identity.
175///
176/// Its serialized representation is exactly the wrapped [`DomainRecordRef`];
177/// the marker exists only at compile time.
178#[derive(Serialize)]
179#[serde(transparent, bound = "")]
180pub struct TypedDomainRecordRef<T: DomainRecordType> {
181    reference: DomainRecordRef,
182    #[serde(skip)]
183    marker: PhantomData<fn() -> T>,
184}
185
186impl<T: DomainRecordType> Clone for TypedDomainRecordRef<T> {
187    fn clone(&self) -> Self {
188        Self {
189            reference: self.reference.clone(),
190            marker: PhantomData,
191        }
192    }
193}
194
195impl<T: DomainRecordType> std::fmt::Debug for TypedDomainRecordRef<T> {
196    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
197        formatter
198            .debug_tuple("TypedDomainRecordRef")
199            .field(&self.reference)
200            .finish()
201    }
202}
203
204impl<T: DomainRecordType> PartialEq for TypedDomainRecordRef<T> {
205    fn eq(&self, other: &Self) -> bool {
206        self.reference == other.reference
207    }
208}
209
210impl<T: DomainRecordType> Eq for TypedDomainRecordRef<T> {}
211
212impl<T: DomainRecordType> PartialOrd for TypedDomainRecordRef<T> {
213    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
214        Some(self.cmp(other))
215    }
216}
217
218impl<T: DomainRecordType> Ord for TypedDomainRecordRef<T> {
219    fn cmp(&self, other: &Self) -> Ordering {
220        self.reference.cmp(&other.reference)
221    }
222}
223
224impl<T: DomainRecordType> Hash for TypedDomainRecordRef<T> {
225    fn hash<H: Hasher>(&self, state: &mut H) {
226        self.reference.hash(state);
227    }
228}
229
230impl<'de, T: DomainRecordType> Deserialize<'de> for TypedDomainRecordRef<T> {
231    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
232    where
233        D: serde::Deserializer<'de>,
234    {
235        let reference = DomainRecordRef::deserialize(deserializer)?;
236        Self::from_untyped(reference).map_err(|reference| {
237            serde::de::Error::custom(format!(
238                "domain record reference {reference} does not match typed kind {}",
239                DomainRecordKind::for_type::<T>()
240            ))
241        })
242    }
243}
244
245impl<T: DomainRecordType> TypedDomainRecordRef<T> {
246    #[must_use]
247    pub fn new(id: impl Into<String>) -> Self {
248        Self {
249            reference: DomainRecordRef {
250                kind: DomainRecordKind::for_type::<T>(),
251                id: id.into(),
252            },
253            marker: PhantomData,
254        }
255    }
256
257    #[must_use]
258    pub const fn as_untyped(&self) -> &DomainRecordRef {
259        &self.reference
260    }
261
262    #[must_use]
263    pub fn into_untyped(self) -> DomainRecordRef {
264        self.reference
265    }
266
267    /// Converts an untyped reference when its namespaced kind matches `T`.
268    ///
269    /// # Errors
270    ///
271    /// Returns the original reference when it belongs to another kind.
272    pub fn from_untyped(reference: DomainRecordRef) -> Result<Self, DomainRecordRef> {
273        if !reference.kind.matches_type::<T>() {
274            return Err(reference);
275        }
276        Ok(Self {
277            reference,
278            marker: PhantomData,
279        })
280    }
281
282    #[must_use]
283    pub fn id(&self) -> &str {
284        &self.reference.id
285    }
286}
287
288impl<T: DomainRecordType> Display for TypedDomainRecordRef<T> {
289    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
290        Display::fmt(&self.reference, formatter)
291    }
292}
293
294impl<T: DomainRecordType> From<TypedDomainRecordRef<T>> for DomainRecordRef {
295    fn from(reference: TypedDomainRecordRef<T>) -> Self {
296        reference.into_untyped()
297    }
298}
299
300impl<T: DomainEntityType> From<TypedDomainRecordRef<T>> for EntityRef {
301    fn from(reference: TypedDomainRecordRef<T>) -> Self {
302        Self::Domain(reference.into_untyped())
303    }
304}
305
306#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
307#[serde(rename_all = "snake_case")]
308pub enum CoreEntityKind {
309    Army,
310    Government,
311    Organization,
312    Person,
313    Resource,
314    Route,
315    Territory,
316}
317
318/// Serializable entity reference used by events, queries, and generic tools.
319#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
320#[serde(tag = "type", content = "id", rename_all = "snake_case")]
321pub enum EntityRef {
322    Army(ArmyId),
323    Domain(DomainRecordRef),
324    Government(GovernmentId),
325    Organization(OrganizationId),
326    Person(PersonId),
327    Resource(ResourceId),
328    Route(RouteId),
329    Territory(TerritoryId),
330}
331
332impl Display for EntityRef {
333    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
334        match self {
335            Self::Army(id) => write!(formatter, "army:{id}"),
336            Self::Domain(reference) => write!(formatter, "domain:{reference}"),
337            Self::Government(id) => write!(formatter, "government:{id}"),
338            Self::Organization(id) => write!(formatter, "organization:{id}"),
339            Self::Person(id) => write!(formatter, "person:{id}"),
340            Self::Resource(id) => write!(formatter, "resource:{id}"),
341            Self::Route(id) => write!(formatter, "route:{id}"),
342            Self::Territory(id) => write!(formatter, "territory:{id}"),
343        }
344    }
345}
346
347impl EntityRef {
348    #[must_use]
349    pub const fn core_kind(&self) -> Option<CoreEntityKind> {
350        match self {
351            Self::Army(_) => Some(CoreEntityKind::Army),
352            Self::Domain(_) => None,
353            Self::Government(_) => Some(CoreEntityKind::Government),
354            Self::Organization(_) => Some(CoreEntityKind::Organization),
355            Self::Person(_) => Some(CoreEntityKind::Person),
356            Self::Resource(_) => Some(CoreEntityKind::Resource),
357            Self::Route(_) => Some(CoreEntityKind::Route),
358            Self::Territory(_) => Some(CoreEntityKind::Territory),
359        }
360    }
361}
362
363/// `SplitMix64` is compact, deterministic, serializable, and sufficient for the
364/// initial movement slice.
365#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
366pub struct DeterministicRng {
367    state: u64,
368}
369
370impl DeterministicRng {
371    const STEP: u64 = 0x9E37_79B9_7F4A_7C15;
372
373    #[must_use]
374    pub const fn from_seed(seed: u64) -> Self {
375        Self { state: seed }
376    }
377
378    #[must_use]
379    pub const fn state(self) -> u64 {
380        self.state
381    }
382
383    #[must_use]
384    pub const fn state_after(seed: u64, draws: u64) -> u64 {
385        seed.wrapping_add(Self::STEP.wrapping_mul(draws))
386    }
387
388    #[must_use]
389    pub const fn seed_before(state: u64, draws: u64) -> u64 {
390        state.wrapping_sub(Self::STEP.wrapping_mul(draws))
391    }
392
393    pub fn next_u64(&mut self) -> u64 {
394        self.state = self.state.wrapping_add(Self::STEP);
395        let mut value = self.state;
396        value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
397        value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
398        value ^ (value >> 31)
399    }
400
401    /// Returns a value in `[0, upper_exclusive)`. Zero returns zero.
402    pub fn range(&mut self, upper_exclusive: u64) -> u64 {
403        if upper_exclusive == 0 {
404            return 0;
405        }
406        self.next_u64() % upper_exclusive
407    }
408}
409
410#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
411pub struct FieldSchema {
412    pub name: String,
413    pub value_type: String,
414    pub description: String,
415    pub reference_type: Option<String>,
416    pub writable_via_debug_command: bool,
417}
418
419#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
420pub struct TypeSchema {
421    pub type_name: String,
422    pub description: String,
423    pub fields: Vec<FieldSchema>,
424}
425
426#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
427pub struct SchemaRegistry {
428    types: BTreeMap<String, TypeSchema>,
429}
430
431impl SchemaRegistry {
432    pub fn register(&mut self, schema: TypeSchema) {
433        self.types.insert(schema.type_name.clone(), schema);
434    }
435
436    #[must_use]
437    pub fn get(&self, type_name: &str) -> Option<&TypeSchema> {
438        self.types.get(type_name)
439    }
440
441    pub fn iter(&self) -> impl Iterator<Item = &TypeSchema> {
442        self.types.values()
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    struct Office;
451
452    impl DomainRecordType for Office {
453        type Payload = String;
454        type Class = DomainEntityKindClass;
455
456        const NAMESPACE: &'static str = "fixture.governance";
457        const NAME: &'static str = "office";
458    }
459
460    struct Obligation;
461
462    impl DomainRecordType for Obligation {
463        type Payload = String;
464        type Class = DomainValueKindClass;
465
466        const NAMESPACE: &'static str = "fixture.governance";
467        const NAME: &'static str = "obligation";
468    }
469
470    #[test]
471    fn typed_domain_identity_preserves_wire_shape_and_kind_boundary() {
472        let typed = TypedDomainRecordRef::<Office>::new("secretariat");
473        let raw = DomainRecordRef::new("fixture.governance", "office", "secretariat");
474
475        assert_eq!(
476            serde_json::to_value(&typed).expect("typed identity should serialize"),
477            serde_json::to_value(&raw).expect("raw identity should serialize")
478        );
479        let round_trip: TypedDomainRecordRef<Office> = serde_json::from_value(
480            serde_json::to_value(&typed).expect("typed identity should serialize"),
481        )
482        .expect("typed identity should deserialize");
483        assert_eq!(round_trip.as_untyped(), &raw);
484        assert_eq!(EntityRef::from(round_trip), EntityRef::Domain(raw.clone()));
485
486        let wrong_kind =
487            DomainRecordRef::new("fixture.governance", "obligation", "secretariat-duty");
488        assert_eq!(
489            TypedDomainRecordRef::<Office>::from_untyped(wrong_kind.clone()),
490            Err(wrong_kind)
491        );
492        assert!(TypedDomainRecordRef::<Obligation>::from_untyped(raw).is_err());
493        assert!(
494            serde_json::from_value::<TypedDomainRecordRef<Office>>(serde_json::json!({
495                "kind": {
496                    "namespace": "fixture.governance",
497                    "name": "obligation"
498                },
499                "id": "secretariat-duty"
500            }))
501            .is_err()
502        );
503    }
504}