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
232    #[test]
233    fn serialize_set_emits_null() {
234        let value: Forgettable<String> = Forgettable::new("Alice".to_string());
235        let json = serde_json::to_value(&value).unwrap();
236        assert_eq!(json, serde_json::json!(null));
237    }
238
239    #[test]
240    fn serialize_forgotten_emits_null() {
241        let value: Forgettable<String> = Forgettable::forgotten();
242        let json = serde_json::to_value(&value).unwrap();
243        assert_eq!(json, serde_json::json!(null));
244    }
245
246    #[test]
247    fn deserialize_value() {
248        let json = serde_json::json!("Alice");
249        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
250        assert_eq!(value, Forgettable::new("Alice".to_string()));
251    }
252
253    #[test]
254    fn deserialize_null() {
255        let json = serde_json::json!(null);
256        let value: Forgettable<String> = serde_json::from_value(json).unwrap();
257        assert_eq!(value, Forgettable::forgotten());
258    }
259
260    #[test]
261    fn serialize_struct_with_forgettable_emits_null() {
262        #[derive(Serialize, Deserialize, Debug, PartialEq)]
263        struct Event {
264            #[serde(rename = "type")]
265            kind: String,
266            name: Forgettable<String>,
267            email: String,
268        }
269
270        let event = Event {
271            kind: "initialized".to_string(),
272            name: Forgettable::new("Alice".to_string()),
273            email: "alice@test.com".to_string(),
274        };
275        let json = serde_json::to_value(&event).unwrap();
276        // Set serializes as null to prevent data leakage
277        assert_eq!(json["name"], serde_json::json!(null));
278        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
279
280        // Deserializing null yields Forgotten (real values come from payload table)
281        let deserialized: Event = serde_json::from_value(json).unwrap();
282        assert_eq!(deserialized.name, Forgettable::forgotten());
283
284        // Forgotten also serializes as null
285        let event_forgotten = Event {
286            kind: "initialized".to_string(),
287            name: Forgettable::forgotten(),
288            email: "alice@test.com".to_string(),
289        };
290        let json = serde_json::to_value(&event_forgotten).unwrap();
291        assert_eq!(json["name"], serde_json::json!(null));
292
293        let deserialized: Event = serde_json::from_value(json).unwrap();
294        assert_eq!(deserialized, event_forgotten);
295    }
296
297    #[test]
298    fn inject_payload() {
299        let mut json = serde_json::json!({
300            "type": "initialized",
301            "id": "uuid",
302            "name": null,
303            "email": "alice@test.com"
304        });
305
306        let payload = serde_json::json!({"name": "Alice"});
307        inject_forgettable_payload(&mut json, payload);
308
309        assert_eq!(json["name"], serde_json::json!("Alice"));
310        assert_eq!(json["email"], serde_json::json!("alice@test.com"));
311    }
312
313    #[test]
314    fn value_helpers() {
315        let set: Forgettable<String> = Forgettable::new("test".to_string());
316        assert!(set.is_set());
317        assert!(!set.is_forgotten());
318        assert_eq!(&*set.value().unwrap(), "test");
319
320        let forgotten: Forgettable<String> = Forgettable::forgotten();
321        assert!(!forgotten.is_set());
322        assert!(forgotten.is_forgotten());
323        assert!(forgotten.value().is_none());
324    }
325
326    #[test]
327    fn extract_payload_value() {
328        let set: Forgettable<String> = Forgettable::new("Alice".to_string());
329        assert_eq!(
330            set.__extract_payload_value(),
331            Some(serde_json::json!("Alice"))
332        );
333
334        let forgotten: Forgettable<String> = Forgettable::forgotten();
335        assert_eq!(forgotten.__extract_payload_value(), None);
336    }
337
338    #[test]
339    fn forgettable_ref_deref() {
340        let f = Forgettable::new("hello".to_string());
341        let r = f.value().unwrap();
342        // Deref to &String
343        assert_eq!(r.len(), 5);
344        assert_eq!(&*r, "hello");
345    }
346
347    #[test]
348    fn forgettable_ref_display() {
349        let f = Forgettable::new("Alice".to_string());
350        let r = f.value().unwrap();
351        assert_eq!(format!("{r}"), "Alice");
352    }
353
354    #[test]
355    fn forgettable_ref_partial_eq() {
356        let f = Forgettable::new("Alice".to_string());
357        let r = f.value().unwrap();
358        assert_eq!(r, "Alice".to_string());
359    }
360
361    #[test]
362    fn default_is_forgotten() {
363        let f: Forgettable<String> = Default::default();
364        assert!(f.is_forgotten());
365        assert!(f.value().is_none());
366    }
367
368    #[test]
369    fn from_value() {
370        let f: Forgettable<String> = "Alice".to_string().into();
371        assert!(f.is_set());
372        assert_eq!(&*f.value().unwrap(), "Alice");
373    }
374}