1use crate::NnrpError;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4#[repr(u8)]
5pub enum MessageType {
6 ClientHello = 0x01,
7 ServerHelloAck = 0x02,
8 SessionPatch = 0x03,
9 SessionPatchAck = 0x04,
10 Close = 0x05,
11 Error = 0x06,
12 SessionOpen = 0x07,
13 SessionOpenAck = 0x08,
14 SessionClose = 0x09,
15 SessionCloseAck = 0x0a,
16 FrameSubmit = 0x10,
17 FrameCancel = 0x11,
18 ResultPush = 0x12,
19 ResultDrop = 0x13,
20 CachePut = 0x14,
21 CacheAck = 0x15,
22 CacheInvalidate = 0x16,
23 FlowUpdate = 0x17,
24 ResultHint = 0x18,
25 TransportProbe = 0x19,
26 TransportProbeAck = 0x1a,
27 SessionMigrate = 0x1b,
28 SessionMigrateAck = 0x1c,
29 Ping = 0x20,
30 Pong = 0x21,
31}
32
33impl MessageType {
34 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
35 let message_type = match value {
36 0x01 => Self::ClientHello,
37 0x02 => Self::ServerHelloAck,
38 0x03 => Self::SessionPatch,
39 0x04 => Self::SessionPatchAck,
40 0x05 => Self::Close,
41 0x06 => Self::Error,
42 0x07 => Self::SessionOpen,
43 0x08 => Self::SessionOpenAck,
44 0x09 => Self::SessionClose,
45 0x0a => Self::SessionCloseAck,
46 0x10 => Self::FrameSubmit,
47 0x11 => Self::FrameCancel,
48 0x12 => Self::ResultPush,
49 0x13 => Self::ResultDrop,
50 0x14 => Self::CachePut,
51 0x15 => Self::CacheAck,
52 0x16 => Self::CacheInvalidate,
53 0x17 => Self::FlowUpdate,
54 0x18 => Self::ResultHint,
55 0x19 => Self::TransportProbe,
56 0x1a => Self::TransportProbeAck,
57 0x1b => Self::SessionMigrate,
58 0x1c => Self::SessionMigrateAck,
59 0x20 => Self::Ping,
60 0x21 => Self::Pong,
61 _ => return Err(NnrpError::UnknownMessageType(value)),
62 };
63
64 Ok(message_type)
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[repr(u8)]
70pub enum SessionPriorityClass {
71 Interactive = 0,
72 Balanced = 1,
73 Background = 2,
74}
75
76impl SessionPriorityClass {
77 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
78 match value {
79 0 => Ok(Self::Interactive),
80 1 => Ok(Self::Balanced),
81 2 => Ok(Self::Background),
82 _ => Err(NnrpError::UnknownEnumValue {
83 enum_name: "session_priority_class",
84 value: value as u64,
85 }),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[repr(u8)]
92pub enum SessionStatus {
93 Opened = 0,
94 Rejected = 1,
95 RetryLater = 2,
96 Resumed = 3,
97}
98
99impl SessionStatus {
100 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
101 match value {
102 0 => Ok(Self::Opened),
103 1 => Ok(Self::Rejected),
104 2 => Ok(Self::RetryLater),
105 3 => Ok(Self::Resumed),
106 _ => Err(NnrpError::UnknownEnumValue {
107 enum_name: "session_status",
108 value: value as u64,
109 }),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[repr(u16)]
116pub enum SessionCloseReason {
117 Normal = 0,
118 ClientShutdown = 1,
119 ServerShutdown = 2,
120 IdleTimeout = 3,
121 ProtocolError = 4,
122 AuthRevoked = 5,
123}
124
125impl SessionCloseReason {
126 pub fn try_from_u16(value: u16) -> Result<Self, NnrpError> {
127 match value {
128 0 => Ok(Self::Normal),
129 1 => Ok(Self::ClientShutdown),
130 2 => Ok(Self::ServerShutdown),
131 3 => Ok(Self::IdleTimeout),
132 4 => Ok(Self::ProtocolError),
133 5 => Ok(Self::AuthRevoked),
134 _ => Err(NnrpError::UnknownEnumValue {
135 enum_name: "close_reason",
136 value: value as u64,
137 }),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[repr(u8)]
144pub enum InFlightPolicy {
145 Drain = 0,
146 Abort = 1,
147}
148
149impl InFlightPolicy {
150 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
151 match value {
152 0 => Ok(Self::Drain),
153 1 => Ok(Self::Abort),
154 _ => Err(NnrpError::UnknownEnumValue {
155 enum_name: "in_flight_policy",
156 value: value as u64,
157 }),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[repr(u8)]
164pub enum SessionCloseStatus {
165 Acknowledged = 0,
166 Draining = 1,
167 Closed = 2,
168 Rejected = 3,
169}
170
171impl SessionCloseStatus {
172 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
173 match value {
174 0 => Ok(Self::Acknowledged),
175 1 => Ok(Self::Draining),
176 2 => Ok(Self::Closed),
177 3 => Ok(Self::Rejected),
178 _ => Err(NnrpError::UnknownEnumValue {
179 enum_name: "close_status",
180 value: value as u64,
181 }),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187#[repr(u8)]
188pub enum OperationState {
189 Accepted = 0,
190 Running = 1,
191 Partial = 2,
192 WaitingTool = 3,
193 Superseded = 4,
194 Cancelled = 5,
195 Failed = 6,
196 Completed = 7,
197}
198
199impl OperationState {
200 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
201 match value {
202 0 => Ok(Self::Accepted),
203 1 => Ok(Self::Running),
204 2 => Ok(Self::Partial),
205 3 => Ok(Self::WaitingTool),
206 4 => Ok(Self::Superseded),
207 5 => Ok(Self::Cancelled),
208 6 => Ok(Self::Failed),
209 7 => Ok(Self::Completed),
210 _ => Err(NnrpError::UnknownEnumValue {
211 enum_name: "operation_state",
212 value: value as u64,
213 }),
214 }
215 }
216
217 pub fn is_terminal(self) -> bool {
218 matches!(
219 self,
220 Self::Superseded | Self::Cancelled | Self::Failed | Self::Completed
221 )
222 }
223
224 pub fn can_transition_to(self, next: Self) -> bool {
225 if self.is_terminal() {
226 return false;
227 }
228
229 matches!(
230 (self, next),
231 (Self::Accepted, Self::Running)
232 | (Self::Accepted, Self::Cancelled)
233 | (Self::Accepted, Self::Failed)
234 | (Self::Accepted, Self::Superseded)
235 | (Self::Running, Self::Partial)
236 | (Self::Running, Self::WaitingTool)
237 | (Self::Running, Self::Cancelled)
238 | (Self::Running, Self::Failed)
239 | (Self::Running, Self::Completed)
240 | (Self::Running, Self::Superseded)
241 | (Self::Partial, Self::Partial)
242 | (Self::Partial, Self::WaitingTool)
243 | (Self::Partial, Self::Cancelled)
244 | (Self::Partial, Self::Failed)
245 | (Self::Partial, Self::Completed)
246 | (Self::Partial, Self::Superseded)
247 | (Self::WaitingTool, Self::Running)
248 | (Self::WaitingTool, Self::Cancelled)
249 | (Self::WaitingTool, Self::Failed)
250 | (Self::WaitingTool, Self::Superseded)
251 )
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[repr(u8)]
257pub enum CancelScope {
258 Operation = 0,
259 Subtree = 1,
260 Group = 2,
261 Session = 3,
262}
263
264impl CancelScope {
265 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
266 match value {
267 0 => Ok(Self::Operation),
268 1 => Ok(Self::Subtree),
269 2 => Ok(Self::Group),
270 3 => Ok(Self::Session),
271 _ => Err(NnrpError::UnknownEnumValue {
272 enum_name: "cancel_scope",
273 value: value as u64,
274 }),
275 }
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280#[repr(u8)]
281pub enum FlowScopeKind {
282 Connection = 0,
283 Session = 1,
284 Operation = 2,
285}
286
287impl FlowScopeKind {
288 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
289 match value {
290 0 => Ok(Self::Connection),
291 1 => Ok(Self::Session),
292 2 => Ok(Self::Operation),
293 _ => Err(NnrpError::UnknownEnumValue {
294 enum_name: "scope_kind",
295 value: value as u64,
296 }),
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302#[repr(u8)]
303pub enum FlowUpdateReason {
304 Grant = 0,
305 Reduce = 1,
306 Pause = 2,
307 Resume = 3,
308 Congestion = 4,
309}
310
311impl FlowUpdateReason {
312 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
313 match value {
314 0 => Ok(Self::Grant),
315 1 => Ok(Self::Reduce),
316 2 => Ok(Self::Pause),
317 3 => Ok(Self::Resume),
318 4 => Ok(Self::Congestion),
319 _ => Err(NnrpError::UnknownEnumValue {
320 enum_name: "update_reason",
321 value: value as u64,
322 }),
323 }
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328#[repr(u8)]
329pub enum BackpressureLevel {
330 None = 0,
331 Soft = 1,
332 Hard = 2,
333}
334
335impl BackpressureLevel {
336 pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
337 match value {
338 0 => Ok(Self::None),
339 1 => Ok(Self::Soft),
340 2 => Ok(Self::Hard),
341 _ => Err(NnrpError::UnknownEnumValue {
342 enum_name: "backpressure_level",
343 value: value as u64,
344 }),
345 }
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct HeaderFlags(pub u32);
351
352impl HeaderFlags {
353 pub const NONE: Self = Self(0);
354 pub const ACK_REQUIRED: Self = Self(0x0000_0001);
355 pub const CAN_DROP: Self = Self(0x0000_0002);
356 pub const STALE: Self = Self(0x0000_0004);
357 pub const EOS: Self = Self(0x0000_0008);
358 pub const RETRANSMIT: Self = Self(0x0000_0010);
359 pub const KEYFRAME: Self = Self(0x0000_0020);
360 pub const KNOWN_MASK: u32 = Self::ACK_REQUIRED.0
361 | Self::CAN_DROP.0
362 | Self::STALE.0
363 | Self::EOS.0
364 | Self::RETRANSMIT.0
365 | Self::KEYFRAME.0;
366
367 pub fn validate_known(self) -> Result<(), NnrpError> {
368 if self.0 & !Self::KNOWN_MASK != 0 {
369 return Err(NnrpError::ReservedBitsSet {
370 value: self.0 as u64,
371 allowed: Self::KNOWN_MASK as u64,
372 });
373 }
374
375 Ok(())
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use crate::NnrpError;
382
383 use super::{
384 BackpressureLevel, CancelScope, FlowScopeKind, FlowUpdateReason, HeaderFlags,
385 InFlightPolicy, MessageType, OperationState, SessionCloseReason, SessionCloseStatus,
386 SessionPriorityClass, SessionStatus,
387 };
388
389 #[test]
390 fn preview3_message_type_assignments_are_frozen() {
391 let assignments = [
392 (0x01, MessageType::ClientHello),
393 (0x02, MessageType::ServerHelloAck),
394 (0x03, MessageType::SessionPatch),
395 (0x04, MessageType::SessionPatchAck),
396 (0x05, MessageType::Close),
397 (0x06, MessageType::Error),
398 (0x07, MessageType::SessionOpen),
399 (0x08, MessageType::SessionOpenAck),
400 (0x09, MessageType::SessionClose),
401 (0x0a, MessageType::SessionCloseAck),
402 (0x10, MessageType::FrameSubmit),
403 (0x11, MessageType::FrameCancel),
404 (0x12, MessageType::ResultPush),
405 (0x13, MessageType::ResultDrop),
406 (0x14, MessageType::CachePut),
407 (0x15, MessageType::CacheAck),
408 (0x16, MessageType::CacheInvalidate),
409 (0x17, MessageType::FlowUpdate),
410 (0x18, MessageType::ResultHint),
411 (0x19, MessageType::TransportProbe),
412 (0x1a, MessageType::TransportProbeAck),
413 (0x1b, MessageType::SessionMigrate),
414 (0x1c, MessageType::SessionMigrateAck),
415 (0x20, MessageType::Ping),
416 (0x21, MessageType::Pong),
417 ];
418
419 for (wire_value, message_type) in assignments {
420 assert_eq!(MessageType::try_from_u8(wire_value), Ok(message_type));
421 assert_eq!(message_type as u8, wire_value);
422 }
423 }
424
425 #[test]
426 fn message_type_rejects_unknown_values() {
427 assert_eq!(
428 MessageType::try_from_u8(0xff),
429 Err(NnrpError::UnknownMessageType(0xff))
430 );
431 }
432
433 #[test]
434 fn header_flags_accept_known_bits_and_reject_reserved_bits() {
435 let all_known = HeaderFlags(
436 HeaderFlags::NONE.0
437 | HeaderFlags::ACK_REQUIRED.0
438 | HeaderFlags::CAN_DROP.0
439 | HeaderFlags::STALE.0
440 | HeaderFlags::EOS.0
441 | HeaderFlags::RETRANSMIT.0
442 | HeaderFlags::KEYFRAME.0,
443 );
444
445 assert_eq!(HeaderFlags::KNOWN_MASK, 0x0000_003f);
446 assert_eq!(all_known.validate_known(), Ok(()));
447 assert_eq!(
448 HeaderFlags(0x0000_0040).validate_known(),
449 Err(NnrpError::ReservedBitsSet {
450 value: 0x0000_0040,
451 allowed: 0x0000_003f
452 })
453 );
454 }
455
456 #[test]
457 fn preview3_session_and_operation_enums_are_frozen() {
458 assert_enum_u8(
459 "session_priority_class",
460 SessionPriorityClass::try_from_u8,
461 0,
462 2,
463 );
464 assert_eq!(
465 SessionPriorityClass::try_from_u8(3),
466 Err(NnrpError::UnknownEnumValue {
467 enum_name: "session_priority_class",
468 value: 3
469 })
470 );
471
472 assert_enum_u8("session_status", SessionStatus::try_from_u8, 0, 3);
473 assert_enum_u16("close_reason", SessionCloseReason::try_from_u16, 0, 5);
474 assert_enum_u8("in_flight_policy", InFlightPolicy::try_from_u8, 0, 1);
475 assert_enum_u8("close_status", SessionCloseStatus::try_from_u8, 0, 3);
476 assert_enum_u8("operation_state", OperationState::try_from_u8, 0, 7);
477 assert_enum_u8("cancel_scope", CancelScope::try_from_u8, 0, 3);
478 }
479
480 #[test]
481 fn preview3_flow_enums_are_frozen() {
482 assert_enum_u8("scope_kind", FlowScopeKind::try_from_u8, 0, 2);
483 assert_enum_u8("update_reason", FlowUpdateReason::try_from_u8, 0, 4);
484 assert_enum_u8("backpressure_level", BackpressureLevel::try_from_u8, 0, 2);
485 }
486
487 #[test]
488 fn operation_state_terminal_and_transition_rules_are_stable() {
489 assert!(!OperationState::Accepted.is_terminal());
490 assert!(OperationState::Completed.is_terminal());
491 assert!(OperationState::Running.can_transition_to(OperationState::Partial));
492 assert!(OperationState::Partial.can_transition_to(OperationState::Completed));
493 assert!(OperationState::WaitingTool.can_transition_to(OperationState::Running));
494 assert!(!OperationState::Completed.can_transition_to(OperationState::Running));
495 assert!(!OperationState::Accepted.can_transition_to(OperationState::Partial));
496 }
497
498 fn assert_enum_u8<T: Copy>(
499 enum_name: &'static str,
500 parse: fn(u8) -> Result<T, NnrpError>,
501 first: u8,
502 last: u8,
503 ) {
504 for value in first..=last {
505 assert!(
506 parse(value).is_ok(),
507 "{enum_name} value {value} should parse"
508 );
509 }
510 assert_eq!(
511 parse(last + 1).map(|_| ()),
512 Err(NnrpError::UnknownEnumValue {
513 enum_name,
514 value: (last + 1) as u64
515 })
516 );
517 }
518
519 fn assert_enum_u16<T: Copy>(
520 enum_name: &'static str,
521 parse: fn(u16) -> Result<T, NnrpError>,
522 first: u16,
523 last: u16,
524 ) {
525 for value in first..=last {
526 assert!(
527 parse(value).is_ok(),
528 "{enum_name} value {value} should parse"
529 );
530 }
531 assert_eq!(
532 parse(last + 1).map(|_| ()),
533 Err(NnrpError::UnknownEnumValue {
534 enum_name,
535 value: (last + 1) as u64
536 })
537 );
538 }
539}