Skip to main content

relaygate_destination/
name.rs

1use std::{fmt, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5use crate::DestinationError;
6
7/// Maximum encoded length of one namespace or destination-name label.
8pub const MAX_LABEL_BYTES: usize = 63;
9/// Maximum encoded length of a complete [`DestinationName`].
10pub const MAX_DESTINATION_NAME_BYTES: usize = 253;
11
12/// A validated single-label tenant or application routing namespace.
13///
14/// Namespaces contain lowercase ASCII letters, digits, and interior hyphens.
15/// Unlike [`DestinationName`], a namespace cannot contain dots.
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[serde(try_from = "String", into = "String")]
18pub struct Namespace(Box<str>);
19
20impl Namespace {
21    /// Validates one lowercase ASCII label; dots are rejected.
22    pub fn new(value: &str) -> Result<Self, DestinationError> {
23        if value.contains('.') {
24            return Err(DestinationError::InvalidNamespace);
25        }
26        validate_label(value)?;
27        Ok(Self(value.into()))
28    }
29
30    #[must_use]
31    /// Borrows the canonical validated text.
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl FromStr for Namespace {
38    type Err = DestinationError;
39
40    fn from_str(value: &str) -> Result<Self, Self::Err> {
41        Self::new(value)
42    }
43}
44
45impl TryFrom<String> for Namespace {
46    type Error = DestinationError;
47
48    fn try_from(value: String) -> Result<Self, Self::Error> {
49        Self::new(&value)
50    }
51}
52
53impl From<Namespace> for String {
54    fn from(value: Namespace) -> Self {
55        value.0.into_string()
56    }
57}
58
59impl fmt::Display for Namespace {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        formatter.write_str(self.as_str())
62    }
63}
64
65/// A validated dot-separated destination name within a [`Namespace`].
66///
67/// Each label contains lowercase ASCII letters, digits, and interior hyphens.
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69#[serde(try_from = "String", into = "String")]
70pub struct DestinationName(Box<str>);
71
72impl DestinationName {
73    /// Validates the total length and every dot-separated label.
74    pub fn new(value: &str) -> Result<Self, DestinationError> {
75        if value.len() > MAX_DESTINATION_NAME_BYTES {
76            return Err(DestinationError::DestinationNameLength);
77        }
78        for label in value.split('.') {
79            validate_label(label)?;
80        }
81        Ok(Self(value.into()))
82    }
83
84    #[must_use]
85    /// Borrows the canonical validated text.
86    pub fn as_str(&self) -> &str {
87        &self.0
88    }
89
90    /// Returns whether this name is below `prefix` on a whole-label boundary.
91    /// The prefix itself is not its descendant.
92    #[must_use]
93    pub fn is_descendant_of(&self, prefix: &Self) -> bool {
94        self.as_str()
95            .strip_prefix(prefix.as_str())
96            .is_some_and(|suffix| suffix.starts_with('.'))
97    }
98}
99
100impl FromStr for DestinationName {
101    type Err = DestinationError;
102
103    fn from_str(value: &str) -> Result<Self, Self::Err> {
104        Self::new(value)
105    }
106}
107
108impl TryFrom<String> for DestinationName {
109    type Error = DestinationError;
110
111    fn try_from(value: String) -> Result<Self, Self::Error> {
112        Self::new(&value)
113    }
114}
115
116impl From<DestinationName> for String {
117    fn from(value: DestinationName) -> Self {
118        value.0.into_string()
119    }
120}
121
122impl fmt::Display for DestinationName {
123    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        formatter.write_str(self.as_str())
125    }
126}
127
128fn validate_label(value: &str) -> Result<(), DestinationError> {
129    let bytes = value.as_bytes();
130    if bytes.is_empty() || bytes.len() > MAX_LABEL_BYTES {
131        return Err(DestinationError::LabelLength);
132    }
133
134    let is_alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
135    if !bytes.first().is_some_and(is_alphanumeric) || !bytes.last().is_some_and(is_alphanumeric) {
136        return Err(DestinationError::LabelBoundary);
137    }
138    if !bytes
139        .iter()
140        .all(|byte| is_alphanumeric(byte) || *byte == b'-')
141    {
142        return Err(DestinationError::LabelCharacter);
143    }
144    Ok(())
145}