Skip to main content

appcore_dnt/
ids.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: ids.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/02 00:04:12 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/02 00:04:12 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! DNT identifier types.
12
13use crate::{DntError, DntResult};
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17/// Logical payload content type stored in the authenticated header.
18#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct ContentType(String);
21
22/// Payload codec identifier stored in the authenticated header.
23#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(transparent)]
25pub struct CodecId(String);
26
27/// Rotation-aware key identifier stored in the authenticated header.
28#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct KeyId(String);
31
32macro_rules! impl_dnt_id {
33    ($ty:ident, $field:literal, $max:expr, $allow_slash:expr) => {
34        impl $ty {
35            /// Creates and validates an identifier.
36            pub fn new(value: impl Into<String>) -> DntResult<Self> {
37                let value = value.into();
38                validate_id($field, &value, $max, $allow_slash)?;
39                Ok(Self(value))
40            }
41
42            /// Returns the identifier as a string slice.
43            pub fn as_str(&self) -> &str {
44                &self.0
45            }
46        }
47
48        impl fmt::Debug for $ty {
49            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50                formatter
51                    .debug_tuple(stringify!($ty))
52                    .field(&self.0)
53                    .finish()
54            }
55        }
56    };
57}
58
59impl_dnt_id!(ContentType, "content_type", 128, true);
60impl_dnt_id!(CodecId, "codec_id", 64, false);
61impl_dnt_id!(KeyId, "key_id", 128, false);
62
63fn validate_id(_field: &'static str, value: &str, max: usize, allow_slash: bool) -> DntResult<()> {
64    if value.is_empty() || value.len() > max {
65        return Err(DntError::InvalidFormat);
66    }
67    let valid = value.bytes().all(|byte| {
68        byte.is_ascii_alphanumeric()
69            || matches!(byte, b'.' | b'-' | b'_')
70            || (allow_slash && byte == b'/')
71    });
72    if valid && !value.starts_with('/') && !value.ends_with('/') {
73        return Ok(());
74    }
75    Err(DntError::InvalidFormat)
76}