fiberplane_models/
names.rs1#[cfg(feature = "fp-bindgen")]
2use fp_bindgen::prelude::Serializable;
3use serde::{
4 de::{self, Visitor},
5 Deserialize, Serialize,
6};
7use std::fmt::{self, Display};
8use std::str::FromStr;
9use std::{convert::TryFrom, ops::Deref};
10use thiserror::Error;
11
12const MAX_LENGTH: usize = 63;
13const MIN_LENGTH: usize = 1;
14
15#[derive(Debug, Error, PartialEq, Eq)]
16#[cfg_attr(
17 feature = "fp-bindgen",
18 derive(Serializable),
19 fp(rust_module = "fiberplane_models::names")
20)]
21#[non_exhaustive]
22pub enum InvalidName {
23 #[error("name is too long")]
24 TooLong,
25 #[error("name contains invalid characters (names can only include lowercase ASCII letters, numbers, and dashes)")]
26 InvalidCharacters,
27 #[error("name cannot be an empty string")]
28 TooShort,
29 #[error("name must start and end with an alphanumeric character")]
30 NonAlphanumericStartOrEnd,
31}
32
33#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq)]
42#[cfg_attr(feature = "sqlx", derive(sqlx::Type), sqlx(transparent))]
43#[cfg_attr(
44 feature = "fp-bindgen",
45 derive(Serializable),
46 fp(rust_module = "fiberplane_models::names")
47)]
48#[non_exhaustive]
49pub struct Name(String);
50
51impl Name {
52 pub fn new(name: impl Into<String>) -> Result<Self, InvalidName> {
55 let name = name.into();
56 Self::validate(&name).map(|()| Name(name))
57 }
58
59 pub fn new_unchecked(name: impl Into<String>) -> Self {
63 Name(name.into())
64 }
65
66 pub fn into_string(self) -> String {
67 self.0
68 }
69
70 pub fn as_str(&self) -> &str {
71 &self.0
72 }
73
74 pub fn from_static(name: &'static str) -> Self {
80 Name::new(name).expect("Invalid name")
81 }
82
83 pub fn validate(name: &str) -> Result<(), InvalidName> {
84 if name.len() < MIN_LENGTH {
86 return Err(InvalidName::TooShort);
87 }
88 if name.len() > MAX_LENGTH {
89 return Err(InvalidName::TooLong);
90 }
91
92 if name
94 .chars()
95 .any(|c| !c.is_ascii_lowercase() && !c.is_numeric() && c != '-')
96 {
97 return Err(InvalidName::InvalidCharacters);
98 }
99
100 let first = name.chars().next().unwrap();
102 let last = name.chars().last().unwrap();
103 if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
104 return Err(InvalidName::NonAlphanumericStartOrEnd);
105 }
106
107 Ok(())
108 }
109}
110
111struct NameVisitor;
112
113impl<'de> Visitor<'de> for NameVisitor {
114 type Value = Name;
115
116 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
117 formatter.write_str("a valid name to identify the resource")
118 }
119
120 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
121 where
122 E: serde::de::Error,
123 {
124 match Name::validate(value) {
125 Ok(()) => Ok(Name(value.to_owned())),
126 Err(error) => Err(de::Error::custom(error.to_string())),
127 }
128 }
129}
130
131impl<'de> Deserialize<'de> for Name {
132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133 where
134 D: serde::Deserializer<'de>,
135 {
136 deserializer.deserialize_str(NameVisitor)
137 }
138}
139
140impl Display for Name {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.write_str(&self.0)
143 }
144}
145
146impl TryFrom<String> for Name {
147 type Error = InvalidName;
148
149 fn try_from(value: String) -> Result<Self, Self::Error> {
150 Self::new(value)
151 }
152}
153
154impl TryFrom<&str> for Name {
155 type Error = InvalidName;
156
157 fn try_from(value: &str) -> Result<Self, Self::Error> {
158 Self::new(value)
159 }
160}
161
162impl FromStr for Name {
163 type Err = InvalidName;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 Self::new(s)
167 }
168}
169
170impl From<Name> for String {
171 fn from(name: Name) -> Self {
172 name.0
173 }
174}
175
176impl Deref for Name {
177 type Target = String;
178
179 fn deref(&self) -> &Self::Target {
180 &self.0
181 }
182}
183
184impl PartialEq<str> for Name {
185 fn eq(&self, other: &str) -> bool {
186 self.0 == other
187 }
188}
189
190impl PartialEq<&str> for Name {
191 fn eq(&self, other: &&str) -> bool {
192 self.0 == *other
193 }
194}
195
196impl PartialEq<Name> for &str {
197 fn eq(&self, other: &Name) -> bool {
198 *self == other.0
199 }
200}
201
202impl PartialEq<Name> for str {
203 fn eq(&self, other: &Name) -> bool {
204 self == other.0
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn name_creation() {
214 assert!(Name::new("abcdefghijklmnopqrstuvwxyz-1234567890").is_ok());
215 assert!(Name::new("a".repeat(63)).is_ok());
216
217 assert_eq!(Name::new("a".repeat(64)), Err(InvalidName::TooLong));
218 assert_eq!(Name::new(""), Err(InvalidName::TooShort));
219 assert_eq!(Name::new("a_b"), Err(InvalidName::InvalidCharacters));
220 assert_eq!(Name::new("ABC"), Err(InvalidName::InvalidCharacters));
221 assert_eq!(Name::new("hi\n there"), Err(InvalidName::InvalidCharacters));
222 assert_eq!(Name::new("hi:there"), Err(InvalidName::InvalidCharacters));
223 assert_eq!(Name::new("a\u{00A7}b"), Err(InvalidName::InvalidCharacters));
224 assert_eq!(
225 Name::new("-hi-there"),
226 Err(InvalidName::NonAlphanumericStartOrEnd)
227 );
228 }
229
230 #[test]
231 fn name_serialization_deserialization() {
232 let name = Name::new("abcdefghijklmnopqrstuvwxyz-1234567890").unwrap();
233 let serialized = serde_json::to_string(&name).unwrap();
234 let deserialized: Name = serde_json::from_str(&serialized).unwrap();
235 assert_eq!(name, deserialized);
236
237 serde_json::from_str::<Name>("\"hi:there\"").unwrap_err();
238 serde_json::from_str::<Name>(r#""hi_there""#).unwrap_err();
239 }
240
241 #[test]
242 fn name_deserialization_error() {
243 assert_eq!(
244 serde_json::from_str::<Name>("\"-hi-there\"").map_err(|error| error.to_string()),
245 Err(
246 "name must start and end with an alphanumeric character at line 1 column 11"
247 .to_owned()
248 )
249 );
250 }
251}