zerokms_protocol/
identified_by.rs1use std::{
2 fmt::{self, Display, Formatter},
3 ops::Deref,
4};
5
6use serde::{Deserialize, Serialize};
7use utoipa::ToSchema;
8use uuid::Uuid;
9
10#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Clone, ToSchema)]
13pub enum IdentifiedBy {
14 Uuid(Uuid),
16 Name(Name),
18}
19
20impl<'de> Deserialize<'de> for IdentifiedBy {
21 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
22 where
23 D: serde::Deserializer<'de>,
24 {
25 #[derive(Deserialize)]
27 enum Stub {
28 Uuid(Uuid),
29 Name(Name),
30 }
31
32 #[derive(Deserialize)]
34 #[serde(untagged)]
35 enum Compat {
36 Uuid(Uuid), IdentifiedBy(Stub), }
39
40 if let Ok(id_compat) = <Compat as Deserialize>::deserialize(deserializer) {
41 match id_compat {
42 Compat::Uuid(uuid) => Ok(IdentifiedBy::Uuid(uuid)),
43 Compat::IdentifiedBy(stub) => match stub {
44 Stub::Uuid(uuid) => Ok(IdentifiedBy::Uuid(uuid)),
45 Stub::Name(name) => Ok(IdentifiedBy::Name(name)),
46 },
47 }
48 } else {
49 Err(serde::de::Error::custom(
50 "expected one of: a UUID, IdentifiedBy::Uuid or IdentifiedBy::Name",
51 ))
52 }
53 }
54}
55
56impl From<Uuid> for IdentifiedBy {
57 fn from(value: Uuid) -> Self {
58 Self::Uuid(value)
59 }
60}
61
62impl From<Name> for IdentifiedBy {
63 fn from(value: Name) -> Self {
64 Self::Name(value)
65 }
66}
67
68impl From<String> for Name {
69 fn from(value: String) -> Self {
70 Self { inner: value }
71 }
72}
73
74impl Display for IdentifiedBy {
75 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
76 match self {
77 IdentifiedBy::Uuid(uuid) => Display::fmt(uuid, f),
78 IdentifiedBy::Name(name) => Display::fmt(name, f),
79 }
80 }
81}
82
83#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
85#[serde(transparent)]
86pub struct Name {
87 inner: String,
88}
89
90impl utoipa::PartialSchema for Name {
94 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
95 String::schema()
96 }
97}
98
99impl utoipa::ToSchema for Name {}
100
101impl Name {
102 pub fn new_untrusted(name: &str) -> Self {
104 Self {
105 inner: name.to_owned(),
106 }
107 }
108}
109
110impl Deref for Name {
111 type Target = str;
112
113 fn deref(&self) -> &Self::Target {
114 &self.inner
115 }
116}
117
118const NAME_MAX_LEN: usize = 64;
119
120pub struct InvalidNameError(pub String);
121
122impl TryFrom<&str> for Name {
123 type Error = InvalidNameError;
124
125 fn try_from(value: &str) -> Result<Self, Self::Error> {
126 if !value.len() <= NAME_MAX_LEN {
127 return Err(InvalidNameError(format!(
128 "name length must be <= 64, got {}",
129 value.len()
130 )));
131 }
132
133 if value.is_empty() {
134 return Err(InvalidNameError(
135 "name must not be an empty string".to_string(),
136 ));
137 }
138
139 if !value
140 .chars()
141 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '/')
142 {
143 return Err(InvalidNameError(
144 "name must consist of only these allowed characters: A-Z a-z 0-9 _ - /".to_string(),
145 ));
146 }
147
148 Ok(Name {
149 inner: value.to_string(),
150 })
151 }
152}
153
154impl Display for Name {
155 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
156 Display::fmt(&self.inner, f)
157 }
158}
159
160#[cfg(test)]
161mod test {
162 use super::*;
163
164 #[test]
165 fn name_validation() {
166 assert!(Name::try_from("alias").is_ok());
167 assert!(Name::try_from("foo/bar").is_ok());
168 assert!(Name::try_from(
169 "abcdefghijklmnopqrstuvqwxyzABCDEFGHIJKLMNOPQRSTUVQWXYZ0123456789-_/"
170 )
171 .is_ok());
172 assert!(Name::try_from(" leading-ws").is_err());
173 assert!(Name::try_from("trailing-ws ").is_err());
174 assert!(Name::try_from("punctuation%").is_err());
175 }
176}