Skip to main content

es_entity/
forgettable.rs

1//! Support for forgettable event data (e.g., for GDPR compliance).
2//!
3//! The [`Forgettable<T>`] wrapper marks event fields containing personal data that
4//! can be permanently deleted. Sensitive field values are stored in a separate
5//! "forgettable payloads" table. Calling `forget()` on the repository deletes
6//! those payloads, leaving the events intact but with `null` for forgotten fields.
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use std::{fmt, hash, ops::Deref};
11
12/// Wrapper for event fields containing data that can be forgotten (e.g., for GDPR).
13///
14/// This is an opaque struct — internal state is private so callers cannot
15/// pattern-match to extract the raw value. Use [`Forgettable::value()`] to get
16/// a [`ForgettableRef`] that derefs to `T` but does **not** implement `Serialize`,
17/// preventing accidental re-serialization of personal data.
18///
19/// # Serde Behavior
20///
21/// - **Both** set and forgotten values serialize as `null` to prevent data
22///   leakage when events are serialized to secondary stores.
23/// - Deserializing `null` produces a forgotten value, non-null produces a set value.
24/// - Real values are extracted via [`__extract_payload_value`] **before** serde runs,
25///   and stored in the forgettable payloads table.
26///
27/// # JSON Schema
28///
29/// With the `json-schema` feature enabled, `Forgettable<T>` implements
30/// `schemars::JsonSchema` by delegating to `Option<T>`, matching the
31/// value-or-null serialized shape — no field-level `#[schemars(with = ...)]`
32/// is needed on containing types.
33///
34/// # Repository Guard
35///
36/// An event type with `Forgettable<T>` fields must be backed by a repository
37/// that enables `forgettable` in `#[es_repo(...)]`; otherwise the payloads are
38/// never scrubbed. The repository derive cannot see the event's
39/// forgettable-ness at macro time, so for a repository that omits the flag it
40/// emits a const assertion on the event's inherent `HAS_FORGETTABLE_FIELDS`.
41/// This is that assertion verbatim, and it fails to compile because the event
42/// is forgettable:
43///
44/// ```compile_fail
45/// use es_entity::*;
46/// use serde::{Deserialize, Serialize};
47///
48/// es_entity::entity_id! { UserId }
49///
50/// #[derive(EsEvent, Serialize, Deserialize)]
51/// #[serde(tag = "type", rename_all = "snake_case")]
52/// #[es_event(id = "UserId")]
53/// pub enum UserEvent {
54///     Initialized { id: UserId, name: Forgettable<String> },
55/// }
56///
57/// const _: () = assert!(
58///     !UserEvent::HAS_FORGETTABLE_FIELDS,
59///     "event type has Forgettable fields but this repo does not enable `forgettable`; add `forgettable` to #[es_repo(...)]"
60/// );
61/// ```
62///
63/// # Example
64///
65/// ```rust
66/// use es_entity::Forgettable;
67///
68/// let name: Forgettable<String> = Forgettable::new("Alice".to_string());
69/// assert_eq!(&*name.value().unwrap(), "Alice");
70///
71/// let forgotten: Forgettable<String> = Forgettable::forgotten();
72/// assert!(forgotten.value().is_none());
73/// ```
74#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub struct Forgettable<T>(Option<T>);
76
77impl<T> Default for Forgettable<T> {
78    /// Returns a forgotten (empty) `Forgettable`.
79    fn default() -> Self {
80        Forgettable(None)
81    }
82}
83
84impl<T> From<T> for Forgettable<T> {
85    fn from(value: T) -> Self {
86        Forgettable(Some(value))
87    }
88}
89
90impl<T> Forgettable<T> {
91    /// Creates a new `Forgettable` containing the given value.
92    pub fn new(value: T) -> Self {
93        Forgettable(Some(value))
94    }
95
96    /// Creates a forgotten (empty) `Forgettable`.
97    pub fn forgotten() -> Self {
98        Forgettable(None)
99    }
100
101    /// Returns a [`ForgettableRef`] wrapping the inner value, or `None` if forgotten.
102    ///
103    /// `ForgettableRef` implements `Deref<Target = T>` but **not** `Serialize`,
104    /// so you can read the value but cannot accidentally serialize it.
105    pub fn value(&self) -> Option<ForgettableRef<'_, T>> {
106        self.0.as_ref().map(ForgettableRef)
107    }
108
109    /// Returns `true` if the value is present.
110    pub fn is_set(&self) -> bool {
111        self.0.is_some()
112    }
113
114    /// Returns `true` if the value has been forgotten.
115    pub fn is_forgotten(&self) -> bool {
116        self.0.is_none()
117    }
118}
119
120impl<T: Serialize> Forgettable<T> {
121    /// Extracts the inner value as a `serde_json::Value` for storage in
122    /// the forgettable payloads table. Returns `None` if forgotten.
123    #[doc(hidden)]
124    pub fn __extract_payload_value(&self) -> Option<serde_json::Value> {
125        self.0
126            .as_ref()
127            .map(|v| serde_json::to_value(v).expect("Failed to serialize forgettable field"))
128    }
129}
130
131impl<T: Serialize> Serialize for Forgettable<T> {
132    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
133        serializer.serialize_none()
134    }
135}
136
137impl<'de, T: Deserialize<'de>> Deserialize<'de> for Forgettable<T> {
138    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
139        let value = Option::<T>::deserialize(deserializer)?;
140        match value {
141            Some(v) => Ok(Forgettable(Some(v))),
142            None => Ok(Forgettable(None)),
143        }
144    }
145}
146
147#[cfg(feature = "json-schema")]
148impl<T: schemars::JsonSchema> schemars::JsonSchema for Forgettable<T> {
149    fn inline_schema() -> bool {
150        Option::<T>::inline_schema()
151    }
152
153    fn schema_name() -> std::borrow::Cow<'static, str> {
154        Option::<T>::schema_name()
155    }
156
157    fn schema_id() -> std::borrow::Cow<'static, str> {
158        Option::<T>::schema_id()
159    }
160
161    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
162        Option::<T>::json_schema(generator)
163    }
164}
165
166/// A non-serializable reference to the value inside a [`Forgettable<T>`].
167///
168/// Implements `Deref<Target = T>` so you can use it like `&T`, but does **not**
169/// implement `Serialize` or `Clone`, preventing accidental re-serialization or
170/// extraction of personal data.
171pub struct ForgettableRef<'a, T>(&'a T);
172
173impl<T: fmt::Debug> fmt::Debug for ForgettableRef<'_, T> {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        self.0.fmt(f)
176    }
177}
178
179impl<T: fmt::Display> fmt::Display for ForgettableRef<'_, T> {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        self.0.fmt(f)
182    }
183}
184
185impl<T> Deref for ForgettableRef<'_, T> {
186    type Target = T;
187
188    fn deref(&self) -> &T {
189        self.0
190    }
191}
192
193impl<T: PartialEq> PartialEq<T> for ForgettableRef<'_, T> {
194    fn eq(&self, other: &T) -> bool {
195        self.0 == other
196    }
197}
198
199impl<T: PartialEq> PartialEq for ForgettableRef<'_, T> {
200    fn eq(&self, other: &Self) -> bool {
201        self.0 == other.0
202    }
203}
204
205impl<T: Eq> Eq for ForgettableRef<'_, T> {}
206
207impl<T: hash::Hash> hash::Hash for ForgettableRef<'_, T> {
208    fn hash<H: hash::Hasher>(&self, state: &mut H) {
209        self.0.hash(state);
210    }
211}
212
213/// Injects forgettable payload values back into an event JSON object.
214///
215/// Merges all keys from the payload into the event JSON, overwriting `null` values
216/// with the original data.
217#[doc(hidden)]
218pub fn inject_forgettable_payload(event_json: &mut serde_json::Value, payload: serde_json::Value) {
219    if let (Some(event_obj), serde_json::Value::Object(payload_obj)) =
220        (event_json.as_object_mut(), payload)
221    {
222        for (key, value) in payload_obj {
223            event_obj.insert(key, value);
224        }
225    }
226}
227
228/// Storage-level remnants of forgettable data found by a repository's
229/// generated `verify_forgotten` check.
230///
231/// `verify_forgotten` inspects the database directly — not hydrated entity
232/// state — and reports anything that should have been erased by `forget()`:
233///
234/// - `payload_rows`: rows still present in the `_forgettable_payloads` table
235/// - `live_index_columns`: `Forgettable<..>` index columns that are non-NULL
236///   in the lookup table
237/// - `event_fields`: `(event_type, field)` pairs of forgettable fields whose
238///   durable event JSON holds a non-null value (defense-in-depth — the
239///   framework always writes `null` there, so any hit indicates data written
240///   outside the framework's serialization path)
241///
242/// An empty report means all configured forgettable data is physically absent
243/// at the storage level.
244#[derive(Debug, Clone, PartialEq, Eq, Default)]
245pub struct ForgettableRemnants {
246    /// Number of rows remaining in the forgettable payloads table.
247    pub payload_rows: usize,
248    /// Names of `Forgettable<..>` index columns that are still non-NULL.
249    pub live_index_columns: Vec<&'static str>,
250    /// `(event_type, field)` pairs with non-null forgettable values in the
251    /// durable event JSON.
252    pub event_fields: Vec<(String, String)>,
253}
254
255impl ForgettableRemnants {
256    /// True when no forgettable data remains at the storage level.
257    pub fn is_empty(&self) -> bool {
258        self.payload_rows == 0 && self.live_index_columns.is_empty() && self.event_fields.is_empty()
259    }
260}
261
262impl fmt::Display for ForgettableRemnants {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(
265            f,
266            "forgettable data still present at storage level: {} payload row(s), non-NULL index columns {:?}, non-null event fields {:?}",
267            self.payload_rows, self.live_index_columns, self.event_fields
268        )
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use proptest::prelude::*;
276
277    /// Bounded JSON strategy (null/bool/int/float/string + one level of
278    /// array/object nesting) used to exercise the merge and serde paths.
279    fn json_value() -> impl Strategy<Value = serde_json::Value> {
280        let scalar = prop_oneof![
281            Just(serde_json::Value::Null),
282            any::<bool>().prop_map(serde_json::Value::Bool),
283            any::<i64>().prop_map(serde_json::Value::from),
284            any::<f64>().prop_map(serde_json::Value::from),
285            ".{0,15}".prop_map(serde_json::Value::String),
286        ]
287        .boxed();
288        let nested = prop_oneof![
289            proptest::collection::vec(scalar.clone(), 0..4).prop_map(serde_json::Value::Array),
290            proptest::collection::vec((".{0,6}", scalar.clone()), 0..4).prop_map(|pairs| {
291                let mut m = serde_json::Map::new();
292                for (k, v) in pairs {
293                    m.insert(k, v);
294                }
295                serde_json::Value::Object(m)
296            },),
297        ];
298        prop_oneof![scalar, nested]
299    }
300
301    #[test]
302    fn serialize_set_emits_null() {
303        let value: Forgettable<String> = Forgettable::new("Alice".to_string());
304        let json = serde_json::to_value(&value).unwrap();
305        assert_eq!(json, serde_json::json!(null));
306    }
307
308    #[test]
309    fn serialize_forgotten_emits_null() {
310        let value: Forgettable<String> = Forgettable::forgotten();
311        let json = serde_json::to_value(&value).unwrap();
312        assert_eq!(json, serde_json::json!(null));
313    }
314
315    #[test]
316    fn deserialize_value() {
317        let json = serde_json::json!("Alice");
318        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
319        assert_eq!(value, Forgettable::new("Alice".to_string()));
320    }
321
322    #[test]
323    fn deserialize_null() {
324        let json = serde_json::json!(null);
325        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
326        assert_eq!(value, Forgettable::forgotten());
327    }
328
329    #[test]
330    fn serialize_struct_with_forgettable_emits_null() {
331        #[derive(Serialize, Deserialize, Debug, PartialEq)]
332        struct Event {
333            #[serde(rename = "type")]
334            kind: String,
335            name: Forgettable<String>,
336            email: String,
337        }
338
339        let event = Event {
340            kind: "initialized".to_string(),
341            name: Forgettable::new("Alice".to_string()),
342            email: "alice@test.com".to_string(),
343        };
344        let json = serde_json::to_value(&event).unwrap();
345        // Set serializes as null to prevent data leakage
346        assert_eq!(json["name"], serde_json::json!(null));
347        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
348
349        // Deserializing null yields Forgotten (real values come from payload table)
350        let deserialized: Event = serde_json::from_value(json).unwrap();
351        assert_eq!(deserialized.name, Forgettable::forgotten());
352
353        // Forgotten also serializes as null
354        let event_forgotten = Event {
355            kind: "initialized".to_string(),
356            name: Forgettable::forgotten(),
357            email: "alice@test.com".to_string(),
358        };
359        let json = serde_json::to_value(&event_forgotten).unwrap();
360        assert_eq!(json["name"], serde_json::json!(null));
361
362        let deserialized: Event = serde_json::from_value(json).unwrap();
363        assert_eq!(deserialized, event_forgotten);
364    }
365
366    #[test]
367    fn inject_payload() {
368        let mut json = serde_json::json!({
369            "type": "initialized",
370            "id": "uuid",
371            "name": null,
372            "email": "alice@test.com"
373        });
374
375        let payload = serde_json::json!({"name": "Alice"});
376        inject_forgettable_payload(&mut json, payload);
377
378        assert_eq!(json["name"], serde_json::json!("Alice"));
379        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
380    }
381
382    #[test]
383    fn value_helpers() {
384        let set: Forgettable<String> = Forgettable::new("test".to_string());
385        assert!(set.is_set());
386        assert!(!set.is_forgotten());
387        assert_eq!(&*set.value().unwrap(), "test");
388
389        let forgotten: Forgettable<String> = Forgettable::forgotten();
390        assert!(!forgotten.is_set());
391        assert!(forgotten.is_forgotten());
392        assert!(forgotten.value().is_none());
393    }
394
395    #[test]
396    fn extract_payload_value() {
397        let set: Forgettable<String> = Forgettable::new("Alice".to_string());
398        assert_eq!(
399            set.__extract_payload_value(),
400            Some(serde_json::json!("Alice"))
401        );
402
403        let forgotten: Forgettable<String> = Forgettable::forgotten();
404        assert_eq!(forgotten.__extract_payload_value(), None);
405    }
406
407    #[test]
408    fn forgettable_ref_deref() {
409        let f = Forgettable::new("hello".to_string());
410        let r = f.value().unwrap();
411        // Deref to &String
412        assert_eq!(r.len(), 5);
413        assert_eq!(&*r, "hello");
414    }
415
416    #[test]
417    fn forgettable_ref_display() {
418        let f = Forgettable::new("Alice".to_string());
419        let r = f.value().unwrap();
420        assert_eq!(format!("{r}"), "Alice");
421    }
422
423    #[test]
424    fn forgettable_ref_partial_eq() {
425        let f = Forgettable::new("Alice".to_string());
426        let r = f.value().unwrap();
427        assert_eq!(r, "Alice".to_string());
428    }
429
430    #[test]
431    fn default_is_forgotten() {
432        let f: Forgettable<String> = Default::default();
433        assert!(f.is_forgotten());
434        assert!(f.value().is_none());
435    }
436
437    #[test]
438    fn from_value() {
439        let f: Forgettable<String> = "Alice".to_string().into();
440        assert!(f.is_set());
441        assert_eq!(&*f.value().unwrap(), "Alice");
442    }
443
444    proptest! {
445        /// `inject_forgettable_payload` must never panic on any pair of JSON
446        /// values. When both sides are objects it merges payload over event
447        /// (payload wins on conflict); otherwise the event is left untouched.
448        #[test]
449        fn inject_never_panics_and_merges_only_objects(
450            event_in in json_value(),
451            payload in json_value(),
452        ) {
453            let mut event = event_in.clone();
454            inject_forgettable_payload(&mut event, payload.clone());
455
456            if event_in.is_object() && payload.is_object() {
457                let payload_obj = payload.as_object().unwrap();
458                // payload keys are present in the result with payload's values.
459                for (k, v) in payload_obj {
460                    prop_assert_eq!(event.get(k), Some(v));
461                }
462                // non-overwritten original keys are retained.
463                for (k, v) in event_in.as_object().unwrap() {
464                    if !payload_obj.contains_key(k) {
465                        prop_assert_eq!(event.get(k), Some(v));
466                    }
467                }
468            } else {
469                prop_assert_eq!(event, event_in);
470            }
471        }
472
473        /// A set `Forgettable` and a forgotten one serialize identically to
474        /// `null` (data-leakage guard), and the set/forgotten flags are always
475        /// mutually exclusive.
476        #[test]
477        fn forgettable_always_serializes_to_null(opt in any::<Option<String>>()) {
478            let v = serde_json::to_value(&opt).expect("serialize option");
479            let f: Forgettable<String> =
480                serde_json::from_value(v.clone()).expect("deserialize forgettable");
481            prop_assert_eq!(f.is_set(), opt.is_some());
482            prop_assert_eq!(f.is_forgotten(), opt.is_none());
483            prop_assert_eq!(serde_json::to_value(&f).unwrap(), serde_json::Value::Null);
484            // round-trip from null yields forgotten.
485            let from_null: Forgettable<String> =
486                serde_json::from_value(serde_json::Value::Null).unwrap();
487            prop_assert!(from_null.is_forgotten());
488        }
489
490        /// Deserializing a non-string, non-null value must error (never panic).
491        #[test]
492        fn forgettable_rejects_non_string_value(n in any::<i64>()) {
493            let res: Result<Forgettable<String>, _> =
494                serde_json::from_value(serde_json::Value::from(n));
495            prop_assert!(res.is_err());
496        }
497    }
498}