1use serde::{Serialize, de::DeserializeOwned};
4
5use crate::error::GuardError;
6
7pub trait Subject: Serialize + DeserializeOwned + Eq + Clone + Send + Sync + 'static {}
18
19impl<T> Subject for T where T: Serialize + DeserializeOwned + Eq + Clone + Send + Sync + 'static {}
20
21pub(crate) fn encode<S: Subject>(subject: &S) -> Result<String, GuardError> {
23 serde_json::to_string(subject).map_err(|e| GuardError::Subject(e.to_string()))
24}
25
26pub(crate) fn decode<S: Subject>(stored: &str) -> Option<S> {
28 serde_json::from_str(stored).ok()
29}
30
31#[cfg(test)]
32mod tests {
33 use std::collections::BTreeMap;
34
35 use serde::Deserialize;
36 use uuid::Uuid;
37
38 use super::*;
39
40 fn round_trip<S: Subject + std::fmt::Debug>(subject: S) {
41 let stored = encode(&subject).unwrap();
42 assert_eq!(decode::<S>(&stored), Some(subject));
43 }
44
45 #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
46 struct TenantUser {
47 tenant: u32,
48 user: String,
49 tags: BTreeMap<String, String>,
50 }
51
52 #[test]
53 fn common_subjects_round_trip() {
54 round_trip(Uuid::new_v4());
55 round_trip(-42_i64);
56 round_trip(u64::MAX);
57 round_trip("user:ΓΌ@example.com\"';--".to_owned());
58 round_trip(TenantUser {
59 tenant: 7,
60 user: "ann".into(),
61 tags: BTreeMap::from([("b".into(), "2".into()), ("a".into(), "1".into())]),
62 });
63 }
64
65 #[test]
66 fn encoding_is_deterministic() {
67 let id = Uuid::new_v4();
68 assert_eq!(encode(&id).unwrap(), encode(&id).unwrap());
69 }
70
71 #[test]
72 fn mismatched_stored_values_are_rejected() {
73 assert_eq!(decode::<Uuid>("\"nope\""), None);
74 assert_eq!(decode::<i64>("\"12\""), None);
75 }
76}