1use crate::{DntError, DntResult};
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct ContentType(String);
21
22#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(transparent)]
25pub struct CodecId(String);
26
27#[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 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 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}