Skip to main content

astarte_interfaces/interface/
name.rs

1// This file is part of Astarte.
2//
3// Copyright 2025 SECO Mind Srl
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//    http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16//
17// SPDX-License-Identifier: Apache-2.0
18
19//! Name of an interface
20//!
21//! This has to be an unique, alphanumeric reverse internet domain name, shorter than 128
22//! characters.
23
24use std::{borrow::Cow, fmt::Display, sync::OnceLock};
25
26use regex::Regex;
27
28/// Error when parsing an [`InterfaceName`].
29#[derive(Debug, thiserror::Error)]
30pub enum InterfaceNameError {
31    /// Interface name cannot be empty
32    #[error("name cannot be empty")]
33    Empty,
34    /// Interface name must be at most 128 characters
35    #[error("it must be shorter than 128 characters, was {0} characters long")]
36    TooLong(usize),
37    /// Interface name must be an alphanumeric reverse domain
38    #[error("must be an alphanumeric reverse domain: {0}")]
39    Invalid(String),
40}
41
42/// Name of an interface
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct InterfaceName<T = String> {
45    inner: T,
46}
47
48impl<T> InterfaceName<T> {
49    /// Validate an interface name.
50    ///
51    /// Implements from for a generic `T` with [`AsRef<T>`].
52    pub fn from_str_ref(value: T) -> Result<Self, InterfaceNameError>
53    where
54        T: AsRef<str>,
55    {
56        static RE: OnceLock<Regex> = OnceLock::new();
57
58        let value_str = value.as_ref();
59        if value_str.is_empty() {
60            return Err(InterfaceNameError::Empty);
61        }
62
63        if value_str.len() > 128 {
64            return Err(InterfaceNameError::TooLong(value_str.len()));
65        }
66
67        let rgx = RE.get_or_init(|| {
68            regex::Regex::new(
69                "^([a-zA-Z][a-zA-Z0-9]*\\.([a-zA-Z0-9][a-zA-Z0-9-]*\\.)*)?[a-zA-Z][a-zA-Z0-9]*$",
70            )
71            .expect("should be a valid regex")
72        });
73
74        if !rgx.is_match(value_str) {
75            return Err(InterfaceNameError::Invalid(value_str.to_string()));
76        }
77
78        Ok(Self { inner: value })
79    }
80
81    /// Returns a reference to the Interface name.
82    pub fn as_str(&self) -> &str
83    where
84        T: AsRef<str>,
85    {
86        self.inner.as_ref()
87    }
88
89    /// Converts the Interface name inner type into a string.
90    pub fn into_string(self) -> InterfaceName<String>
91    where
92        T: Into<String>,
93    {
94        InterfaceName {
95            inner: self.inner.into(),
96        }
97    }
98}
99
100impl<'a> TryFrom<&'a str> for InterfaceName<&'a str> {
101    type Error = InterfaceNameError;
102
103    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
104        Self::from_str_ref(value)
105    }
106}
107
108impl TryFrom<String> for InterfaceName<String> {
109    type Error = InterfaceNameError;
110
111    fn try_from(value: String) -> Result<Self, Self::Error> {
112        Self::from_str_ref(value)
113    }
114}
115
116impl<'a> TryFrom<Cow<'a, str>> for InterfaceName<Cow<'a, str>> {
117    type Error = InterfaceNameError;
118
119    fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
120        Self::from_str_ref(value)
121    }
122}
123
124impl<T> AsRef<str> for InterfaceName<T>
125where
126    T: AsRef<str>,
127{
128    fn as_ref(&self) -> &str {
129        self.as_str()
130    }
131}
132
133impl<T> From<InterfaceName<T>> for String
134where
135    T: Into<String>,
136{
137    fn from(value: InterfaceName<T>) -> Self {
138        value.inner.into()
139    }
140}
141
142impl<'a> From<&'a InterfaceName> for InterfaceName<Cow<'a, str>> {
143    fn from(value: &'a InterfaceName) -> Self {
144        InterfaceName {
145            inner: value.as_ref().into(),
146        }
147    }
148}
149
150impl Display for InterfaceName {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        write!(f, "{}", self.inner)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn should_validate_str() {
162        let err = InterfaceName::from_str_ref("").unwrap_err();
163        assert!(matches!(err, InterfaceNameError::Empty));
164
165        let err = InterfaceName::from_str_ref("A".repeat(129)).unwrap_err();
166        assert!(matches!(err, InterfaceNameError::TooLong(129)));
167
168        let err = InterfaceName::from_str_ref("09com.example").unwrap_err();
169        assert!(matches!(err, InterfaceNameError::Invalid(..)));
170    }
171}