plane_common/
names.rs

1use crate::types::NodeKind;
2use clap::error::ErrorKind;
3use serde::{Deserialize, Serialize};
4use std::fmt::{Debug, Display};
5
6pub const MAX_NAME_LENGTH: usize = 45;
7
8#[derive(Debug, thiserror::Error, PartialEq)]
9pub enum NameError {
10    #[error("invalid prefix: {0}")]
11    InvalidAnyPrefix(String),
12
13    #[error("invalid prefix: {0}, expected {1}-")]
14    InvalidPrefix(String, String),
15
16    #[error("invalid character: {0} at position {1}")]
17    InvalidCharacter(char, usize),
18
19    #[error(
20        "too long ({length} characters; max is {max} including prefix)",
21        length = "{0}",
22        max = MAX_NAME_LENGTH
23    )]
24    TooLong(usize),
25}
26
27pub trait Name:
28    Display + ToString + Debug + Clone + Send + Sync + 'static + TryFrom<String, Error = NameError>
29{
30    fn as_str(&self) -> &str;
31
32    fn new_random() -> Self;
33
34    fn prefix() -> Option<&'static str>;
35}
36
37#[macro_export]
38macro_rules! entity_name {
39    ($name:ident, $prefix:expr) => {
40        #[derive(
41            Debug,
42            Clone,
43            PartialEq,
44            Eq,
45            Hash,
46            serde::Serialize,
47            serde::Deserialize,
48            valuable::Valuable,
49        )]
50        pub struct $name(String);
51
52        impl $crate::names::Name for $name {
53            fn as_str(&self) -> &str {
54                &self.0
55            }
56
57            fn new_random() -> Self {
58                if let Some(prefix) = $prefix {
59                    Self($crate::util::random_prefixed_string(prefix))
60                } else {
61                    Self($crate::util::random_string())
62                }
63            }
64
65            fn prefix() -> Option<&'static str> {
66                $prefix
67            }
68        }
69
70        impl std::fmt::Display for $name {
71            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72                write!(f, "{}", &self.0)
73            }
74        }
75
76        impl TryFrom<String> for $name {
77            type Error = $crate::names::NameError;
78
79            fn try_from(s: String) -> Result<Self, $crate::names::NameError> {
80                if let Some(prefix) = $prefix {
81                    if !s.starts_with(prefix) {
82                        return Err($crate::names::NameError::InvalidPrefix(
83                            s,
84                            prefix.to_string(),
85                        ));
86                    }
87                }
88
89                if s.len() > $crate::names::MAX_NAME_LENGTH {
90                    return Err($crate::names::NameError::TooLong(s.len()));
91                }
92
93                for (i, c) in s.chars().enumerate() {
94                    if !(c.is_ascii_lowercase() || c.is_ascii_digit()) && c != '-' {
95                        return Err($crate::names::NameError::InvalidCharacter(c, i));
96                    }
97                }
98
99                Ok(Self(s))
100            }
101        }
102
103        impl clap::builder::ValueParserFactory for $name {
104            type Parser = $crate::names::NameParser<$name>;
105            fn value_parser() -> Self::Parser {
106                $crate::names::NameParser::<$name>::new()
107            }
108        }
109    };
110}
111
112#[derive(Clone)]
113pub struct NameParser<T: Name> {
114    _marker: std::marker::PhantomData<T>,
115}
116
117impl<T: Name> Default for NameParser<T> {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl<T: Name> NameParser<T> {
124    pub fn new() -> Self {
125        Self {
126            _marker: std::marker::PhantomData,
127        }
128    }
129}
130
131pub trait OrRandom<T> {
132    fn or_random(self) -> T;
133}
134
135impl<T: Name> OrRandom<T> for Option<T> {
136    fn or_random(self) -> T {
137        self.unwrap_or_else(T::new_random)
138    }
139}
140
141impl<T: Name> clap::builder::TypedValueParser for NameParser<T> {
142    type Value = T;
143
144    fn parse_ref(
145        &self,
146        cmd: &clap::Command,
147        _arg: Option<&clap::Arg>,
148        value: &std::ffi::OsStr,
149    ) -> Result<Self::Value, clap::Error> {
150        let st = value
151            .to_str()
152            .ok_or_else(|| clap::Error::new(ErrorKind::InvalidUtf8))?;
153        match T::try_from(st.to_string()) {
154            Ok(val) => Ok(val),
155            Err(err) => Err(cmd.clone().error(ErrorKind::InvalidValue, err.to_string())),
156        }
157    }
158}
159
160entity_name!(ControllerName, Some("co"));
161entity_name!(BackendName, None::<&'static str>);
162entity_name!(ProxyName, Some("px"));
163entity_name!(DroneName, Some("dr"));
164entity_name!(AcmeDnsServerName, Some("ns"));
165entity_name!(BackendActionName, Some("ak"));
166
167impl BackendName {
168    pub fn from_container_id(container_id: String) -> Result<Self, NameError> {
169        container_id
170            .strip_prefix("plane-")
171            .ok_or_else(|| NameError::InvalidPrefix(container_id.clone(), "plane-".to_string()))?
172            .to_string()
173            .try_into()
174    }
175
176    pub fn to_container_id(&self) -> String {
177        format!("plane-{}", self)
178    }
179}
180
181pub trait NodeName: Name {
182    fn kind(&self) -> NodeKind;
183}
184
185impl NodeName for ProxyName {
186    fn kind(&self) -> NodeKind {
187        NodeKind::Proxy
188    }
189}
190
191impl NodeName for DroneName {
192    fn kind(&self) -> NodeKind {
193        NodeKind::Drone
194    }
195}
196
197impl NodeName for AcmeDnsServerName {
198    fn kind(&self) -> NodeKind {
199        NodeKind::AcmeDnsServer
200    }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub enum AnyNodeName {
205    Proxy(ProxyName),
206    Drone(DroneName),
207    AcmeDnsServer(AcmeDnsServerName),
208}
209
210impl Display for AnyNodeName {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        match self {
213            AnyNodeName::Proxy(name) => write!(f, "{}", name),
214            AnyNodeName::Drone(name) => write!(f, "{}", name),
215            AnyNodeName::AcmeDnsServer(name) => write!(f, "{}", name),
216        }
217    }
218}
219
220impl TryFrom<String> for AnyNodeName {
221    type Error = NameError;
222
223    fn try_from(s: String) -> Result<Self, Self::Error> {
224        if s.starts_with(ProxyName::prefix().expect("has prefix")) {
225            Ok(AnyNodeName::Proxy(ProxyName::try_from(s)?))
226        } else if s.starts_with(DroneName::prefix().expect("has prefix")) {
227            Ok(AnyNodeName::Drone(DroneName::try_from(s)?))
228        } else if s.starts_with(AcmeDnsServerName::prefix().expect("has prefix")) {
229            Ok(AnyNodeName::AcmeDnsServer(AcmeDnsServerName::try_from(s)?))
230        } else {
231            Err(NameError::InvalidAnyPrefix(s))
232        }
233    }
234}
235
236impl AnyNodeName {
237    pub fn kind(&self) -> NodeKind {
238        match self {
239            AnyNodeName::Proxy(_) => NodeKind::Proxy,
240            AnyNodeName::Drone(_) => NodeKind::Drone,
241            AnyNodeName::AcmeDnsServer(_) => NodeKind::AcmeDnsServer,
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_random_controller_name() {
252        let name = ControllerName::new_random();
253        assert!(name.to_string().starts_with("co-"));
254    }
255
256    #[test]
257    fn test_valid_name() {
258        assert_eq!(
259            Ok(ControllerName("co-abcd".to_string())),
260            ControllerName::try_from("co-abcd".to_string())
261        );
262    }
263
264    #[test]
265    fn test_invalid_prefix() {
266        assert_eq!(
267            Err(NameError::InvalidPrefix(
268                "invalid".to_string(),
269                "co".to_string()
270            )),
271            ControllerName::try_from("invalid".to_string())
272        );
273    }
274
275    #[test]
276    fn test_invalid_chars() {
277        assert_eq!(
278            Err(NameError::InvalidCharacter('*', 3)),
279            ControllerName::try_from("co-*a".to_string())
280        );
281    }
282
283    #[test]
284    fn test_invalid_uppercase() {
285        assert_eq!(
286            Err(NameError::InvalidCharacter('A', 5)),
287            ControllerName::try_from("co-aaA".to_string())
288        );
289    }
290
291    #[test]
292    fn test_too_long() {
293        let name = "co-".to_string() + &"a".repeat(100 - 3);
294        assert_eq!(Err(NameError::TooLong(100)), ControllerName::try_from(name));
295    }
296
297    #[test]
298    fn test_backend_name_from_invalid_container_id() {
299        let container_id = "invalid-123".to_string();
300        assert_eq!(
301            Err(NameError::InvalidPrefix(
302                "invalid-123".to_string(),
303                "plane-".to_string()
304            )),
305            BackendName::try_from(container_id)
306        );
307    }
308}