Skip to main content

canwu_event/
lib.rs

1//! Inspectable, serializable events with compact causal provenance.
2
3use canwu_core::{BoundaryId, CommandId, EntityRef, EventId, KnowledgeHolderRef, PersonId};
4use canwu_time::SimTime;
5use serde::{
6    Deserialize, Deserializer, Serialize, Serializer,
7    de::{self, DeserializeOwned, MapAccess, SeqAccess, Visitor},
8    ser::SerializeMap,
9};
10use serde_json::{Number, Value};
11use std::collections::BTreeMap;
12use std::error::Error;
13use std::fmt::{self, Display, Formatter};
14
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(tag = "type", content = "id", rename_all = "snake_case")]
17pub enum CauseRef {
18    Boundary(BoundaryId),
19    Command(CommandId),
20    Event(EventId),
21    System(String),
22}
23
24/// Domain-neutral event identity and structured fields.
25///
26/// The `type` tag and flattened field layout deliberately preserve the wire
27/// shape used by earlier concrete event variants. Domain crates and runtime
28/// compatibility modules own typed payloads and encode them through
29/// [`Self::from_payload`]; this crate does not own their vocabulary.
30#[derive(Clone, Debug)]
31pub struct EventKind {
32    event_type: String,
33    fields: BTreeMap<String, EventValue>,
34}
35
36#[derive(Clone, Debug)]
37enum EventValue {
38    Null,
39    Bool(bool),
40    Number(Number),
41    String(String),
42    Array(Vec<Self>),
43    Object(BTreeMap<String, Self>),
44}
45
46impl PartialEq for EventValue {
47    fn eq(&self, other: &Self) -> bool {
48        match (self, other) {
49            (Self::Null, Self::Null) => true,
50            (Self::Bool(left), Self::Bool(right)) => left == right,
51            (Self::Number(left), Self::Number(right)) => left == right,
52            (Self::String(left), Self::String(right)) => left == right,
53            (Self::Array(left), Self::Array(right)) => left == right,
54            (Self::Object(left), Self::Object(right)) => left == right,
55            _ => false,
56        }
57    }
58}
59
60impl Eq for EventValue {}
61
62impl PartialEq for EventKind {
63    fn eq(&self, other: &Self) -> bool {
64        self.event_type == other.event_type && self.fields == other.fields
65    }
66}
67
68impl Eq for EventKind {}
69
70impl EventValue {
71    fn from_json(value: Value) -> Self {
72        match value {
73            Value::Null => Self::Null,
74            Value::Bool(value) => Self::Bool(value),
75            Value::Number(value) => Self::Number(value),
76            Value::String(value) => Self::String(value),
77            Value::Array(values) => Self::Array(values.into_iter().map(Self::from_json).collect()),
78            Value::Object(fields) => Self::Object(
79                fields
80                    .into_iter()
81                    .map(|(key, value)| (key, Self::from_json(value)))
82                    .collect(),
83            ),
84        }
85    }
86
87    fn into_json(self) -> Value {
88        match self {
89            Self::Null => Value::Null,
90            Self::Bool(value) => Value::Bool(value),
91            Self::Number(value) => Value::Number(value),
92            Self::String(value) => Value::String(value),
93            Self::Array(values) => Value::Array(values.into_iter().map(Self::into_json).collect()),
94            Self::Object(fields) => Value::Object(
95                fields
96                    .into_iter()
97                    .map(|(key, value)| (key, value.into_json()))
98                    .collect(),
99            ),
100        }
101    }
102
103    fn as_str(&self) -> Option<&str> {
104        match self {
105            Self::String(value) => Some(value),
106            _ => None,
107        }
108    }
109}
110
111impl Serialize for EventValue {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: Serializer,
115    {
116        match self {
117            Self::Null => serializer.serialize_none(),
118            Self::Bool(value) => serializer.serialize_bool(*value),
119            Self::Number(value) => value.serialize(serializer),
120            Self::String(value) => serializer.serialize_str(value),
121            Self::Array(values) => values.serialize(serializer),
122            Self::Object(fields) => {
123                let mut map = serializer.serialize_map(Some(fields.len()))?;
124                for (key, value) in fields {
125                    map.serialize_entry(key, value)?;
126                }
127                map.end()
128            }
129        }
130    }
131}
132
133impl<'de> Deserialize<'de> for EventValue {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: Deserializer<'de>,
137    {
138        struct EventValueVisitor;
139
140        impl<'de> Visitor<'de> for EventValueVisitor {
141            type Value = EventValue;
142
143            fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
144                formatter.write_str("a JSON-compatible event field value")
145            }
146
147            fn visit_unit<E>(self) -> Result<Self::Value, E> {
148                Ok(EventValue::Null)
149            }
150
151            fn visit_none<E>(self) -> Result<Self::Value, E> {
152                Ok(EventValue::Null)
153            }
154
155            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
156            where
157                D: Deserializer<'de>,
158            {
159                EventValue::deserialize(deserializer)
160            }
161
162            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
163                Ok(EventValue::Bool(value))
164            }
165
166            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
167                Ok(EventValue::Number(value.into()))
168            }
169
170            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
171                Ok(EventValue::Number(value.into()))
172            }
173
174            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
175            where
176                E: de::Error,
177            {
178                Number::from_f64(value)
179                    .map(EventValue::Number)
180                    .ok_or_else(|| E::custom("event field contains a non-finite number"))
181            }
182
183            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
184                Ok(EventValue::String(value.to_owned()))
185            }
186
187            fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
188                Ok(EventValue::String(value))
189            }
190
191            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
192            where
193                A: SeqAccess<'de>,
194            {
195                let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
196                while let Some(value) = sequence.next_element()? {
197                    values.push(value);
198                }
199                Ok(EventValue::Array(values))
200            }
201
202            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
203            where
204                A: MapAccess<'de>,
205            {
206                let mut fields = BTreeMap::new();
207                while let Some(key) = map.next_key::<String>()? {
208                    if fields.insert(key.clone(), map.next_value()?).is_some() {
209                        return Err(de::Error::custom(format!(
210                            "event field object contains duplicate key {key}"
211                        )));
212                    }
213                }
214                Ok(EventValue::Object(fields))
215            }
216        }
217
218        deserializer.deserialize_any(EventValueVisitor)
219    }
220}
221
222impl Serialize for EventKind {
223    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
224    where
225        S: Serializer,
226    {
227        let mut map = serializer.serialize_map(Some(self.fields.len() + 1))?;
228        map.serialize_entry("type", &self.event_type)?;
229        for (key, value) in &self.fields {
230            map.serialize_entry(key, value)?;
231        }
232        map.end()
233    }
234}
235
236impl<'de> Deserialize<'de> for EventKind {
237    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238    where
239        D: Deserializer<'de>,
240    {
241        struct EventKindVisitor;
242
243        impl<'de> Visitor<'de> for EventKindVisitor {
244            type Value = EventKind;
245
246            fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
247                formatter.write_str("an event object with a type field")
248            }
249
250            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
251            where
252                A: MapAccess<'de>,
253            {
254                let mut event_type = None;
255                let mut fields = BTreeMap::new();
256                while let Some(key) = map.next_key::<String>()? {
257                    if key == "type" {
258                        if event_type.is_some() {
259                            return Err(de::Error::duplicate_field("type"));
260                        }
261                        event_type = Some(map.next_value::<String>()?);
262                    } else if fields.insert(key.clone(), map.next_value()?).is_some() {
263                        return Err(de::Error::custom(format!(
264                            "event payload contains duplicate field {key}"
265                        )));
266                    }
267                }
268                let event_type = event_type.ok_or_else(|| de::Error::missing_field("type"))?;
269                if event_type.is_empty() {
270                    return Err(de::Error::custom("event type cannot be empty"));
271                }
272                Ok(EventKind { event_type, fields })
273            }
274        }
275
276        deserializer.deserialize_map(EventKindVisitor)
277    }
278}
279
280#[derive(Deserialize)]
281struct OrderedFields(
282    #[serde(deserialize_with = "deserialize_ordered_fields")] Vec<(String, EventValue)>,
283);
284
285fn deserialize_ordered_fields<'de, D>(
286    deserializer: D,
287) -> Result<Vec<(String, EventValue)>, D::Error>
288where
289    D: Deserializer<'de>,
290{
291    struct OrderedFieldsVisitor;
292
293    impl<'de> Visitor<'de> for OrderedFieldsVisitor {
294        type Value = Vec<(String, EventValue)>;
295
296        fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
297            formatter.write_str("an event payload object")
298        }
299
300        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
301        where
302            A: MapAccess<'de>,
303        {
304            let mut fields = Vec::with_capacity(map.size_hint().unwrap_or(0));
305            while let Some(key) = map.next_key::<String>()? {
306                if fields.iter().any(|(existing, _)| existing == &key) {
307                    return Err(de::Error::custom(format!(
308                        "event payload contains duplicate field {key}"
309                    )));
310                }
311                fields.push((key, map.next_value()?));
312            }
313            Ok(fields)
314        }
315    }
316
317    deserializer.deserialize_map(OrderedFieldsVisitor)
318}
319
320#[derive(Debug)]
321pub enum EventKindError {
322    EmptyEventType,
323    ReservedTypeField,
324    PayloadMustBeObject,
325    MissingField(String),
326    UnexpectedEventType {
327        expected: &'static str,
328        actual: String,
329    },
330    InvalidPayload(serde_json::Error),
331}
332
333impl Display for EventKindError {
334    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
335        match self {
336            Self::EmptyEventType => formatter.write_str("event type cannot be empty"),
337            Self::ReservedTypeField => {
338                formatter.write_str("event payload cannot contain the reserved type field")
339            }
340            Self::PayloadMustBeObject => {
341                formatter.write_str("event payload must serialize as an object")
342            }
343            Self::MissingField(field) => {
344                write!(formatter, "event payload is missing field {field}")
345            }
346            Self::UnexpectedEventType { expected, actual } => {
347                write!(formatter, "expected event type {expected}, found {actual}")
348            }
349            Self::InvalidPayload(error) => Display::fmt(error, formatter),
350        }
351    }
352}
353
354impl Error for EventKindError {
355    fn source(&self) -> Option<&(dyn Error + 'static)> {
356        match self {
357            Self::InvalidPayload(error) => Some(error),
358            Self::EmptyEventType
359            | Self::ReservedTypeField
360            | Self::PayloadMustBeObject
361            | Self::MissingField(_)
362            | Self::UnexpectedEventType { .. } => None,
363        }
364    }
365}
366
367impl From<serde_json::Error> for EventKindError {
368    fn from(error: serde_json::Error) -> Self {
369        Self::InvalidPayload(error)
370    }
371}
372
373/// Declarative audience for a persisted event projection.
374///
375/// This is intentionally separate from plugin-to-plugin dispatch permissions:
376/// it controls only whether a trusted viewer may receive the event through a
377/// player-facing observation projection. `Private` is the safe default for
378/// plugin events that do not declare an audience.
379#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
380#[serde(tag = "type", content = "value", rename_all = "snake_case")]
381pub enum EventAudience {
382    Public,
383    Actor(PersonId),
384    Actors(Vec<PersonId>),
385    KnowledgeHolder(KnowledgeHolderRef),
386    /// The event is visible to actors represented by `EntityRef::Person` in
387    /// the event's affected-entity list.
388    AffectedActors,
389    #[default]
390    Private,
391}
392
393impl EventKind {
394    /// Builds an event record from an explicit type and flattened fields.
395    ///
396    /// # Errors
397    ///
398    /// Returns an error when the type is empty or the fields contain the
399    /// reserved `type` key.
400    pub fn from_fields(
401        event_type: impl Into<String>,
402        fields: BTreeMap<String, Value>,
403    ) -> Result<Self, EventKindError> {
404        let event_type = event_type.into();
405        if event_type.is_empty() {
406            return Err(EventKindError::EmptyEventType);
407        }
408        if fields.contains_key("type") {
409            return Err(EventKindError::ReservedTypeField);
410        }
411        Ok(Self {
412            event_type,
413            fields: fields
414                .into_iter()
415                .map(|(key, value)| (key, EventValue::from_json(value)))
416                .collect(),
417        })
418    }
419
420    /// Serializes a typed domain payload into an event record.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error when serialization fails, the payload is not an
425    /// object, the type is empty, or the payload contains the reserved `type`
426    /// key.
427    pub fn from_payload<T: Serialize>(
428        event_type: impl Into<String>,
429        payload: &T,
430    ) -> Result<Self, EventKindError> {
431        let encoded = serde_json::to_vec(payload)?;
432        let OrderedFields(fields) =
433            serde_json::from_slice(&encoded).map_err(|_| EventKindError::PayloadMustBeObject)?;
434        let event_type = event_type.into();
435        if event_type.is_empty() {
436            return Err(EventKindError::EmptyEventType);
437        }
438        if fields.iter().any(|(key, _)| key == "type") {
439            return Err(EventKindError::ReservedTypeField);
440        }
441        Ok(Self {
442            event_type,
443            fields: fields.into_iter().collect(),
444        })
445    }
446
447    /// Constructs the compatibility wire identity for a plugin event.
448    #[must_use]
449    pub fn plugin(plugin: impl Into<String>, event_type: impl Into<String>) -> Self {
450        Self {
451            event_type: "plugin".to_owned(),
452            fields: BTreeMap::from([
453                ("plugin".to_owned(), EventValue::String(plugin.into())),
454                (
455                    "event_type".to_owned(),
456                    EventValue::String(event_type.into()),
457                ),
458            ]),
459        }
460    }
461
462    #[must_use]
463    pub fn event_type(&self) -> &str {
464        &self.event_type
465    }
466
467    #[must_use]
468    pub fn is_type(&self, event_type: &str) -> bool {
469        self.event_type == event_type
470    }
471
472    #[must_use]
473    pub fn fields(&self) -> BTreeMap<String, Value> {
474        self.fields
475            .iter()
476            .map(|(key, value)| (key.clone(), value.clone().into_json()))
477            .collect()
478    }
479
480    #[must_use]
481    pub fn field(&self, name: &str) -> Option<Value> {
482        self.field_value(name).cloned().map(EventValue::into_json)
483    }
484
485    /// Replaces one existing field with a serializable value.
486    ///
487    /// # Errors
488    ///
489    /// Returns an error when the field is absent or the value cannot be
490    /// represented as structured JSON data.
491    pub fn set_field<T: Serialize>(&mut self, name: &str, value: &T) -> Result<(), EventKindError> {
492        let encoded = serde_json::to_vec(value)?;
493        let replacement = serde_json::from_slice::<EventValue>(&encoded)?;
494        let field = self
495            .fields
496            .get_mut(name)
497            .ok_or_else(|| EventKindError::MissingField(name.to_owned()))?;
498        *field = replacement;
499        Ok(())
500    }
501
502    /// Decodes all flattened fields into a domain-owned payload type.
503    ///
504    /// # Errors
505    ///
506    /// Returns an error when the fields do not deserialize as `T`.
507    pub fn decode_payload<T: DeserializeOwned>(&self) -> Result<T, EventKindError> {
508        struct Payload<'a>(&'a BTreeMap<String, EventValue>);
509
510        impl Serialize for Payload<'_> {
511            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
512            where
513                S: Serializer,
514            {
515                let mut map = serializer.serialize_map(Some(self.0.len()))?;
516                for (key, value) in self.0 {
517                    map.serialize_entry(key, value)?;
518                }
519                map.end()
520            }
521        }
522
523        let encoded = serde_json::to_vec(&Payload(&self.fields))?;
524        Ok(serde_json::from_slice(&encoded)?)
525    }
526
527    /// Decodes one named field.
528    ///
529    /// # Errors
530    ///
531    /// Returns an error when the field is absent or does not deserialize as
532    /// `T`.
533    pub fn decode_field<T: DeserializeOwned>(&self, name: &str) -> Result<T, EventKindError> {
534        let value = self
535            .field_value(name)
536            .ok_or_else(|| EventKindError::MissingField(name.to_owned()))?;
537        let encoded = serde_json::to_vec(value)?;
538        Ok(serde_json::from_slice(&encoded)?)
539    }
540
541    #[must_use]
542    pub fn plugin_identity(&self) -> Option<(&str, &str)> {
543        if self.event_type != "plugin" || self.fields.len() != 2 {
544            return None;
545        }
546        Some((
547            self.field_value("plugin")?.as_str()?,
548            self.field_value("event_type")?.as_str()?,
549        ))
550    }
551
552    /// Returns the stable, display-oriented type identity for this event.
553    ///
554    /// Built-in event kinds retain the same snake-case labels returned by
555    /// [`Self::event_type`]. Plugin events are qualified with their registered
556    /// plugin name and event type, separated by a dot. The two plugin-provided
557    /// components are preserved as registered; no case folding or additional
558    /// normalization is applied.
559    #[must_use]
560    pub fn qualified_event_type(&self) -> String {
561        self.plugin_identity().map_or_else(
562            || self.event_type.clone(),
563            |(plugin, event_type)| format!("{plugin}.{event_type}"),
564        )
565    }
566
567    fn field_value(&self, name: &str) -> Option<&EventValue> {
568        self.fields.get(name)
569    }
570}
571
572#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
573pub struct SimEvent {
574    pub id: EventId,
575    pub timestamp: SimTime,
576    pub kind: EventKind,
577    pub affected_entities: Vec<EntityRef>,
578    pub summary: String,
579    pub cause: Option<CauseRef>,
580    pub correlation_id: u64,
581}
582
583#[cfg(test)]
584mod tests {
585    use super::EventKind;
586    use canwu_core::{ArmyId, TerritoryId};
587    use serde::{Deserialize, Serialize, ser::SerializeMap};
588    use serde_json::json;
589
590    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
591    struct ArmyArrived {
592        army: ArmyId,
593        territory: TerritoryId,
594    }
595
596    struct DuplicateFields;
597
598    impl Serialize for DuplicateFields {
599        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600        where
601            S: serde::Serializer,
602        {
603            let mut map = serializer.serialize_map(Some(2))?;
604            map.serialize_entry("value", &1_u8)?;
605            map.serialize_entry("value", &2_u8)?;
606            map.end()
607        }
608    }
609
610    #[test]
611    fn qualified_event_type_preserves_legacy_labels_and_disambiguates_plugins() {
612        let payload = ArmyArrived {
613            army: ArmyId::new(1),
614            territory: TerritoryId::new(2),
615        };
616        let built_in = EventKind::from_payload("army_arrived", &payload).unwrap();
617        assert_eq!(built_in.event_type(), "army_arrived");
618        assert_eq!(built_in.qualified_event_type(), "army_arrived");
619        assert_eq!(built_in.decode_payload::<ArmyArrived>().unwrap(), payload);
620        assert_eq!(
621            serde_json::to_value(&built_in).unwrap(),
622            json!({"type": "army_arrived", "army": 1, "territory": 2})
623        );
624
625        let supply = EventKind::plugin("example-supply", "grain_allocated");
626        let demand = EventKind::plugin("example-demand", "grain_allocated");
627
628        assert_eq!(supply.event_type(), "plugin");
629        assert_eq!(
630            supply.qualified_event_type(),
631            "example-supply.grain_allocated"
632        );
633        assert_eq!(
634            demand.qualified_event_type(),
635            "example-demand.grain_allocated"
636        );
637        assert_ne!(supply.qualified_event_type(), demand.qualified_event_type());
638        assert_eq!(
639            serde_json::to_value(&supply).unwrap(),
640            json!({
641                "type": "plugin",
642                "plugin": "example-supply",
643                "event_type": "grain_allocated"
644            })
645        );
646        assert_eq!(
647            serde_json::to_string(&supply).unwrap(),
648            r#"{"type":"plugin","event_type":"grain_allocated","plugin":"example-supply"}"#
649        );
650    }
651
652    #[test]
653    fn typed_payload_and_field_replacement_reject_duplicate_serialized_keys() {
654        assert!(EventKind::from_payload("duplicate", &DuplicateFields).is_err());
655
656        let mut event = EventKind::from_fields(
657            "replacement",
658            std::collections::BTreeMap::from([("payload".to_owned(), json!({}))]),
659        )
660        .expect("event fixture should build");
661        assert!(event.set_field("payload", &DuplicateFields).is_err());
662    }
663
664    #[test]
665    fn event_wire_shapes_round_trip_in_canonical_field_order() {
666        let fixtures = [
667            r#"{"type":"move_ordered","army":1,"arrival_at":60,"from":2,"to":3}"#,
668            r#"{"type":"army_arrived","army":1,"territory":3}"#,
669            r#"{"type":"person_move_ordered","arrival_at":60,"from":2,"person":4,"to":3}"#,
670            r#"{"type":"person_arrived","person":4,"territory":3}"#,
671            r#"{"type":"letter_delivered","carrier":4,"letter":5,"recipient":6,"territory":3}"#,
672            r#"{"type":"report_dispatched","army":1,"arrives_at":90,"recipient":4}"#,
673            r#"{"type":"knowledge_updated","army":1,"known_location":3,"recipient":4}"#,
674            r#"{"type":"knowledge_published","holder":{"id":4,"type":"person"},"record_count":2}"#,
675            r#"{"type":"debug_field_changed","entity":{"id":1,"type":"army"},"field":"morale","new_value":"75","old_value":"70"}"#,
676            r#"{"type":"plugin","event_type":"changed","plugin":"example"}"#,
677        ];
678
679        for fixture in fixtures {
680            let event: EventKind = serde_json::from_str(fixture).unwrap();
681            assert_eq!(serde_json::to_string(&event).unwrap(), fixture);
682        }
683
684        let ordered: EventKind =
685            serde_json::from_str(r#"{"type":"army_arrived","army":1,"territory":3}"#).unwrap();
686        let reordered: EventKind =
687            serde_json::from_str(r#"{"territory":3,"army":1,"type":"army_arrived"}"#).unwrap();
688        assert_eq!(ordered, reordered);
689
690        let plugin_with_extra: EventKind = serde_json::from_str(
691            r#"{"type":"plugin","plugin":"example","event_type":"changed","extra":true}"#,
692        )
693        .unwrap();
694        assert_eq!(plugin_with_extra.plugin_identity(), None);
695    }
696}