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}