Skip to main content

snap_dataplane/
state.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! SNAP data plane state.
15
16use std::{fmt::Display, sync::LazyLock};
17
18use regex::Regex;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21use utoipa::ToSchema;
22
23/// Generic identifier trait.
24pub trait Id {
25    /// Creates an identifier from a `usize`.
26    fn from_usize(val: usize) -> Self;
27    /// Returns the identifier as a `usize`.
28    fn as_usize(&self) -> usize;
29}
30
31/// SNAP node hostname.
32#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, ToSchema, Serialize)]
33#[serde(transparent)]
34pub struct Hostname(String);
35
36/// Hostname validation error.
37#[derive(Debug, Error)]
38pub enum HostnameError {
39    /// Empty hostname.
40    #[error("Hostname is empty")]
41    Empty,
42    /// Hostname too long.
43    #[error("Hostname is too long: {0} characters (maximum is 253)")]
44    TooLong(usize),
45    /// Invalid hostname.
46    #[error("Invalid hostname: {0}")]
47    Invalid(String),
48}
49
50/// Regular expression for validating hostnames according to RFC 1123.
51pub const HOSTNAME_REGEX: &str = r"^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$";
52
53impl Hostname {
54    /// Initialize a valid hostname according to RFC 1123.
55    ///
56    /// A valid hostname must:
57    /// - Be at most 253 characters long
58    /// - Match the pattern: [`HOSTNAME_REGEX`]
59    pub fn new(hostname: String) -> Result<Self, HostnameError> {
60        Self::validate(&hostname)?;
61        Ok(Self(hostname))
62    }
63
64    fn validate(hostname: &str) -> Result<(), HostnameError> {
65        if hostname.len() > 253 {
66            return Err(HostnameError::TooLong(hostname.len()));
67        }
68
69        static RE: LazyLock<Regex> =
70            LazyLock::new(|| Regex::new(HOSTNAME_REGEX).expect("valid regex"));
71        if !RE.is_match(hostname) {
72            return Err(HostnameError::Invalid(hostname.to_string()));
73        }
74
75        Ok(())
76    }
77}
78
79impl Display for Hostname {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}", self.0)
82    }
83}
84
85impl From<Hostname> for String {
86    fn from(value: Hostname) -> Self {
87        value.0
88    }
89}
90
91impl TryFrom<String> for Hostname {
92    type Error = HostnameError;
93
94    fn try_from(value: String) -> Result<Self, Self::Error> {
95        Self::new(value)
96    }
97}
98
99impl TryFrom<&str> for Hostname {
100    type Error = HostnameError;
101
102    fn try_from(value: &str) -> Result<Self, Self::Error> {
103        Self::new(value.to_string())
104    }
105}
106
107// Custom deserializer that validates the hostname
108impl<'de> Deserialize<'de> for Hostname {
109    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110    where
111        D: serde::Deserializer<'de>,
112    {
113        let s = String::deserialize(deserializer)?;
114        Hostname::new(s).map_err(serde::de::Error::custom)
115    }
116}