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
9pub(crate) fn 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 pub fn validate(&self) -> crate::Result<()> {
74 if self.publish.is_empty() && self.subscribe.is_empty() {
75 return Err(crate::Error::UselessGrant);
76 }
77 if self.revalidate.is_some() && self.expires.is_none() {
78 return Err(crate::Error::UnboundedRevalidate);
79 }
80 if self.revalidate.is_some_and(|cadence| cadence.is_zero()) {
82 return Err(crate::Error::ZeroRevalidate);
83 }
84 if self.expires.is_some_and(|expires| until(expires).is_zero()) {
85 return Err(crate::Error::GrantExpired);
86 }
87 Ok(())
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn patterns(texts: &[&str]) -> Patterns {
96 texts.iter().map(|text| text.parse().unwrap()).collect()
97 }
98
99 #[test]
100 fn round_trips_in_seconds() {
101 let grant = Grant {
102 publish: patterns(&["alice/**"]),
103 subscribe: patterns(&["**"]),
104 root: Some("pid/room".into()),
105 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
106 revalidate: Some(Duration::from_secs(60)),
107 tier: Some("websocket".into()),
108 peer: true,
109 };
110 let json = serde_json::to_value(&grant).unwrap();
111 assert_eq!(json["expires"], 4_102_444_800_i64);
112 assert_eq!(json["revalidate"], 60);
113 assert_eq!(json["publish"], serde_json::json!(["alice/**"]));
114 assert_eq!(json["peer"], true);
115 assert_eq!(serde_json::from_value::<Grant>(json).unwrap(), grant);
116 }
117
118 #[test]
121 fn serializes_to_the_cross_language_vector() {
122 let grant = Grant {
123 publish: patterns(&["alice/**"]),
124 subscribe: patterns(&["**"]),
125 root: Some("pid/room".into()),
126 expires: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(4_102_444_800)),
127 revalidate: Some(Duration::from_secs(60)),
128 tier: Some("websocket".into()),
129 peer: true,
130 };
131 assert_eq!(
132 serde_json::to_string(&grant).unwrap(),
133 r#"{"publish":["alice/**"],"subscribe":["**"],"root":"pid/room","expires":4102444800,"revalidate":60,"tier":"websocket","peer":true}"#
134 );
135 }
136
137 #[test]
138 fn empty_fields_are_omitted_and_defaulted() {
139 let grant = Grant::new(patterns(&["**"]), Patterns::new());
140 assert_eq!(serde_json::to_string(&grant).unwrap(), r#"{"publish":["**"]}"#);
141 assert_eq!(serde_json::from_str::<Grant>(r#"{"publish":["**"]}"#).unwrap(), grant);
142 }
143
144 #[test]
145 fn validate_refuses_nothing_unbounded_and_expired() {
146 assert!(matches!(Grant::default().validate(), Err(crate::Error::UselessGrant)));
147
148 let mut grant = Grant::new(patterns(&["**"]), Patterns::new());
149 grant.validate().unwrap();
150
151 grant.revalidate = Some(Duration::from_secs(1));
152 assert!(matches!(grant.validate(), Err(crate::Error::UnboundedRevalidate)));
153
154 grant.expires = Some(SystemTime::now() - Duration::from_secs(1));
155 grant.validate().unwrap();
156
157 grant.expires = Some(SystemTime::now() - CLOCK_SKEW - Duration::from_secs(1));
158 assert!(matches!(grant.validate(), Err(crate::Error::GrantExpired)));
159
160 grant.expires = Some(SystemTime::now() + Duration::from_secs(60));
161 grant.validate().unwrap();
162
163 grant.revalidate = Some(Duration::ZERO);
164 assert!(matches!(grant.validate(), Err(crate::Error::ZeroRevalidate)));
165 }
166}