Skip to main content

es_entity/
events.rs

1//! Manage events and related operations for event-sourcing.
2
3use chrono::{DateTime, Utc};
4
5use super::{
6    error::EntityHydrationError,
7    snapshot::{EsSnapshot, NoSnapshot, Replay, SnapshotRecord},
8    traits::*,
9};
10
11/// An alias for iterator over the persisted events
12pub type LastPersisted<'a, E> = std::slice::Iter<'a, PersistedEvent<E>>;
13
14/// Represent the events in raw deserialized format when loaded from database
15///
16/// Events in the database are stored as JSON blobs and loaded initially as `GenericEvents<Id>` where `Id`
17/// belongs to the entity the events is a part of. Acts a bridge between database model and
18/// domain model when later converted to the `PersistedEvent` type internally
19pub struct GenericEvent<Id> {
20    pub entity_id: Id,
21    pub sequence: i32,
22    pub event: serde_json::Value,
23    pub context: Option<crate::ContextData>,
24    pub recorded_at: DateTime<Utc>,
25    pub forgettable_payload: Option<serde_json::Value>,
26}
27
28/// Row type of every loader on a `snapshot` repo (aliased as `Repo__DbEvent`
29/// there). `GenericEvent<Id>` itself is untouched — every existing repo's
30/// `query_as!` and `.sqlx` entry stay as is; this confines the wider row
31/// shape to snapshot repos only.
32pub struct SnapshotGenericEvent<Id> {
33    pub entity_id: Id,
34    pub sequence: i32,
35    pub event: Option<serde_json::Value>,
36    pub context: Option<crate::ContextData>,
37    pub recorded_at: Option<DateTime<Utc>>,
38    pub forgettable_payload: Option<serde_json::Value>,
39    pub snapshot: Option<serde_json::Value>,
40    pub snapshot_sequence: Option<i32>,
41    pub snapshot_recorded_at: Option<DateTime<Utc>>,
42    pub snapshot_first_recorded_at: Option<DateTime<Utc>>,
43    pub snapshot_forgettable_payload: Option<serde_json::Value>,
44}
45
46/// Internal common shape loaders normalise into before decoding a container.
47#[doc(hidden)]
48pub struct HydrationRow<Id> {
49    pub entity_id: Id,
50    pub sequence: i32,
51    pub event: Option<serde_json::Value>,
52    pub context: Option<crate::ContextData>,
53    pub recorded_at: Option<DateTime<Utc>>,
54    pub forgettable_payload: Option<serde_json::Value>,
55    pub snapshot: Option<serde_json::Value>,
56    pub snapshot_sequence: Option<i32>,
57    pub snapshot_recorded_at: Option<DateTime<Utc>>,
58    pub snapshot_first_recorded_at: Option<DateTime<Utc>>,
59    pub snapshot_forgettable_payload: Option<serde_json::Value>,
60}
61
62impl<Id> From<GenericEvent<Id>> for HydrationRow<Id> {
63    fn from(e: GenericEvent<Id>) -> Self {
64        Self {
65            entity_id: e.entity_id,
66            sequence: e.sequence,
67            event: Some(e.event),
68            context: e.context,
69            recorded_at: Some(e.recorded_at),
70            forgettable_payload: e.forgettable_payload,
71            snapshot: None,
72            snapshot_sequence: None,
73            snapshot_recorded_at: None,
74            snapshot_first_recorded_at: None,
75            snapshot_forgettable_payload: None,
76        }
77    }
78}
79
80impl<Id> From<SnapshotGenericEvent<Id>> for HydrationRow<Id> {
81    fn from(e: SnapshotGenericEvent<Id>) -> Self {
82        Self {
83            entity_id: e.entity_id,
84            sequence: e.sequence,
85            event: e.event,
86            context: e.context,
87            recorded_at: e.recorded_at,
88            forgettable_payload: e.forgettable_payload,
89            snapshot: e.snapshot,
90            snapshot_sequence: e.snapshot_sequence,
91            snapshot_recorded_at: e.snapshot_recorded_at,
92            snapshot_first_recorded_at: e.snapshot_first_recorded_at,
93            snapshot_forgettable_payload: e.snapshot_forgettable_payload,
94        }
95    }
96}
97
98/// Strongly-typed event wrapper with metadata for successfully stored events.
99///
100/// Contains the event data along with persistence metadata (sequence, timestamp, entity_id).
101/// All `new_events` from [`EntityEvents`] are converted to this structure once persisted to construct
102/// entities, enabling event sourcing operations and other database operations.
103pub struct PersistedEvent<E: EsEvent> {
104    /// The identifier of the entity which the event is used to construct
105    pub entity_id: <E as EsEvent>::EntityId,
106    /// The timestamp which marks event persistence
107    pub recorded_at: DateTime<Utc>,
108    /// The sequence number of the event in the event stream
109    pub sequence: usize,
110    /// The event itself
111    pub event: E,
112    /// The context when the event was persisted
113    /// It is only popluated if 'event_context' set on EsEvent
114    pub context: Option<crate::ContextData>,
115}
116
117impl<E: Clone + EsEvent> Clone for PersistedEvent<E> {
118    fn clone(&self) -> Self {
119        PersistedEvent {
120            entity_id: self.entity_id.clone(),
121            recorded_at: self.recorded_at,
122            sequence: self.sequence,
123            event: self.event.clone(),
124            context: self.context.clone(),
125        }
126    }
127}
128
129pub struct EventWithContext<E: EsEvent> {
130    pub event: E,
131    pub context: Option<crate::ContextData>,
132}
133
134impl<E: Clone + EsEvent> Clone for EventWithContext<E> {
135    fn clone(&self) -> Self {
136        EventWithContext {
137            event: self.event.clone(),
138            context: self.context.clone(),
139        }
140    }
141}
142
143/// A [`Vec`] wrapper that manages event-stream of an entity with helpers for event-sourcing operations
144///
145/// Provides event sourcing operations for loading, appending, and persisting events in chronological
146/// sequence. Required field for all event-sourced entities to maintain their state change history.
147pub struct EntityEvents<T: EsEvent, S: EsSnapshot = NoSnapshot> {
148    /// The entity's id
149    pub entity_id: <T as EsEvent>::EntityId,
150    /// Sequence of the last event folded into `snapshot` (0 when none). The
151    /// tail (`persisted_events`) starts at `base_sequence + 1`.
152    base_sequence: usize,
153    /// The loaded snapshot, if any.
154    snapshot: Option<SnapshotRecord<S>>,
155    /// Events that have been persisted in database and marked (the tail,
156    /// after the snapshot).
157    persisted_events: Vec<PersistedEvent<T>>,
158    /// New events that are yet to be persisted to track state changes
159    new_events: Vec<EventWithContext<T>>,
160}
161
162impl<T: Clone + EsEvent, S: EsSnapshot + Clone> Clone for EntityEvents<T, S> {
163    fn clone(&self) -> Self {
164        Self {
165            entity_id: self.entity_id.clone(),
166            base_sequence: self.base_sequence,
167            snapshot: self.snapshot.clone(),
168            persisted_events: self.persisted_events.clone(),
169            new_events: self.new_events.clone(),
170        }
171    }
172}
173
174impl<T, S> EntityEvents<T, S>
175where
176    T: EsEvent,
177    S: EsSnapshot,
178{
179    /// Initializes a new `EntityEvents` instance with the given entity ID and initial events which is returned by [`IntoEvents`] method
180    pub fn init(id: <T as EsEvent>::EntityId, initial_events: impl IntoIterator<Item = T>) -> Self {
181        let context = if <T as EsEvent>::event_context() {
182            Some(crate::EventContext::data_for_storing())
183        } else {
184            None
185        };
186        let new_events = initial_events
187            .into_iter()
188            .map(|event| EventWithContext {
189                event,
190                context: context.clone(),
191            })
192            .collect();
193        Self {
194            entity_id: id,
195            base_sequence: 0,
196            snapshot: None,
197            persisted_events: Vec::new(),
198            new_events,
199        }
200    }
201
202    /// Returns a reference to the entity's identifier
203    pub fn id(&self) -> &<T as EsEvent>::EntityId {
204        &self.entity_id
205    }
206
207    /// Returns the timestamp of the first persisted event, indicating when the entity was created
208    pub fn entity_first_persisted_at(&self) -> Option<DateTime<Utc>> {
209        self.snapshot
210            .as_ref()
211            .map(|s| s.first_recorded_at)
212            .or_else(|| self.persisted_events.first().map(|e| e.recorded_at))
213    }
214
215    /// Returns the timestamp of the last persisted event, indicating when the entity was last modified
216    pub fn entity_last_modified_at(&self) -> Option<DateTime<Utc>> {
217        self.persisted_events
218            .last()
219            .map(|e| e.recorded_at)
220            .or_else(|| self.snapshot.as_ref().map(|s| s.recorded_at))
221    }
222
223    /// Appends a single new event to the entity's event stream to be persisted later
224    pub fn push(&mut self, event: T) {
225        let context = if <T as EsEvent>::event_context() {
226            Some(crate::EventContext::data_for_storing())
227        } else {
228            None
229        };
230        self.new_events.push(EventWithContext { event, context });
231    }
232
233    /// Appends multiple new events to the entity's event stream to be persisted later
234    pub fn extend(&mut self, events: impl IntoIterator<Item = T>) {
235        let context = if <T as EsEvent>::event_context() {
236            Some(crate::EventContext::data_for_storing())
237        } else {
238            None
239        };
240        self.new_events
241            .extend(events.into_iter().map(|event| EventWithContext {
242                event,
243                context: context.clone(),
244            }));
245    }
246
247    /// Returns true if there are any unpersisted events waiting to be saved
248    pub fn any_new(&self) -> bool {
249        !self.new_events.is_empty()
250    }
251
252    /// Returns the count of persisted events, including those folded into the
253    /// snapshot. This is the OCC offset used by every write path.
254    pub fn len_persisted(&self) -> usize {
255        self.base_sequence + self.persisted_events.len()
256    }
257
258    /// Returns the count of events after the snapshot: the persisted tail
259    /// plus any events staged for the write in flight. This is what
260    /// `HeadSnapshot::capture()` thresholds on.
261    pub fn tail_len(&self) -> usize {
262        self.persisted_events.len() + self.new_events.len()
263    }
264
265    #[doc(hidden)]
266    pub fn len_new(&self) -> usize {
267        self.new_events.len()
268    }
269
270    /// Returns the loaded snapshot, if any.
271    pub fn snapshot(&self) -> Option<&SnapshotRecord<S>> {
272        self.snapshot.as_ref()
273    }
274
275    /// Returns an iterator over the last `n` persisted events after the
276    /// snapshot, if any. This is what post-persist hooks receive.
277    pub fn last_persisted(&self, n: usize) -> LastPersisted<'_, T> {
278        let start = self.persisted_events.len().saturating_sub(n);
279        self.persisted_events[start..].iter()
280    }
281
282    /// Returns an iterator over the full replay: the snapshot (if any),
283    /// first, followed by the tail and any new events, in chronological
284    /// order.
285    pub fn replay(&self) -> impl DoubleEndedIterator<Item = Replay<'_, T, S>> + Clone {
286        self.snapshot
287            .iter()
288            .map(|r| Replay::Snapshot(&r.state))
289            .chain(
290                self.persisted_events
291                    .iter()
292                    .map(|e| Replay::Event(&e.event)),
293            )
294            .chain(self.new_events.iter().map(|e| Replay::Event(&e.event)))
295    }
296
297    /// Like [`replay`][Self::replay] but only over persisted state: the
298    /// snapshot record (if any) followed by the persisted tail. No new
299    /// events.
300    pub fn replay_persisted(
301        &self,
302    ) -> impl DoubleEndedIterator<Item = Replay<'_, PersistedEvent<T>, SnapshotRecord<S>>> + Clone
303    {
304        self.snapshot
305            .iter()
306            .map(Replay::Snapshot)
307            .chain(self.persisted_events.iter().map(Replay::Event))
308    }
309
310    /// Compacts the tail into a snapshot after a write that took one: the
311    /// persisted tail is dropped and replaced by the given state at the
312    /// current head. An entity in memory then looks exactly like a reload.
313    #[doc(hidden)]
314    pub fn compact_to_snapshot(
315        &mut self,
316        state: S,
317        recorded_at: DateTime<Utc>,
318        first_recorded_at: DateTime<Utc>,
319    ) {
320        debug_assert!(self.new_events.is_empty());
321        let sequence = self.len_persisted();
322        self.persisted_events.clear();
323        self.base_sequence = sequence;
324        self.snapshot = Some(SnapshotRecord {
325            sequence,
326            state,
327            recorded_at,
328            first_recorded_at,
329        });
330    }
331
332    /// Applies one hydration row (snapshot fields and/or an event) to this
333    /// container. Shared by `load_first` and `load_n`.
334    fn apply_hydration_row(
335        &mut self,
336        row: HydrationRow<<T as EsEvent>::EntityId>,
337    ) -> Result<(), EntityHydrationError> {
338        if let Some(mut snapshot_json) = row.snapshot
339            && S::IS_SNAPSHOT
340        {
341            if let Some(payload) = row.snapshot_forgettable_payload {
342                crate::forgettable::inject_forgettable_payload(&mut snapshot_json, payload);
343            }
344            let sequence = row
345                .snapshot_sequence
346                .expect("snapshot row missing snapshot_sequence");
347            let state: S = serde_json::from_value(snapshot_json)
348                .map_err(|source| EntityHydrationError::SnapshotDecode { sequence, source })?;
349            self.base_sequence = sequence as usize;
350            self.snapshot = Some(SnapshotRecord {
351                sequence: sequence as usize,
352                state,
353                recorded_at: row
354                    .snapshot_recorded_at
355                    .expect("snapshot row missing snapshot_recorded_at"),
356                first_recorded_at: row
357                    .snapshot_first_recorded_at
358                    .expect("snapshot row missing snapshot_first_recorded_at"),
359            });
360        }
361
362        match row.event {
363            Some(mut event_json) => {
364                if self.persisted_events.is_empty()
365                    && self.snapshot.is_some()
366                    && row.sequence as usize != self.base_sequence + 1
367                {
368                    return Err(EntityHydrationError::SnapshotGap {
369                        snapshot_sequence: self.base_sequence as i32,
370                        next_event_sequence: row.sequence,
371                    });
372                }
373                if let Some(payload) = row.forgettable_payload {
374                    crate::forgettable::inject_forgettable_payload(&mut event_json, payload);
375                }
376                self.persisted_events.push(PersistedEvent {
377                    entity_id: row.entity_id,
378                    recorded_at: row.recorded_at.expect("event row missing recorded_at"),
379                    sequence: row.sequence as usize,
380                    event: serde_json::from_value(event_json)?,
381                    context: row.context,
382                });
383                Ok(())
384            }
385            None if self.snapshot.is_some() => Ok(()),
386            None => Err(EntityHydrationError::NoEvents),
387        }
388    }
389
390    /// Loads and reconstructs the first entity from a stream of hydration
391    /// rows, marking events as `persisted`.
392    ///
393    /// Returns `Ok(None)` if no events are present, `Ok(Some(entity))` on success.
394    pub fn load_first<E>(
395        events: impl IntoIterator<Item = impl Into<HydrationRow<<T as EsEvent>::EntityId>>>,
396    ) -> Result<Option<E>, EntityHydrationError>
397    where
398        E: EsEntity<Event = T, Snapshot = S>,
399    {
400        let mut current_id = None;
401        let mut current: Option<Self> = None;
402        for e in events {
403            let row: HydrationRow<<T as EsEvent>::EntityId> = e.into();
404            if current_id.is_none() {
405                current_id = Some(row.entity_id.clone());
406                current = Some(Self {
407                    entity_id: row.entity_id.clone(),
408                    base_sequence: 0,
409                    snapshot: None,
410                    persisted_events: Vec::new(),
411                    new_events: Vec::new(),
412                });
413            }
414            if current_id.as_ref() != Some(&row.entity_id) {
415                break;
416            }
417            let cur = current.as_mut().expect("Could not get current");
418            cur.apply_hydration_row(row)?;
419        }
420        if let Some(current) = current {
421            Ok(Some(E::try_from_events(current)?))
422        } else {
423            Ok(None)
424        }
425    }
426
427    /// Loads and reconstructs up to `n` entities from a stream of hydration
428    /// rows. Assumes the rows are grouped by `id` and ordered by `sequence`
429    /// per `id`.
430    ///
431    /// Returns both the entities and a flag indicating whether more entities were available in the stream.
432    pub fn load_n<E>(
433        events: impl IntoIterator<Item = impl Into<HydrationRow<<T as EsEvent>::EntityId>>>,
434        n: usize,
435    ) -> Result<(Vec<E>, bool), EntityHydrationError>
436    where
437        E: EsEntity<Event = T, Snapshot = S>,
438    {
439        if n == 0 {
440            // Asking for zero entities yields zero entities. `has_more` reports
441            // whether the stream was non-empty, mirroring the `LIMIT n + 1`
442            // over-fetch contract the generated repos rely on: callers query
443            // `LIMIT (first + 1)`, so a non-empty stream means a next page exists.
444            let has_more = events.into_iter().next().is_some();
445            return Ok((Vec::new(), has_more));
446        }
447        let mut ret: Vec<E> = Vec::new();
448        let mut current_id = None;
449        let mut current: Option<Self> = None;
450        for e in events {
451            let row: HydrationRow<<T as EsEvent>::EntityId> = e.into();
452            if current_id.as_ref() != Some(&row.entity_id) {
453                if let Some(current) = current.take() {
454                    ret.push(E::try_from_events(current)?);
455                    if ret.len() == n {
456                        return Ok((ret, true));
457                    }
458                }
459
460                current_id = Some(row.entity_id.clone());
461                current = Some(Self {
462                    entity_id: row.entity_id.clone(),
463                    base_sequence: 0,
464                    snapshot: None,
465                    persisted_events: Vec::new(),
466                    new_events: Vec::new(),
467                });
468            }
469            let cur = current.as_mut().expect("Could not get current");
470            cur.apply_hydration_row(row)?;
471        }
472        if let Some(current) = current.take() {
473            ret.push(E::try_from_events(current)?);
474        }
475        Ok((ret, false))
476    }
477
478    #[doc(hidden)]
479    pub fn iter_new_events(&self) -> impl Iterator<Item = &EventWithContext<T>> {
480        self.new_events.iter()
481    }
482
483    #[doc(hidden)]
484    pub fn mark_new_events_persisted_at(
485        &mut self,
486        recorded_at: chrono::DateTime<chrono::Utc>,
487    ) -> usize {
488        let n = self.new_events.len();
489        let offset = self.len_persisted() + 1;
490        self.persisted_events
491            .extend(
492                self.new_events
493                    .drain(..)
494                    .enumerate()
495                    .map(|(i, event)| PersistedEvent {
496                        entity_id: self.entity_id.clone(),
497                        recorded_at,
498                        sequence: i + offset,
499                        event: event.event,
500                        context: event.context,
501                    }),
502            );
503        n
504    }
505
506    #[doc(hidden)]
507    pub fn new_event_types(&self) -> Vec<String> {
508        self.new_events
509            .iter()
510            .map(|event| event.event.event_type().to_string())
511            .collect()
512    }
513
514    #[doc(hidden)]
515    pub fn serialize_new_events(&self) -> Vec<serde_json::Value> {
516        self.new_events
517            .iter()
518            .map(|event| serde_json::to_value(&event.event).expect("Failed to serialize event"))
519            .collect()
520    }
521
522    /// Forgets all forgettable payloads in persisted events and returns the taken events.
523    ///
524    /// Applies `forget_fn` to each persisted event, then takes ownership of the event
525    /// stream, leaving `self` as an empty shell. The returned `EntityEvents` can be passed
526    /// to `TryFromEvents::try_from_events` to rebuild the entity with forgotten fields.
527    ///
528    /// Only used by non-snapshot repos: snapshot repos rebuild via a
529    /// full-history reload (the in-memory tail lacks events folded into the
530    /// snapshot).
531    #[doc(hidden)]
532    pub fn forget_and_take(&mut self, mut forget_fn: impl FnMut(&mut T)) -> Self {
533        for persisted in &mut self.persisted_events {
534            forget_fn(&mut persisted.event);
535        }
536        let entity_id = self.entity_id.clone();
537        std::mem::replace(
538            self,
539            Self {
540                entity_id,
541                base_sequence: 0,
542                snapshot: None,
543                persisted_events: Vec::new(),
544                new_events: Vec::new(),
545            },
546        )
547    }
548
549    #[doc(hidden)]
550    pub fn serialize_new_event_contexts(&self) -> Option<Vec<crate::ContextData>> {
551        if <T as EsEvent>::event_context() {
552            let contexts = self
553                .new_events
554                .iter()
555                .map(|event| event.context.clone().expect("Missing context"))
556                .collect();
557
558            Some(contexts)
559        } else {
560            None
561        }
562    }
563}
564
565impl<T: EsEvent> EntityEvents<T, NoSnapshot> {
566    /// Returns an iterator over all persisted events
567    pub fn iter_persisted(&self) -> impl DoubleEndedIterator<Item = &PersistedEvent<T>> + Clone {
568        self.persisted_events.iter()
569    }
570
571    /// Returns an iterator over all events (both persisted and new) in
572    /// chronological order.
573    ///
574    /// Only exists for `EntityEvents<T, NoSnapshot>` — a switch to a real
575    /// snapshot type is a compile error at every such scan, rather than a
576    /// silently-wrong fold that skips whatever the snapshot summarised:
577    ///
578    /// ```compile_fail,E0599
579    /// use es_entity::*;
580    /// use serde::{Serialize, Deserialize};
581    ///
582    /// es_entity::entity_id! { IterAllMeterId }
583    ///
584    /// #[derive(EsEvent, Debug, Serialize, Deserialize)]
585    /// #[serde(tag = "type", rename_all = "snake_case")]
586    /// #[es_event(id = "IterAllMeterId")]
587    /// pub enum IterAllMeterEvent {
588    ///     Initialized { id: IterAllMeterId },
589    /// }
590    ///
591    /// #[derive(EsSnapshot, Debug, Clone, Serialize, Deserialize)]
592    /// #[es_snapshot(version = 1)]
593    /// pub struct IterAllMeterSnapshot {
594    ///     pub id: IterAllMeterId,
595    /// }
596    ///
597    /// fn count_all(events: &EntityEvents<IterAllMeterEvent, IterAllMeterSnapshot>) -> usize {
598    ///     // error[E0599]: no method named `iter_all` on this type — it is
599    ///     // only defined for `EntityEvents<T, NoSnapshot>`. Use `replay()`,
600    ///     // which forces every match to account for `Replay::Snapshot`.
601    ///     events.iter_all().count()
602    /// }
603    /// ```
604    pub fn iter_all(&self) -> impl DoubleEndedIterator<Item = &T> + Clone {
605        self.persisted_events
606            .iter()
607            .map(|e| &e.event)
608            .chain(self.new_events.iter().map(|e| &e.event))
609    }
610
611    /// Widens a freshly-initialized container into one carrying a real
612    /// snapshot type. A brand-new entity always has no snapshot, so this
613    /// conversion is exact regardless of `S`.
614    #[doc(hidden)]
615    pub fn widen_snapshot<S: EsSnapshot>(self) -> EntityEvents<T, S> {
616        EntityEvents {
617            entity_id: self.entity_id,
618            base_sequence: 0,
619            snapshot: None,
620            persisted_events: self.persisted_events,
621            new_events: self.new_events,
622        }
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use proptest::prelude::*;
630    use uuid::Uuid;
631
632    /// Builds a `GenericEvent` whose JSON deserializes to `Created(name)`.
633    fn valid_event(id: Uuid, sequence: i32, name: &str) -> GenericEvent<Uuid> {
634        GenericEvent {
635            entity_id: id,
636            sequence,
637            event: serde_json::to_value(DummyEntityEvent::Created(name.to_string()))
638                .expect("could not serialize"),
639            context: None,
640            recorded_at: chrono::Utc::now(),
641            forgettable_payload: None,
642        }
643    }
644
645    /// Small bounded JSON strategy covering null/bool/int/float/string plus one
646    /// level of array/object nesting. Cheap to shrink and enough to exercise the
647    /// serde error paths without ballooning runtime.
648    fn json_value() -> impl Strategy<Value = serde_json::Value> {
649        let scalar = prop_oneof![
650            Just(serde_json::Value::Null),
651            any::<bool>().prop_map(serde_json::Value::Bool),
652            any::<i64>().prop_map(serde_json::Value::from),
653            any::<f64>().prop_map(serde_json::Value::from),
654            ".{0,15}".prop_map(serde_json::Value::String),
655        ]
656        .boxed();
657        let nested = prop_oneof![
658            proptest::collection::vec(scalar.clone(), 0..4).prop_map(serde_json::Value::Array),
659            proptest::collection::vec((".{0,6}", scalar.clone()), 0..4).prop_map(|pairs| {
660                let mut m = serde_json::Map::new();
661                for (k, v) in pairs {
662                    m.insert(k, v);
663                }
664                serde_json::Value::Object(m)
665            },),
666        ];
667        prop_oneof![scalar, nested]
668    }
669
670    #[derive(Debug, serde::Serialize, serde::Deserialize)]
671    enum DummyEntityEvent {
672        Created(String),
673    }
674
675    impl EsEvent for DummyEntityEvent {
676        type EntityId = Uuid;
677        fn event_context() -> bool {
678            true
679        }
680        fn event_type(&self) -> &'static str {
681            match self {
682                Self::Created(_) => "created",
683            }
684        }
685    }
686
687    struct DummyEntity {
688        name: String,
689
690        events: EntityEvents<DummyEntityEvent>,
691    }
692
693    impl EsEntity for DummyEntity {
694        type Event = DummyEntityEvent;
695        type New = NewDummyEntity;
696        type Snapshot = NoSnapshot;
697
698        fn events_mut(&mut self) -> &mut EntityEvents<DummyEntityEvent> {
699            &mut self.events
700        }
701        fn events(&self) -> &EntityEvents<DummyEntityEvent> {
702            &self.events
703        }
704    }
705
706    impl TryFromEvents<DummyEntityEvent> for DummyEntity {
707        fn try_from_events(
708            events: EntityEvents<DummyEntityEvent>,
709        ) -> Result<Self, EntityHydrationError> {
710            let name = events
711                .iter_persisted()
712                .map(|e| match &e.event {
713                    DummyEntityEvent::Created(name) => name.clone(),
714                })
715                .next()
716                .expect("Could not find name");
717            Ok(Self { name, events })
718        }
719    }
720
721    struct NewDummyEntity {}
722
723    impl IntoEvents<DummyEntityEvent> for NewDummyEntity {
724        fn into_events(self) -> EntityEvents<DummyEntityEvent> {
725            EntityEvents::init(
726                Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(),
727                vec![DummyEntityEvent::Created("".to_owned())],
728            )
729        }
730    }
731
732    #[test]
733    fn load_zero_events() {
734        let generic_events: Vec<GenericEvent<Uuid>> = vec![];
735        let res = EntityEvents::load_first::<DummyEntity>(generic_events);
736        assert!(matches!(res, Ok(None)));
737    }
738
739    #[test]
740    fn load_first() {
741        let generic_events = vec![GenericEvent {
742            entity_id: Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
743            sequence: 1,
744            event: serde_json::to_value(DummyEntityEvent::Created("dummy-name".to_owned()))
745                .expect("Could not serialize"),
746            context: None,
747            recorded_at: chrono::Utc::now(),
748            forgettable_payload: None,
749        }];
750        let entity: DummyEntity = EntityEvents::load_first(generic_events)
751            .expect("Could not load")
752            .expect("No entity found");
753        assert!(entity.name == "dummy-name");
754    }
755
756    #[test]
757    fn load_n() {
758        let generic_events = vec![
759            GenericEvent {
760                entity_id: Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(),
761                sequence: 1,
762                event: serde_json::to_value(DummyEntityEvent::Created("dummy-name".to_owned()))
763                    .expect("Could not serialize"),
764                context: None,
765                recorded_at: chrono::Utc::now(),
766                forgettable_payload: None,
767            },
768            GenericEvent {
769                entity_id: Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
770                sequence: 1,
771                event: serde_json::to_value(DummyEntityEvent::Created("other-name".to_owned()))
772                    .expect("Could not serialize"),
773                context: None,
774                recorded_at: chrono::Utc::now(),
775                forgettable_payload: None,
776            },
777        ];
778        let (entity, more): (Vec<DummyEntity>, _) =
779            EntityEvents::load_n(generic_events, 2).expect("Could not load");
780        assert!(!more);
781        assert_eq!(entity.len(), 2);
782    }
783
784    #[test]
785    fn last_persisted_does_not_panic_when_n_exceeds_len() {
786        let generic_events = vec![GenericEvent {
787            entity_id: Uuid::parse_str("00000000-0000-0000-0000-000000000004").unwrap(),
788            sequence: 1,
789            event: serde_json::to_value(DummyEntityEvent::Created("dummy".to_owned()))
790                .expect("Could not serialize"),
791            context: None,
792            recorded_at: chrono::Utc::now(),
793            forgettable_payload: None,
794        }];
795        let entity: DummyEntity = EntityEvents::load_first(generic_events)
796            .expect("Could not load")
797            .expect("No entity found");
798        let events = entity.events();
799
800        // n == len works
801        assert_eq!(events.last_persisted(1).count(), 1);
802        // n > len clamps to all persisted events instead of underflowing
803        assert_eq!(events.last_persisted(10).count(), 1);
804        // n == 0 yields nothing
805        assert_eq!(events.last_persisted(0).count(), 0);
806    }
807
808    proptest! {
809        #[test]
810        fn load_first_empty_input_returns_none(_ in Just(())) {
811            let res = EntityEvents::<DummyEntityEvent>::load_first::<DummyEntity>(
812                Vec::<GenericEvent<Uuid>>::new(),
813            );
814            prop_assert!(matches!(res, Ok(None)));
815        }
816
817        #[test]
818        fn load_first_non_empty_valid_hydrates(
819            count in 1u8..8,
820            names in proptest::collection::vec(".{0,10}", 1..8),
821        ) {
822            let id = Uuid::nil();
823            let events: Vec<_> = (1..=count as i32)
824                .zip(names.iter())
825                .map(|(s, n)| valid_event(id, s, n))
826                .collect();
827            let res = EntityEvents::<DummyEntityEvent>::load_first::<DummyEntity>(events);
828            prop_assert!(matches!(res, Ok(Some(_))));
829        }
830
831        /// Feeds arbitrary JSON through the loader. It may hydrate or error, but
832        /// it must never panic — and `Ok(None)` is only possible for empty input.
833        #[test]
834        fn load_first_arbitrary_json_never_panics(
835            raw in proptest::collection::vec(json_value(), 0..10),
836        ) {
837            let events: Vec<_> = raw
838                .into_iter()
839                .enumerate()
840                .map(|(i, v)| GenericEvent {
841                    entity_id: Uuid::nil(),
842                    sequence: i as i32,
843                    event: v,
844                    context: None,
845                    recorded_at: chrono::Utc::now(),
846                    forgettable_payload: None,
847                })
848                .collect();
849            let is_empty = events.is_empty();
850            let res = EntityEvents::<DummyEntityEvent>::load_first::<DummyEntity>(events);
851            if let Ok(None) = res {
852                prop_assert!(is_empty);
853            }
854        }
855
856        /// With a grouped, ascending stream (the documented precondition) and
857        /// `n >= 1`, `load_n` returns `min(n, k)` entities and reports `has_more`
858        /// exactly when `n < k`.
859        #[test]
860        fn load_n_respects_limit_and_more_flag(
861            k in 1u8..8,
862            per in 1u8..4,
863            n in 1u8..12,
864        ) {
865            let mut events = Vec::new();
866            for i in 0..k {
867                let id = Uuid::from_u128(i as u128);
868                for s in 1..=per as i32 {
869                    events.push(valid_event(id, s, &format!("e{i}-{s}")));
870                }
871            }
872            let (entities, has_more) =
873                EntityEvents::<DummyEntityEvent>::load_n::<DummyEntity>(events, n as usize)
874                    .expect("valid events hydrate");
875            prop_assert_eq!(entities.len(), (n as usize).min(k as usize));
876            prop_assert_eq!(has_more, n < k);
877        }
878
879        /// Regression for the `n = 0` degenerate case: previously returned *all*
880        /// entities instead of zero. Now returns none and reports `has_more` iff
881        /// the stream was non-empty (the `LIMIT n + 1` contract).
882        #[test]
883        fn load_n_zero_returns_no_entities(k in 0u8..5, per in 1u8..3) {
884            let mut events = Vec::new();
885            for i in 0..k {
886                let id = Uuid::from_u128(i as u128);
887                for s in 1..=per as i32 {
888                    events.push(valid_event(id, s, &format!("e{i}-{s}")));
889                }
890            }
891            let (entities, has_more) =
892                EntityEvents::<DummyEntityEvent>::load_n::<DummyEntity>(events, 0)
893                    .expect("valid events hydrate");
894            prop_assert!(entities.is_empty());
895            prop_assert_eq!(has_more, k > 0);
896        }
897
898        #[test]
899        fn load_n_arbitrary_json_never_panics(
900            raw in proptest::collection::vec(json_value(), 0..10),
901            n in 0u8..12,
902        ) {
903            let events: Vec<_> = raw
904                .into_iter()
905                .enumerate()
906                .map(|(i, v)| GenericEvent {
907                    entity_id: Uuid::nil(),
908                    sequence: i as i32,
909                    event: v,
910                    context: None,
911                    recorded_at: chrono::Utc::now(),
912                    forgettable_payload: None,
913                })
914                .collect();
915            let _ = EntityEvents::<DummyEntityEvent>::load_n::<DummyEntity>(events, n as usize);
916        }
917
918        /// `last_persisted(n)` must clamp to the available events for any `n`,
919        /// generalizing the dedicated saturating-sub test above.
920        #[test]
921        fn last_persisted_clamps_for_any_n(
922            p in 1u8..8,
923            n in 0u16..12,
924        ) {
925            let id = Uuid::nil();
926            let events: Vec<_> = (1..=p as i32)
927                .map(|s| valid_event(id, s, &format!("n{s}")))
928                .collect();
929            let entity: DummyEntity =
930                EntityEvents::<DummyEntityEvent>::load_first::<DummyEntity>(events)
931                    .expect("load")
932                    .expect("some");
933            let count = entity.events().last_persisted(n as usize).count();
934            prop_assert_eq!(count, (n as usize).min(p as usize));
935        }
936
937        /// Marking new events as persisted must drain `new_events`, leave
938        /// `len_persisted` consistent, and assign contiguous 1-based sequences
939        /// across repeated calls.
940        #[test]
941        fn mark_new_events_assigns_contiguous_sequences(
942            a in 0u8..5,
943            b in 0u8..5,
944        ) {
945            let id = Uuid::nil();
946            let mut events = EntityEvents::init(
947                id,
948                (0..a).map(|i| DummyEntityEvent::Created(format!("n{i}"))),
949            );
950            let now = chrono::Utc::now();
951
952            prop_assert_eq!(events.mark_new_events_persisted_at(now), a as usize);
953            prop_assert!(!events.any_new());
954            prop_assert_eq!(events.len_persisted(), a as usize);
955            let seqs: Vec<usize> = events.iter_persisted().map(|e| e.sequence).collect();
956            prop_assert_eq!(seqs, (1..=a as usize).collect::<Vec<_>>());
957
958            for i in 0..b {
959                events.push(DummyEntityEvent::Created(format!("m{i}")));
960            }
961            if b > 0 {
962                prop_assert!(events.any_new());
963            }
964            prop_assert_eq!(events.mark_new_events_persisted_at(now), b as usize);
965            prop_assert!(!events.any_new());
966            prop_assert_eq!(events.len_persisted(), (a + b) as usize);
967            let seqs: Vec<usize> = events.iter_persisted().map(|e| e.sequence).collect();
968            prop_assert_eq!(seqs, (1..=(a + b) as usize).collect::<Vec<_>>());
969        }
970    }
971}