fiberplane_models/
labels.rs1#[cfg(feature = "fp-bindgen")]
2use fp_bindgen::prelude::Serializable;
3use serde::{Deserialize, Serialize};
4use std::fmt::{self, Display, Formatter};
5use thiserror::Error;
6
7const MAX_LABEL_VALUE_LENGTH: usize = 63;
8const MAX_LABEL_NAME_LENGTH: usize = 63;
9const MAX_LABEL_PREFIX_LENGTH: usize = 253;
10
11#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
13#[cfg_attr(
14 feature = "fp-bindgen",
15 derive(Serializable),
16 fp(rust_module = "fiberplane_models::labels")
17)]
18#[non_exhaustive]
19#[serde(rename_all = "camelCase")]
20pub struct Label {
21 pub key: String,
23
24 pub value: String,
26}
27
28impl Label {
29 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
30 Self {
31 key: key.into(),
32 value: value.into(),
33 }
34 }
35
36 pub fn validate(&self) -> Result<(), LabelValidationError> {
38 Label::validate_key(&self.key)?;
39 Label::validate_value(&self.value)?;
40
41 Ok(())
42 }
43
44 pub fn validate_key(key: &str) -> Result<(), LabelValidationError> {
55 if key.is_empty() {
56 return Err(LabelValidationError::EmptyKey);
57 }
58
59 let (prefix, name) = match key.split_once('/') {
60 Some((prefix, name)) => (Some(prefix), name),
61 None => (None, key),
62 };
63
64 if name.is_empty() {
66 return Err(LabelValidationError::EmptyName);
67 }
68
69 if name.len() > MAX_LABEL_NAME_LENGTH {
70 return Err(LabelValidationError::NameTooLong);
71 }
72
73 let first = name.chars().next().unwrap();
75 let last = name.chars().last().unwrap();
76 if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
77 return Err(LabelValidationError::NameInvalidCharacters);
78 }
79
80 if name.chars().any(|c| !is_valid_label_char(c)) {
81 return Err(LabelValidationError::NameInvalidCharacters);
82 }
83
84 match prefix {
85 Some(prefix) => validate_prefix(prefix),
86 None => Ok(()),
87 }
88 }
89
90 pub fn validate_value(value: &str) -> Result<(), LabelValidationError> {
95 if !value.is_empty() {
97 if value.len() > MAX_LABEL_VALUE_LENGTH {
98 return Err(LabelValidationError::ValueTooLong);
99 }
100
101 let first = value.chars().next().unwrap();
103 let last = value.chars().last().unwrap();
104 if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
105 return Err(LabelValidationError::ValueInvalidCharacters);
106 }
107
108 if value.chars().any(|c| !is_valid_label_char(c)) {
109 return Err(LabelValidationError::ValueInvalidCharacters);
110 }
111 }
112 Ok(())
113 }
114}
115
116impl Display for Label {
117 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
118 f.write_str(&self.key)?;
119 if !self.value.is_empty() {
120 f.write_str(&format!("={}", &self.value))?;
121 }
122 Ok(())
123 }
124}
125
126#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Error)]
127#[cfg_attr(
128 feature = "fp-bindgen",
129 derive(Serializable),
130 fp(rust_module = "fiberplane_models::labels")
131)]
132#[non_exhaustive]
133#[serde(rename_all = "snake_case")]
134pub enum LabelValidationError {
135 #[error("The key in the label was empty")]
136 EmptyKey,
137
138 #[error("The name portion of the key was empty")]
139 EmptyName,
140
141 #[error("The name portion of the key was too long")]
142 NameTooLong,
143
144 #[error("The name portion of the key contains invalid characters")]
145 NameInvalidCharacters,
146
147 #[error("The prefix portion of the key was empty")]
148 EmptyPrefix,
149
150 #[error("The prefix portion of the key was too long")]
151 PrefixTooLong,
152
153 #[error("The prefix portion of the key contains invalid characters")]
154 PrefixInvalidCharacters,
155
156 #[error("The value is too long")]
157 ValueTooLong,
158
159 #[error("The value contains invalid characters")]
160 ValueInvalidCharacters,
161}
162
163fn is_valid_label_char(c: char) -> bool {
168 c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'
169}
170
171fn validate_prefix(prefix: &str) -> Result<(), LabelValidationError> {
172 if prefix.is_empty() {
173 return Err(LabelValidationError::EmptyPrefix);
174 }
175
176 if prefix.len() > MAX_LABEL_PREFIX_LENGTH {
177 return Err(LabelValidationError::PrefixTooLong);
178 }
179
180 for subdomain in prefix.split('.') {
181 if subdomain.is_empty() {
182 return Err(LabelValidationError::PrefixInvalidCharacters);
183 }
184
185 let first = subdomain.chars().next().unwrap();
187 let last = subdomain.chars().last().unwrap();
188 if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
189 return Err(LabelValidationError::PrefixInvalidCharacters);
190 }
191
192 if subdomain
193 .chars()
194 .any(|c| !c.is_ascii_alphanumeric() && c != '-')
195 {
196 return Err(LabelValidationError::ValueInvalidCharacters);
197 }
198 }
199
200 Ok(())
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn label_key_valid() {
209 let keys = vec![
210 "key",
211 "key.with.dot",
212 "key_with_underscore",
213 "key-with-dash",
214 "key..with..double..dot",
215 "fiberplane.io/key",
216 "fiberplane.io/key.with.dot",
217 "fiberplane.io/key_with_underscore",
218 "fiberplane.io/key-with-dash",
219 ];
220 for key in keys.into_iter() {
221 assert!(
222 Label::validate_key(key).is_ok(),
223 "Key \"{key}\" should have passed validation"
224 );
225 }
226 }
227
228 #[test]
229 fn label_key_invalid() {
230 let keys = vec![
231 "",
232 "too_long_name_too_long_name_too_long_name_too_long_name_too_long_name_",
233 "fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.fiberplane.com/name",
234 "-name_start_with_non_alpha_numeric",
235 "name_end_with_non_alpha_numeric-",
236 "fiberplane..com/name",
237 "fiberplane.com/invalid/name",
238 "/name",
239 ];
240 for key in keys.into_iter() {
241 assert!(
242 Label::validate_key(key).is_err(),
243 "Key \"{key}\" should have failed validation"
244 );
245 }
246 }
247
248 #[test]
249 fn label_value_valid() {
250 let values = vec![
251 "",
252 "value",
253 "value.with.dot",
254 "value_with_underscore",
255 "value-with-dash",
256 ];
257 for value in values.into_iter() {
258 assert!(
259 Label::validate_value(value).is_ok(),
260 "Value \"{value}\" should have passed validation"
261 );
262 }
263 }
264
265 #[test]
266 fn label_value_invalid() {
267 let values = vec![
268 "too_long_name_too_long_name_too_long_name_too_long_name_too_long_name_",
269 "-value_starting_with_a_dash",
270 "value_ending_with_a_dash-",
271 ];
272 for value in values.into_iter() {
273 assert!(
274 Label::validate_key(value).is_err(),
275 "Value \"{value}\" should have failed validation"
276 );
277 }
278 }
279}