Skip to main content

synd_protocol/
session.rs

1use std::{fmt, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5use crate::CapabilitySet;
6
7/// Opaque identifier assigned by the daemon to an opened session.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct SessionId(String);
10
11impl SessionId {
12    pub fn new(value: impl Into<String>) -> Self {
13        Self(value.into())
14    }
15
16    pub fn as_str(&self) -> &str {
17        &self.0
18    }
19}
20
21impl fmt::Display for SessionId {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        f.write_str(self.as_str())
24    }
25}
26
27/// Request body used by a client to open a daemon session.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct OpenSessionRequest {
30    required_capabilities: CapabilitySet,
31}
32
33impl OpenSessionRequest {
34    pub fn new(required_capabilities: CapabilitySet) -> Self {
35        Self {
36            required_capabilities,
37        }
38    }
39
40    pub fn required_capabilities(&self) -> &CapabilitySet {
41        &self.required_capabilities
42    }
43}
44
45/// Response body returned after the daemon accepts a session.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct OpenSessionResponse {
48    session_id: SessionId,
49    available_capabilities: CapabilitySet,
50    lease: SessionLease,
51}
52
53impl OpenSessionResponse {
54    pub fn new(session_id: SessionId, available_capabilities: CapabilitySet) -> Self {
55        Self::with_lease(session_id, available_capabilities, SessionLease::default())
56    }
57
58    pub fn with_lease(
59        session_id: SessionId,
60        available_capabilities: CapabilitySet,
61        lease: SessionLease,
62    ) -> Self {
63        Self {
64            session_id,
65            available_capabilities,
66            lease,
67        }
68    }
69
70    pub fn session_id(&self) -> &SessionId {
71        &self.session_id
72    }
73
74    pub fn available_capabilities(&self) -> &CapabilitySet {
75        &self.available_capabilities
76    }
77
78    pub fn lease(&self) -> SessionLease {
79        self.lease
80    }
81}
82
83/// Machine-readable reason for rejecting a session open request.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum OpenSessionErrorCode {
87    MissingCapabilities,
88}
89
90/// Error body returned when the daemon rejects a session open request.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct OpenSessionErrorResponse {
93    code: OpenSessionErrorCode,
94    missing_capabilities: CapabilitySet,
95}
96
97impl OpenSessionErrorResponse {
98    pub fn from_missing_capabilities(missing_capabilities: CapabilitySet) -> Self {
99        Self {
100            code: OpenSessionErrorCode::MissingCapabilities,
101            missing_capabilities,
102        }
103    }
104
105    pub fn code(&self) -> OpenSessionErrorCode {
106        self.code
107    }
108
109    pub fn missing_capabilities(&self) -> &CapabilitySet {
110        &self.missing_capabilities
111    }
112}
113
114impl fmt::Display for OpenSessionErrorResponse {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self.code {
117            OpenSessionErrorCode::MissingCapabilities => {
118                write!(
119                    f,
120                    "missing required capabilities: {}",
121                    self.missing_capabilities
122                )
123            }
124        }
125    }
126}
127
128/// Request body used by a client to close a daemon session.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct CloseSessionRequest {
131    session_id: SessionId,
132}
133
134impl CloseSessionRequest {
135    pub fn new(session_id: SessionId) -> Self {
136        Self { session_id }
137    }
138
139    pub fn session_id(&self) -> &SessionId {
140        &self.session_id
141    }
142}
143
144/// Lease granted by the daemon for one accepted session.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct SessionLease(Duration);
148
149impl SessionLease {
150    pub fn new(duration: Duration) -> Self {
151        Self(duration)
152    }
153
154    pub fn duration(self) -> Duration {
155        self.0
156    }
157}
158
159impl Default for SessionLease {
160    fn default() -> Self {
161        Self(Duration::from_secs(30))
162    }
163}
164
165/// Request body used by a client to renew a daemon session lease.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct RenewSessionRequest {
168    session_id: SessionId,
169}
170
171impl RenewSessionRequest {
172    pub fn new(session_id: SessionId) -> Self {
173        Self { session_id }
174    }
175
176    pub fn session_id(&self) -> &SessionId {
177        &self.session_id
178    }
179}
180
181/// Response body returned after the daemon renews a session lease.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct RenewSessionResponse {
184    session_id: SessionId,
185    lease: SessionLease,
186}
187
188impl RenewSessionResponse {
189    pub fn new(session_id: SessionId, lease: SessionLease) -> Self {
190        Self { session_id, lease }
191    }
192
193    pub fn session_id(&self) -> &SessionId {
194        &self.session_id
195    }
196
197    pub fn lease(&self) -> SessionLease {
198        self.lease
199    }
200}
201
202/// Machine-readable reason for rejecting a session renew request.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum RenewSessionErrorCode {
206    UnknownSession,
207}
208
209/// Error body returned when the daemon rejects a session renew request.
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct RenewSessionErrorResponse {
212    code: RenewSessionErrorCode,
213    session_id: SessionId,
214}
215
216impl RenewSessionErrorResponse {
217    pub fn unknown_session(session_id: SessionId) -> Self {
218        Self {
219            code: RenewSessionErrorCode::UnknownSession,
220            session_id,
221        }
222    }
223
224    pub fn code(&self) -> RenewSessionErrorCode {
225        self.code
226    }
227
228    pub fn session_id(&self) -> &SessionId {
229        &self.session_id
230    }
231}
232
233impl fmt::Display for RenewSessionErrorResponse {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        match self.code {
236            RenewSessionErrorCode::UnknownSession => {
237                write!(f, "unknown session {}", self.session_id)
238            }
239        }
240    }
241}
242
243/// Response body returned after the daemon closes a session.
244#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
245pub struct CloseSessionResponse {}
246
247impl CloseSessionResponse {
248    pub fn new() -> Self {
249        Self {}
250    }
251}
252
253/// Machine-readable reason for rejecting a session close request.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum CloseSessionErrorCode {
257    UnknownSession,
258}
259
260/// Error body returned when the daemon rejects a session close request.
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct CloseSessionErrorResponse {
263    code: CloseSessionErrorCode,
264    session_id: SessionId,
265}
266
267impl CloseSessionErrorResponse {
268    pub fn unknown_session(session_id: SessionId) -> Self {
269        Self {
270            code: CloseSessionErrorCode::UnknownSession,
271            session_id,
272        }
273    }
274
275    pub fn code(&self) -> CloseSessionErrorCode {
276        self.code
277    }
278
279    pub fn session_id(&self) -> &SessionId {
280        &self.session_id
281    }
282}
283
284impl fmt::Display for CloseSessionErrorResponse {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match self.code {
287            CloseSessionErrorCode::UnknownSession => {
288                write!(f, "unknown session {}", self.session_id)
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use serde_json::json;
297
298    use crate::{
299        CapabilitySet,
300        session::{
301            CloseSessionErrorResponse, CloseSessionRequest, CloseSessionResponse,
302            OpenSessionErrorResponse, OpenSessionRequest, OpenSessionResponse,
303            RenewSessionErrorResponse, RenewSessionRequest, RenewSessionResponse, SessionId,
304            SessionLease,
305        },
306    };
307
308    #[test]
309    fn serializes_session_contracts() {
310        let cases = [
311            (
312                serde_json::to_value(OpenSessionRequest::new(CapabilitySet::new([
313                    "timeline.read",
314                ])))
315                .unwrap(),
316                json!({
317                    "required_capabilities": {
318                        "names": ["timeline.read"]
319                    }
320                }),
321            ),
322            (
323                serde_json::to_value(OpenSessionResponse::new(
324                    SessionId::new("session-1"),
325                    CapabilitySet::new(["timeline.read"]),
326                ))
327                .unwrap(),
328                json!({
329                    "session_id": "session-1",
330                    "available_capabilities": {
331                        "names": ["timeline.read"]
332                    },
333                    "lease": {
334                        "secs": 30,
335                        "nanos": 0
336                    }
337                }),
338            ),
339            (
340                serde_json::to_value(RenewSessionRequest::new(SessionId::new("session-1")))
341                    .unwrap(),
342                json!({
343                    "session_id": "session-1"
344                }),
345            ),
346            (
347                serde_json::to_value(RenewSessionResponse::new(
348                    SessionId::new("session-1"),
349                    SessionLease::new(std::time::Duration::from_secs(30)),
350                ))
351                .unwrap(),
352                json!({
353                    "session_id": "session-1",
354                    "lease": {
355                        "secs": 30,
356                        "nanos": 0
357                    }
358                }),
359            ),
360            (
361                serde_json::to_value(CloseSessionRequest::new(SessionId::new("session-1")))
362                    .unwrap(),
363                json!({
364                    "session_id": "session-1"
365                }),
366            ),
367            (
368                serde_json::to_value(CloseSessionResponse::new()).unwrap(),
369                json!({}),
370            ),
371            (
372                serde_json::to_value(OpenSessionErrorResponse::from_missing_capabilities(
373                    CapabilitySet::new(["timeline.read"]),
374                ))
375                .unwrap(),
376                json!({
377                    "code": "missing_capabilities",
378                    "missing_capabilities": {
379                        "names": ["timeline.read"]
380                    }
381                }),
382            ),
383            (
384                serde_json::to_value(CloseSessionErrorResponse::unknown_session(SessionId::new(
385                    "session-1",
386                )))
387                .unwrap(),
388                json!({
389                    "code": "unknown_session",
390                    "session_id": "session-1"
391                }),
392            ),
393            (
394                serde_json::to_value(RenewSessionErrorResponse::unknown_session(SessionId::new(
395                    "session-1",
396                )))
397                .unwrap(),
398                json!({
399                    "code": "unknown_session",
400                    "session_id": "session-1"
401                }),
402            ),
403        ];
404
405        for (actual, expected) in cases {
406            assert_eq!(actual, expected);
407        }
408    }
409
410    #[test]
411    fn detects_missing_capabilities() {
412        let required = CapabilitySet::new(["timeline.read", "subscription.write"]);
413        let available = CapabilitySet::new(["timeline.read"]);
414
415        assert_eq!(
416            required.missing_from(&available),
417            CapabilitySet::new(["subscription.write"])
418        );
419    }
420}