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