1use std::{fmt, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5use crate::CapabilitySet;
6
7#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum OpenSessionErrorCode {
87 MissingCapabilities,
88}
89
90#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205pub enum RenewSessionErrorCode {
206 UnknownSession,
207}
208
209#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum CloseSessionErrorCode {
257 UnknownSession,
258}
259
260#[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}