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#[cfg(test)]
229mod tests {
230    use super::*;
231    use proptest::prelude::*;
232
233    /// Bounded JSON strategy (null/bool/int/float/string + one level of
234    /// array/object nesting) used to exercise the merge and serde paths.
235    fn json_value() -> impl Strategy<Value = serde_json::Value> {
236        let scalar = prop_oneof![
237            Just(serde_json::Value::Null),
238            any::<bool>().prop_map(serde_json::Value::Bool),
239            any::<i64>().prop_map(serde_json::Value::from),
240            any::<f64>().prop_map(serde_json::Value::from),
241            ".{0,15}".prop_map(serde_json::Value::String),
242        ]
243        .boxed();
244        let nested = prop_oneof![
245            proptest::collection::vec(scalar.clone(), 0..4).prop_map(serde_json::Value::Array),
246            proptest::collection::vec((".{0,6}", scalar.clone()), 0..4).prop_map(|pairs| {
247                let mut m = serde_json::Map::new();
248                for (k, v) in pairs {
249                    m.insert(k, v);
250                }
251                serde_json::Value::Object(m)
252            },),
253        ];
254        prop_oneof![scalar, nested]
255    }
256
257    #[test]
258    fn serialize_set_emits_null() {
259        let value: Forgettable<String> = Forgettable::new("Alice".to_string());
260        let json = serde_json::to_value(&value).unwrap();
261        assert_eq!(json, serde_json::json!(null));
262    }
263
264    #[test]
265    fn serialize_forgotten_emits_null() {
266        let value: Forgettable<String> = Forgettable::forgotten();
267        let json = serde_json::to_value(&value).unwrap();
268        assert_eq!(json, serde_json::json!(null));
269    }
270
271    #[test]
272    fn deserialize_value() {
273        let json = serde_json::json!("Alice");
274        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
275        assert_eq!(value, Forgettable::new("Alice".to_string()));
276    }
277
278    #[test]
279    fn deserialize_null() {
280        let json = serde_json::json!(null);
281        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
282        assert_eq!(value, Forgettable::forgotten());
283    }
284
285    #[test]
286    fn serialize_struct_with_forgettable_emits_null() {
287        #[derive(Serialize, Deserialize, Debug, PartialEq)]
288        struct Event {
289            #[serde(rename = "type")]
290            kind: String,
291            name: Forgettable<String>,
292            email: String,
293        }
294
295        let event = Event {
296            kind: "initialized".to_string(),
297            name: Forgettable::new("Alice".to_string()),
298            email: "alice@test.com".to_string(),
299        };
300        let json = serde_json::to_value(&event).unwrap();
301        // Set serializes as null to prevent data leakage
302        assert_eq!(json["name"], serde_json::json!(null));
303        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
304
305        // Deserializing null yields Forgotten (real values come from payload table)
306        let deserialized: Event = serde_json::from_value(json).unwrap();
307        assert_eq!(deserialized.name, Forgettable::forgotten());
308
309        // Forgotten also serializes as null
310        let event_forgotten = Event {
311            kind: "initialized".to_string(),
312            name: Forgettable::forgotten(),
313            email: "alice@test.com".to_string(),
314        };
315        let json = serde_json::to_value(&event_forgotten).unwrap();
316        assert_eq!(json["name"], serde_json::json!(null));
317
318        let deserialized: Event = serde_json::from_value(json).unwrap();
319        assert_eq!(deserialized, event_forgotten);
320    }
321
322    #[test]
323    fn inject_payload() {
324        let mut json = serde_json::json!({
325            "type": "initialized",
326            "id": "uuid",
327            "name": null,
328            "email": "alice@test.com"
329        });
330
331        let payload = serde_json::json!({"name": "Alice"});
332        inject_forgettable_payload(&mut json, payload);
333
334        assert_eq!(json["name"], serde_json::json!("Alice"));
335        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
336    }
337
338    #[test]
339    fn value_helpers() {
340        let set: Forgettable<String> = Forgettable::new("test".to_string());
341        assert!(set.is_set());
342        assert!(!set.is_forgotten());
343        assert_eq!(&*set.value().unwrap(), "test");
344
345        let forgotten: Forgettable<String> = Forgettable::forgotten();
346        assert!(!forgotten.is_set());
347        assert!(forgotten.is_forgotten());
348        assert!(forgotten.value().is_none());
349    }
350
351    #[test]
352    fn extract_payload_value() {
353        let set: Forgettable<String> = Forgettable::new("Alice".to_string());
354        assert_eq!(
355            set.__extract_payload_value(),
356            Some(serde_json::json!("Alice"))
357        );
358
359        let forgotten: Forgettable<String> = Forgettable::forgotten();
360        assert_eq!(forgotten.__extract_payload_value(), None);
361    }
362
363    #[test]
364    fn forgettable_ref_deref() {
365        let f = Forgettable::new("hello".to_string());
366        let r = f.value().unwrap();
367        // Deref to &String
368        assert_eq!(r.len(), 5);
369        assert_eq!(&*r, "hello");
370    }
371
372    #[test]
373    fn forgettable_ref_display() {
374        let f = Forgettable::new("Alice".to_string());
375        let r = f.value().unwrap();
376        assert_eq!(format!("{r}"), "Alice");
377    }
378
379    #[test]
380    fn forgettable_ref_partial_eq() {
381        let f = Forgettable::new("Alice".to_string());
382        let r = f.value().unwrap();
383        assert_eq!(r, "Alice".to_string());
384    }
385
386    #[test]
387    fn default_is_forgotten() {
388        let f: Forgettable<String> = Default::default();
389        assert!(f.is_forgotten());
390        assert!(f.value().is_none());
391    }
392
393    #[test]
394    fn from_value() {
395        let f: Forgettable<String> = "Alice".to_string().into();
396        assert!(f.is_set());
397        assert_eq!(&*f.value().unwrap(), "Alice");
398    }
399
400    proptest! {
401        /// `inject_forgettable_payload` must never panic on any pair of JSON
402        /// values. When both sides are objects it merges payload over event
403        /// (payload wins on conflict); otherwise the event is left untouched.
404        #[test]
405        fn inject_never_panics_and_merges_only_objects(
406            event_in in json_value(),
407            payload in json_value(),
408        ) {
409            let mut event = event_in.clone();
410            inject_forgettable_payload(&mut event, payload.clone());
411
412            if event_in.is_object() && payload.is_object() {
413                let payload_obj = payload.as_object().unwrap();
414                // payload keys are present in the result with payload's values.
415                for (k, v) in payload_obj {
416                    prop_assert_eq!(event.get(k), Some(v));
417                }
418                // non-overwritten original keys are retained.
419                for (k, v) in event_in.as_object().unwrap() {
420                    if !payload_obj.contains_key(k) {
421                        prop_assert_eq!(event.get(k), Some(v));
422                    }
423                }
424            } else {
425                prop_assert_eq!(event, event_in);
426            }
427        }
428
429        /// A set `Forgettable` and a forgotten one serialize identically to
430        /// `null` (data-leakage guard), and the set/forgotten flags are always
431        /// mutually exclusive.
432        #[test]
433        fn forgettable_always_serializes_to_null(opt in any::<Option<String>>()) {
434            let v = serde_json::to_value(&opt).expect("serialize option");
435            let f: Forgettable<String> =
436                serde_json::from_value(v.clone()).expect("deserialize forgettable");
437            prop_assert_eq!(f.is_set(), opt.is_some());
438            prop_assert_eq!(f.is_forgotten(), opt.is_none());
439            prop_assert_eq!(serde_json::to_value(&f).unwrap(), serde_json::Value::Null);
440            // round-trip from null yields forgotten.
441            let from_null: Forgettable<String> =
442                serde_json::from_value(serde_json::Value::Null).unwrap();
443            prop_assert!(from_null.is_forgotten());
444        }
445
446        /// Deserializing a non-string, non-null value must error (never panic).
447        #[test]
448        fn forgettable_rejects_non_string_value(n in any::<i64>()) {
449            let res: Result<Forgettable<String>, _> =
450                serde_json::from_value(serde_json::Value::from(n));
451            prop_assert!(res.is_err());
452        }
453    }
454}