use serde::{Serialize, de::DeserializeOwned};
use crate::error::GuardError;
pub trait Subject: Serialize + DeserializeOwned + Eq + Clone + Send + Sync + 'static {}
impl<T> Subject for T where T: Serialize + DeserializeOwned + Eq + Clone + Send + Sync + 'static {}
pub(crate) fn encode<S: Subject>(subject: &S) -> Result<String, GuardError> {
serde_json::to_string(subject).map_err(|e| GuardError::Subject(e.to_string()))
}
pub(crate) fn decode<S: Subject>(stored: &str) -> Option<S> {
serde_json::from_str(stored).ok()
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde::Deserialize;
use uuid::Uuid;
use super::*;
fn round_trip<S: Subject + std::fmt::Debug>(subject: S) {
let stored = encode(&subject).unwrap();
assert_eq!(decode::<S>(&stored), Some(subject));
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
struct TenantUser {
tenant: u32,
user: String,
tags: BTreeMap<String, String>,
}
#[test]
fn common_subjects_round_trip() {
round_trip(Uuid::new_v4());
round_trip(-42_i64);
round_trip(u64::MAX);
round_trip("user:ΓΌ@example.com\"';--".to_owned());
round_trip(TenantUser {
tenant: 7,
user: "ann".into(),
tags: BTreeMap::from([("b".into(), "2".into()), ("a".into(), "1".into())]),
});
}
#[test]
fn encoding_is_deterministic() {
let id = Uuid::new_v4();
assert_eq!(encode(&id).unwrap(), encode(&id).unwrap());
}
#[test]
fn mismatched_stored_values_are_rejected() {
assert_eq!(decode::<Uuid>("\"nope\""), None);
assert_eq!(decode::<i64>("\"12\""), None);
}
}