astarte_interfaces/interface/
name.rs1use std::{borrow::Cow, fmt::Display, sync::OnceLock};
25
26use regex::Regex;
27
28#[derive(Debug, thiserror::Error)]
30pub enum InterfaceNameError {
31 #[error("name cannot be empty")]
33 Empty,
34 #[error("it must be shorter than 128 characters, was {0} characters long")]
36 TooLong(usize),
37 #[error("must be an alphanumeric reverse domain: {0}")]
39 Invalid(String),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct InterfaceName<T = String> {
45 inner: T,
46}
47
48impl<T> InterfaceName<T> {
49 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 pub fn as_str(&self) -> &str
83 where
84 T: AsRef<str>,
85 {
86 self.inner.as_ref()
87 }
88
89 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}