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