1use moq_pattern::Patterns;
2use serde::{Deserialize, Serialize};
3use serde_with::{DurationSeconds, TimestampSeconds, serde_as};
4use std::time::{Duration, SystemTime};
5
6pub(crate) const CLOCK_SKEW: Duration = Duration::from_secs(5);
8
9fn until(at: SystemTime) -> Duration {
13 match at.duration_since(SystemTime::now()) {
14 Ok(remaining) => remaining,
15 Err(late) => CLOCK_SKEW.saturating_sub(late.duration()),
16 }
17}
18
19#[serde_as]
26#[serde_with::skip_serializing_none]
27#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(default)]
29#[non_exhaustive]
30pub struct Grant {
31 #[serde(skip_serializing_if = "Patterns::is_empty")]
33 pub publish: Patterns,
34
35 #[serde(skip_serializing_if = "Patterns::is_empty")]
37 pub subscribe: Patterns,
38
39 pub root: Option<String>,
42
43 #[serde_as(as = "Option<TimestampSeconds<i64>>")]
45 pub expires: Option<SystemTime>,
46
47 #[serde_as(as = "Option<DurationSeconds<u64>>")]
49 pub revalidate: Option<Duration>,
50
51 pub tier: Option<String>,
53
54 #[serde(skip_serializing_if = "std::ops::Not::not")]
57 pub peer: bool,
58}
59
60impl Grant {
61 pub fn new(publish: Patterns, subscribe: Patterns) -> Self {
63 Self {
64 publish,
65 subscribe,
66 ..Default::default()
67 }
68 }
69
70 #[cfg(feature = "tokio")]
72 pub fn deadline(&self) -> Option<tokio::time::Instant> {
73 self.expires.map(|at| tokio::time::Instant::now() + until(at))
74 }
75
76 pub fn validate(&self) -> crate::Result<()> {
80 if self.publish.is_empty() && self.subscribe.is_empty() {
81 return Err(crate::Error::UselessGrant);
82 }
83 if self.revalidate.is_some() && self.expires.is_none() {
84 return Err(crate::Error::UnboundedRevalidate);
85 }
86 if self.revalidate.is_some_and(|cadence| cadence.is_zero()) {
88 return Err(crate::Error::ZeroRevalidate);
89 }
90 if self.expires.is_some_and(|expires| until(expires).is_zero()) {
91 return Err(crate::Error::GrantExpired);
92 }
93 Ok(())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn patterns(texts: &[&str]) -> Patterns {
102 texts.iter().map(|text| text.parse().unwrap()).collect()
103 }
104
105 #[test]
106 fn round_trips_in_seconds() {
107 let grant = Grant {
108 publish: patterns(&["alice/**"]),
109 subscribe: patterns(&["**"]),
110 root: Some("pid/room".into()),
111 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
112 revalidate: Some(Duration::from_secs(60)),
113 tier: Some("websocket".into()),
114 peer: true,
115 };
116 let json = serde_json::to_value(&grant).unwrap();
117 assert_eq!(json["expires"], 4_102_444_800_i64);
118 assert_eq!(json["revalidate"], 60);
119 assert_eq!(json["publish"], serde_json::json!(["alice/**"]));
120 assert_eq!(json["peer"], true);
121 assert_eq!(serde_json::from_value::<Grant>(json).unwrap(), grant);
122 }
123
124 #[test]
127 fn serializes_to_the_cross_language_vector() {
128 let grant = Grant {
129 publish: patterns(&["alice/**"]),
130 subscribe: patterns(&["**"]),
131 root: Some("pid/room".into()),
132 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
133 revalidate: Some(Duration::from_secs(60)),
134 tier: Some("websocket".into()),
135 peer: true,
136 };
137 assert_eq!(
138 serde_json::to_string(&grant).unwrap(),
139 r#"{"publish":["alice/**"],"subscribe":["**"],"root":"pid/room","expires":4102444800,"revalidate":60,"tier":"websocket","peer":true}"#
140 );
141 }
142
143 #[test]
144 fn empty_fields_are_omitted_and_defaulted() {
145 let grant = Grant::new(patterns(&["**"]), Patterns::new());
146 assert_eq!(serde_json::to_string(&grant).unwrap(), r#"{"publish":["**"]}"#);
147 assert_eq!(serde_json::from_str::<Grant>(r#"{"publish":["**"]}"#).unwrap(), grant);
148 }
149
150 #[test]
151 fn validate_refuses_nothing_unbounded_and_expired() {
152 assert!(matches!(Grant::default().validate(), Err(crate::Error::UselessGrant)));
153
154 let mut grant = Grant::new(patterns(&["**"]), Patterns::new());
155 grant.validate().unwrap();
156
157 grant.revalidate = Some(Duration::from_secs(1));
158 assert!(matches!(grant.validate(), Err(crate::Error::UnboundedRevalidate)));
159
160 grant.expires = Some(SystemTime::now() - Duration::from_secs(1));
161 grant.validate().unwrap();
162
163 grant.expires = Some(SystemTime::now() - CLOCK_SKEW - Duration::from_secs(1));
164 assert!(matches!(grant.validate(), Err(crate::Error::GrantExpired)));
165
166 grant.expires = Some(SystemTime::now() + Duration::from_secs(60));
167 grant.validate().unwrap();
168
169 grant.revalidate = Some(Duration::ZERO);
170 assert!(matches!(grant.validate(), Err(crate::Error::ZeroRevalidate)));
171 }
172}