Skip to main content

uqa_core/notifications/
identity.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bounded, canonical identities without transport or random-source ownership.
8
9use std::{fmt, str::FromStr};
10
11/// Invalid subscription identity syntax; diagnostics never retain the input.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
13pub enum InvalidNotificationIdentity {
14    #[error("notification epoch must be a canonical UUID version 4")]
15    Epoch,
16    #[error("notification request identity must contain 1 to 128 ASCII letters, digits, - or _")]
17    RequestId,
18}
19
20/// One UUID version 4 identifying a subscription epoch.
21///
22/// Constructors validate representation only. The delivery adapter must obtain fresh random bytes for each new registration; Core does not own an RNG.
23#[derive(Clone, Copy, PartialEq, Eq, Hash)]
24pub struct NotificationEpoch([u8; 16]);
25
26impl NotificationEpoch {
27    pub const fn from_bytes(bytes: [u8; 16]) -> Result<Self, InvalidNotificationIdentity> {
28        if bytes[6] >> 4 != 4 || bytes[8] >> 6 != 2 {
29            return Err(InvalidNotificationIdentity::Epoch);
30        }
31        Ok(Self(bytes))
32    }
33
34    pub const fn as_bytes(&self) -> &[u8; 16] {
35        &self.0
36    }
37}
38
39impl FromStr for NotificationEpoch {
40    type Err = InvalidNotificationIdentity;
41
42    fn from_str(value: &str) -> Result<Self, Self::Err> {
43        let input = value.as_bytes();
44        if input.len() != 36 {
45            return Err(InvalidNotificationIdentity::Epoch);
46        }
47        let mut bytes = [0; 16];
48        let mut offset = 0;
49        for (index, byte) in bytes.iter_mut().enumerate() {
50            if matches!(index, 4 | 6 | 8 | 10) {
51                if input[offset] != b'-' {
52                    return Err(InvalidNotificationIdentity::Epoch);
53                }
54                offset += 1;
55            }
56            let digit = |value| match value {
57                b'0'..=b'9' => Ok(value - b'0'),
58                b'a'..=b'f' => Ok(value - b'a' + 10),
59                _ => Err(InvalidNotificationIdentity::Epoch),
60            };
61            *byte = digit(input[offset])? << 4 | digit(input[offset + 1])?;
62            offset += 2;
63        }
64        Self::from_bytes(bytes)
65    }
66}
67
68impl fmt::Display for NotificationEpoch {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        for (index, byte) in self.0.iter().enumerate() {
71            if matches!(index, 4 | 6 | 8 | 10) {
72                f.write_str("-")?;
73            }
74            write!(f, "{byte:02x}")?;
75        }
76        Ok(())
77    }
78}
79
80impl fmt::Debug for NotificationEpoch {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        fmt::Display::fmt(self, f)
83    }
84}
85
86/// Bounded request metadata, absent from direct embedded subscriptions.
87#[derive(Debug, Clone, PartialEq, Eq, Hash)]
88pub struct NotificationRequestId(Box<str>);
89
90impl NotificationRequestId {
91    pub fn new(value: &str) -> Result<Self, InvalidNotificationIdentity> {
92        if value.is_empty()
93            || value.len() > 128
94            || !value
95                .bytes()
96                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
97        {
98            return Err(InvalidNotificationIdentity::RequestId);
99        }
100        Ok(Self(value.into()))
101    }
102
103    pub fn as_str(&self) -> &str {
104        &self.0
105    }
106}
107
108impl fmt::Display for NotificationRequestId {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.as_str())
111    }
112}