es-entity 0.12.10

Event Sourcing Entity Framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! Support for forgettable event data (e.g., for GDPR compliance).
//!
//! The [`Forgettable<T>`] wrapper marks event fields containing personal data that
//! can be permanently deleted. Sensitive field values are stored in a separate
//! "forgettable payloads" table. Calling `forget()` on the repository deletes
//! those payloads, leaving the events intact but with `null` for forgotten fields.

use serde::{Deserialize, Deserializer, Serialize, Serializer};

use std::{fmt, hash, ops::Deref};

/// Wrapper for event fields containing data that can be forgotten (e.g., for GDPR).
///
/// This is an opaque struct — internal state is private so callers cannot
/// pattern-match to extract the raw value. Use [`Forgettable::value()`] to get
/// a [`ForgettableRef`] that derefs to `T` but does **not** implement `Serialize`,
/// preventing accidental re-serialization of personal data.
///
/// # Serde Behavior
///
/// - **Both** set and forgotten values serialize as `null` to prevent data
///   leakage when events are serialized to secondary stores.
/// - Deserializing `null` produces a forgotten value, non-null produces a set value.
/// - Real values are extracted via [`__extract_payload_value`] **before** serde runs,
///   and stored in the forgettable payloads table.
///
/// # JSON Schema
///
/// With the `json-schema` feature enabled, `Forgettable<T>` implements
/// `schemars::JsonSchema` by delegating to `Option<T>`, matching the
/// value-or-null serialized shape — no field-level `#[schemars(with = ...)]`
/// is needed on containing types.
///
/// # Repository Guard
///
/// An event type with `Forgettable<T>` fields must be backed by a repository
/// that enables `forgettable` in `#[es_repo(...)]`; otherwise the payloads are
/// never scrubbed. The repository derive cannot see the event's
/// forgettable-ness at macro time, so for a repository that omits the flag it
/// emits a const assertion on the event's inherent `HAS_FORGETTABLE_FIELDS`.
/// This is that assertion verbatim, and it fails to compile because the event
/// is forgettable:
///
/// ```compile_fail
/// use es_entity::*;
/// use serde::{Deserialize, Serialize};
///
/// es_entity::entity_id! { UserId }
///
/// #[derive(EsEvent, Serialize, Deserialize)]
/// #[serde(tag = "type", rename_all = "snake_case")]
/// #[es_event(id = "UserId")]
/// pub enum UserEvent {
///     Initialized { id: UserId, name: Forgettable<String> },
/// }
///
/// const _: () = assert!(
///     !UserEvent::HAS_FORGETTABLE_FIELDS,
///     "event type has Forgettable fields but this repo does not enable `forgettable`; add `forgettable` to #[es_repo(...)]"
/// );
/// ```
///
/// # Example
///
/// ```rust
/// use es_entity::Forgettable;
///
/// let name: Forgettable<String> = Forgettable::new("Alice".to_string());
/// assert_eq!(&*name.value().unwrap(), "Alice");
///
/// let forgotten: Forgettable<String> = Forgettable::forgotten();
/// assert!(forgotten.value().is_none());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Forgettable<T>(Option<T>);

impl<T> Default for Forgettable<T> {
    /// Returns a forgotten (empty) `Forgettable`.
    fn default() -> Self {
        Forgettable(None)
    }
}

impl<T> From<T> for Forgettable<T> {
    fn from(value: T) -> Self {
        Forgettable(Some(value))
    }
}

impl<T> Forgettable<T> {
    /// Creates a new `Forgettable` containing the given value.
    pub fn new(value: T) -> Self {
        Forgettable(Some(value))
    }

    /// Creates a forgotten (empty) `Forgettable`.
    pub fn forgotten() -> Self {
        Forgettable(None)
    }

    /// Returns a [`ForgettableRef`] wrapping the inner value, or `None` if forgotten.
    ///
    /// `ForgettableRef` implements `Deref<Target = T>` but **not** `Serialize`,
    /// so you can read the value but cannot accidentally serialize it.
    pub fn value(&self) -> Option<ForgettableRef<'_, T>> {
        self.0.as_ref().map(ForgettableRef)
    }

    /// Returns `true` if the value is present.
    pub fn is_set(&self) -> bool {
        self.0.is_some()
    }

    /// Returns `true` if the value has been forgotten.
    pub fn is_forgotten(&self) -> bool {
        self.0.is_none()
    }
}

impl<T: Serialize> Forgettable<T> {
    /// Extracts the inner value as a `serde_json::Value` for storage in
    /// the forgettable payloads table. Returns `None` if forgotten.
    #[doc(hidden)]
    pub fn __extract_payload_value(&self) -> Option<serde_json::Value> {
        self.0
            .as_ref()
            .map(|v| serde_json::to_value(v).expect("Failed to serialize forgettable field"))
    }
}

impl<T: Serialize> Serialize for Forgettable<T> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_none()
    }
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for Forgettable<T> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let value = Option::<T>::deserialize(deserializer)?;
        match value {
            Some(v) => Ok(Forgettable(Some(v))),
            None => Ok(Forgettable(None)),
        }
    }
}

#[cfg(feature = "json-schema")]
impl<T: schemars::JsonSchema> schemars::JsonSchema for Forgettable<T> {
    fn inline_schema() -> bool {
        Option::<T>::inline_schema()
    }

    fn schema_name() -> std::borrow::Cow<'static, str> {
        Option::<T>::schema_name()
    }

    fn schema_id() -> std::borrow::Cow<'static, str> {
        Option::<T>::schema_id()
    }

    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        Option::<T>::json_schema(generator)
    }
}

/// A non-serializable reference to the value inside a [`Forgettable<T>`].
///
/// Implements `Deref<Target = T>` so you can use it like `&T`, but does **not**
/// implement `Serialize` or `Clone`, preventing accidental re-serialization or
/// extraction of personal data.
pub struct ForgettableRef<'a, T>(&'a T);

impl<T: fmt::Debug> fmt::Debug for ForgettableRef<'_, T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl<T: fmt::Display> fmt::Display for ForgettableRef<'_, T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl<T> Deref for ForgettableRef<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        self.0
    }
}

impl<T: PartialEq> PartialEq<T> for ForgettableRef<'_, T> {
    fn eq(&self, other: &T) -> bool {
        self.0 == other
    }
}

impl<T: PartialEq> PartialEq for ForgettableRef<'_, T> {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl<T: Eq> Eq for ForgettableRef<'_, T> {}

impl<T: hash::Hash> hash::Hash for ForgettableRef<'_, T> {
    fn hash<H: hash::Hasher>(&self, state: &mut H) {
        self.0.hash(state);
    }
}

/// Injects forgettable payload values back into an event JSON object.
///
/// Merges all keys from the payload into the event JSON, overwriting `null` values
/// with the original data.
#[doc(hidden)]
pub fn inject_forgettable_payload(event_json: &mut serde_json::Value, payload: serde_json::Value) {
    if let (Some(event_obj), serde_json::Value::Object(payload_obj)) =
        (event_json.as_object_mut(), payload)
    {
        for (key, value) in payload_obj {
            event_obj.insert(key, value);
        }
    }
}

/// Storage-level remnants of forgettable data found by a repository's
/// generated `verify_forgotten` check.
///
/// `verify_forgotten` inspects the database directly — not hydrated entity
/// state — and reports anything that should have been erased by `forget()`:
///
/// - `payload_rows`: rows still present in the `_forgettable_payloads` table
/// - `live_index_columns`: `Forgettable<..>` index columns that are non-NULL
///   in the lookup table
/// - `event_fields`: `(event_type, field)` pairs of forgettable fields whose
///   durable event JSON holds a non-null value (defense-in-depth — the
///   framework always writes `null` there, so any hit indicates data written
///   outside the framework's serialization path)
///
/// An empty report means all configured forgettable data is physically absent
/// at the storage level.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ForgettableRemnants {
    /// Number of rows remaining in the forgettable payloads table.
    pub payload_rows: usize,
    /// Names of `Forgettable<..>` index columns that are still non-NULL.
    pub live_index_columns: Vec<&'static str>,
    /// `(event_type, field)` pairs with non-null forgettable values in the
    /// durable event JSON.
    pub event_fields: Vec<(String, String)>,
}

impl ForgettableRemnants {
    /// True when no forgettable data remains at the storage level.
    pub fn is_empty(&self) -> bool {
        self.payload_rows == 0 && self.live_index_columns.is_empty() && self.event_fields.is_empty()
    }
}

impl fmt::Display for ForgettableRemnants {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "forgettable data still present at storage level: {} payload row(s), non-NULL index columns {:?}, non-null event fields {:?}",
            self.payload_rows, self.live_index_columns, self.event_fields
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    /// Bounded JSON strategy (null/bool/int/float/string + one level of
    /// array/object nesting) used to exercise the merge and serde paths.
    fn json_value() -> impl Strategy<Value = serde_json::Value> {
        let scalar = prop_oneof![
            Just(serde_json::Value::Null),
            any::<bool>().prop_map(serde_json::Value::Bool),
            any::<i64>().prop_map(serde_json::Value::from),
            any::<f64>().prop_map(serde_json::Value::from),
            ".{0,15}".prop_map(serde_json::Value::String),
        ]
        .boxed();
        let nested = prop_oneof![
            proptest::collection::vec(scalar.clone(), 0..4).prop_map(serde_json::Value::Array),
            proptest::collection::vec((".{0,6}", scalar.clone()), 0..4).prop_map(|pairs| {
                let mut m = serde_json::Map::new();
                for (k, v) in pairs {
                    m.insert(k, v);
                }
                serde_json::Value::Object(m)
            },),
        ];
        prop_oneof![scalar, nested]
    }

    #[test]
    fn serialize_set_emits_null() {
        let value: Forgettable<String> = Forgettable::new("Alice".to_string());
        let json = serde_json::to_value(&value).unwrap();
        assert_eq!(json, serde_json::json!(null));
    }

    #[test]
    fn serialize_forgotten_emits_null() {
        let value: Forgettable<String> = Forgettable::forgotten();
        let json = serde_json::to_value(&value).unwrap();
        assert_eq!(json, serde_json::json!(null));
    }

    #[test]
    fn deserialize_value() {
        let json = serde_json::json!("Alice");
        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
        assert_eq!(value, Forgettable::new("Alice".to_string()));
    }

    #[test]
    fn deserialize_null() {
        let json = serde_json::json!(null);
        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
        assert_eq!(value, Forgettable::forgotten());
    }

    #[test]
    fn serialize_struct_with_forgettable_emits_null() {
        #[derive(Serialize, Deserialize, Debug, PartialEq)]
        struct Event {
            #[serde(rename = "type")]
            kind: String,
            name: Forgettable<String>,
            email: String,
        }

        let event = Event {
            kind: "initialized".to_string(),
            name: Forgettable::new("Alice".to_string()),
            email: "alice@test.com".to_string(),
        };
        let json = serde_json::to_value(&event).unwrap();
        // Set serializes as null to prevent data leakage
        assert_eq!(json["name"], serde_json::json!(null));
        assert_eq!(json["email"], serde_json::json!("alice@test.com"));

        // Deserializing null yields Forgotten (real values come from payload table)
        let deserialized: Event = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized.name, Forgettable::forgotten());

        // Forgotten also serializes as null
        let event_forgotten = Event {
            kind: "initialized".to_string(),
            name: Forgettable::forgotten(),
            email: "alice@test.com".to_string(),
        };
        let json = serde_json::to_value(&event_forgotten).unwrap();
        assert_eq!(json["name"], serde_json::json!(null));

        let deserialized: Event = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized, event_forgotten);
    }

    #[test]
    fn inject_payload() {
        let mut json = serde_json::json!({
            "type": "initialized",
            "id": "uuid",
            "name": null,
            "email": "alice@test.com"
        });

        let payload = serde_json::json!({"name": "Alice"});
        inject_forgettable_payload(&mut json, payload);

        assert_eq!(json["name"], serde_json::json!("Alice"));
        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
    }

    #[test]
    fn value_helpers() {
        let set: Forgettable<String> = Forgettable::new("test".to_string());
        assert!(set.is_set());
        assert!(!set.is_forgotten());
        assert_eq!(&*set.value().unwrap(), "test");

        let forgotten: Forgettable<String> = Forgettable::forgotten();
        assert!(!forgotten.is_set());
        assert!(forgotten.is_forgotten());
        assert!(forgotten.value().is_none());
    }

    #[test]
    fn extract_payload_value() {
        let set: Forgettable<String> = Forgettable::new("Alice".to_string());
        assert_eq!(
            set.__extract_payload_value(),
            Some(serde_json::json!("Alice"))
        );

        let forgotten: Forgettable<String> = Forgettable::forgotten();
        assert_eq!(forgotten.__extract_payload_value(), None);
    }

    #[test]
    fn forgettable_ref_deref() {
        let f = Forgettable::new("hello".to_string());
        let r = f.value().unwrap();
        // Deref to &String
        assert_eq!(r.len(), 5);
        assert_eq!(&*r, "hello");
    }

    #[test]
    fn forgettable_ref_display() {
        let f = Forgettable::new("Alice".to_string());
        let r = f.value().unwrap();
        assert_eq!(format!("{r}"), "Alice");
    }

    #[test]
    fn forgettable_ref_partial_eq() {
        let f = Forgettable::new("Alice".to_string());
        let r = f.value().unwrap();
        assert_eq!(r, "Alice".to_string());
    }

    #[test]
    fn default_is_forgotten() {
        let f: Forgettable<String> = Default::default();
        assert!(f.is_forgotten());
        assert!(f.value().is_none());
    }

    #[test]
    fn from_value() {
        let f: Forgettable<String> = "Alice".to_string().into();
        assert!(f.is_set());
        assert_eq!(&*f.value().unwrap(), "Alice");
    }

    proptest! {
        /// `inject_forgettable_payload` must never panic on any pair of JSON
        /// values. When both sides are objects it merges payload over event
        /// (payload wins on conflict); otherwise the event is left untouched.
        #[test]
        fn inject_never_panics_and_merges_only_objects(
            event_in in json_value(),
            payload in json_value(),
        ) {
            let mut event = event_in.clone();
            inject_forgettable_payload(&mut event, payload.clone());

            if event_in.is_object() && payload.is_object() {
                let payload_obj = payload.as_object().unwrap();
                // payload keys are present in the result with payload's values.
                for (k, v) in payload_obj {
                    prop_assert_eq!(event.get(k), Some(v));
                }
                // non-overwritten original keys are retained.
                for (k, v) in event_in.as_object().unwrap() {
                    if !payload_obj.contains_key(k) {
                        prop_assert_eq!(event.get(k), Some(v));
                    }
                }
            } else {
                prop_assert_eq!(event, event_in);
            }
        }

        /// A set `Forgettable` and a forgotten one serialize identically to
        /// `null` (data-leakage guard), and the set/forgotten flags are always
        /// mutually exclusive.
        #[test]
        fn forgettable_always_serializes_to_null(opt in any::<Option<String>>()) {
            let v = serde_json::to_value(&opt).expect("serialize option");
            let f: Forgettable<String> =
                serde_json::from_value(v.clone()).expect("deserialize forgettable");
            prop_assert_eq!(f.is_set(), opt.is_some());
            prop_assert_eq!(f.is_forgotten(), opt.is_none());
            prop_assert_eq!(serde_json::to_value(&f).unwrap(), serde_json::Value::Null);
            // round-trip from null yields forgotten.
            let from_null: Forgettable<String> =
                serde_json::from_value(serde_json::Value::Null).unwrap();
            prop_assert!(from_null.is_forgotten());
        }

        /// Deserializing a non-string, non-null value must error (never panic).
        #[test]
        fn forgettable_rejects_non_string_value(n in any::<i64>()) {
            let res: Result<Forgettable<String>, _> =
                serde_json::from_value(serde_json::Value::from(n));
            prop_assert!(res.is_err());
        }
    }
}