use moq_pattern::Patterns;
use serde::{Deserialize, Serialize};
use serde_with::{DurationSeconds, TimestampSeconds, serde_as};
use std::time::{Duration, SystemTime};
pub(crate) const CLOCK_SKEW: Duration = Duration::from_secs(5);
fn until(at: SystemTime) -> Duration {
match at.duration_since(SystemTime::now()) {
Ok(remaining) => remaining,
Err(late) => CLOCK_SKEW.saturating_sub(late.duration()),
}
}
#[serde_as]
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
#[non_exhaustive]
pub struct Grant {
#[serde(skip_serializing_if = "Patterns::is_empty")]
pub publish: Patterns,
#[serde(skip_serializing_if = "Patterns::is_empty")]
pub subscribe: Patterns,
pub root: Option<String>,
#[serde_as(as = "Option<TimestampSeconds<i64>>")]
pub expires: Option<SystemTime>,
#[serde_as(as = "Option<DurationSeconds<u64>>")]
pub revalidate: Option<Duration>,
pub tier: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub peer: bool,
}
impl Grant {
pub fn new(publish: Patterns, subscribe: Patterns) -> Self {
Self {
publish,
subscribe,
..Default::default()
}
}
#[cfg(feature = "tokio")]
pub fn deadline(&self) -> Option<tokio::time::Instant> {
self.expires.map(|at| tokio::time::Instant::now() + until(at))
}
pub fn validate(&self) -> crate::Result<()> {
if self.publish.is_empty() && self.subscribe.is_empty() {
return Err(crate::Error::UselessGrant);
}
if self.revalidate.is_some() && self.expires.is_none() {
return Err(crate::Error::UnboundedRevalidate);
}
if self.revalidate.is_some_and(|cadence| cadence.is_zero()) {
return Err(crate::Error::ZeroRevalidate);
}
if self.expires.is_some_and(|expires| until(expires).is_zero()) {
return Err(crate::Error::GrantExpired);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn patterns(texts: &[&str]) -> Patterns {
texts.iter().map(|text| text.parse().unwrap()).collect()
}
#[test]
fn round_trips_in_seconds() {
let grant = Grant {
publish: patterns(&["alice/**"]),
subscribe: patterns(&["**"]),
root: Some("pid/room".into()),
expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
revalidate: Some(Duration::from_secs(60)),
tier: Some("websocket".into()),
peer: true,
};
let json = serde_json::to_value(&grant).unwrap();
assert_eq!(json["expires"], 4_102_444_800_i64);
assert_eq!(json["revalidate"], 60);
assert_eq!(json["publish"], serde_json::json!(["alice/**"]));
assert_eq!(json["peer"], true);
assert_eq!(serde_json::from_value::<Grant>(json).unwrap(), grant);
}
#[test]
fn serializes_to_the_cross_language_vector() {
let grant = Grant {
publish: patterns(&["alice/**"]),
subscribe: patterns(&["**"]),
root: Some("pid/room".into()),
expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
revalidate: Some(Duration::from_secs(60)),
tier: Some("websocket".into()),
peer: true,
};
assert_eq!(
serde_json::to_string(&grant).unwrap(),
r#"{"publish":["alice/**"],"subscribe":["**"],"root":"pid/room","expires":4102444800,"revalidate":60,"tier":"websocket","peer":true}"#
);
}
#[test]
fn empty_fields_are_omitted_and_defaulted() {
let grant = Grant::new(patterns(&["**"]), Patterns::new());
assert_eq!(serde_json::to_string(&grant).unwrap(), r#"{"publish":["**"]}"#);
assert_eq!(serde_json::from_str::<Grant>(r#"{"publish":["**"]}"#).unwrap(), grant);
}
#[test]
fn validate_refuses_nothing_unbounded_and_expired() {
assert!(matches!(Grant::default().validate(), Err(crate::Error::UselessGrant)));
let mut grant = Grant::new(patterns(&["**"]), Patterns::new());
grant.validate().unwrap();
grant.revalidate = Some(Duration::from_secs(1));
assert!(matches!(grant.validate(), Err(crate::Error::UnboundedRevalidate)));
grant.expires = Some(SystemTime::now() - Duration::from_secs(1));
grant.validate().unwrap();
grant.expires = Some(SystemTime::now() - CLOCK_SKEW - Duration::from_secs(1));
assert!(matches!(grant.validate(), Err(crate::Error::GrantExpired)));
grant.expires = Some(SystemTime::now() + Duration::from_secs(60));
grant.validate().unwrap();
grant.revalidate = Some(Duration::ZERO);
assert!(matches!(grant.validate(), Err(crate::Error::ZeroRevalidate)));
}
}