uptrakit_surfaces/
validation.rs1use thiserror::Error;
5
6pub const MAX_ICON_NAME_LEN: usize = 64;
8
9#[non_exhaustive]
11#[derive(Debug, Clone, PartialEq, Eq, Error)]
12pub enum IconNameError {
13 #[error("icon name must not be empty")]
14 Empty,
15 #[error("icon name exceeds {MAX_ICON_NAME_LEN} characters")]
16 TooLong,
17 #[error("icon name must match lucide kebab-case (lowercase letters, digits, hyphens)")]
18 InvalidFormat,
19}
20
21pub fn validate_icon_name(name: &str) -> Result<(), IconNameError> {
32 if name.is_empty() {
33 return Err(IconNameError::Empty);
34 }
35 if name.len() > MAX_ICON_NAME_LEN {
36 return Err(IconNameError::TooLong);
37 }
38 let bytes = name.as_bytes();
42 let [first, middle @ .., last] = bytes else {
43 return Err(IconNameError::InvalidFormat);
44 };
45 if !first.is_ascii_lowercase() {
46 return Err(IconNameError::InvalidFormat);
47 }
48 if !(last.is_ascii_lowercase() || last.is_ascii_digit()) {
49 return Err(IconNameError::InvalidFormat);
50 }
51 for &byte in middle {
52 let allowed = byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-';
53 if !allowed {
54 return Err(IconNameError::InvalidFormat);
55 }
56 }
57 Ok(())
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn validate_icon_name_accepts_kebab_case() {
66 validate_icon_name("trash-2").unwrap();
67 validate_icon_name("refresh-cw").unwrap();
68 validate_icon_name("server-cog").unwrap();
69 validate_icon_name("plug-zap").unwrap();
70 validate_icon_name("box").unwrap();
71 validate_icon_name("ab").unwrap();
72 }
73
74 #[test]
75 fn validate_icon_name_rejects_empty() {
76 assert_eq!(validate_icon_name(""), Err(IconNameError::Empty));
77 }
78
79 #[test]
80 fn validate_icon_name_rejects_oversized() {
81 let long = "a".repeat(MAX_ICON_NAME_LEN + 1);
82 assert_eq!(validate_icon_name(&long), Err(IconNameError::TooLong));
83 }
84
85 #[test]
86 fn validate_icon_name_rejects_pascal_case() {
87 assert_eq!(
88 validate_icon_name("Trash2"),
89 Err(IconNameError::InvalidFormat)
90 );
91 assert_eq!(
92 validate_icon_name("Package"),
93 Err(IconNameError::InvalidFormat)
94 );
95 }
96
97 #[test]
98 fn validate_icon_name_rejects_underscore() {
99 assert_eq!(
100 validate_icon_name("trash_2"),
101 Err(IconNameError::InvalidFormat)
102 );
103 }
104
105 #[test]
106 fn validate_icon_name_rejects_leading_or_trailing_dash() {
107 assert_eq!(
108 validate_icon_name("-trash"),
109 Err(IconNameError::InvalidFormat)
110 );
111 assert_eq!(
112 validate_icon_name("trash-"),
113 Err(IconNameError::InvalidFormat)
114 );
115 }
116
117 #[test]
118 fn validate_icon_name_rejects_single_char() {
119 assert_eq!(validate_icon_name("x"), Err(IconNameError::InvalidFormat));
120 }
121
122 #[test]
123 fn validate_icon_name_rejects_uppercase_or_punctuation_in_middle() {
124 assert_eq!(
125 validate_icon_name("traSh-2"),
126 Err(IconNameError::InvalidFormat)
127 );
128 assert_eq!(
129 validate_icon_name("trash 2"),
130 Err(IconNameError::InvalidFormat)
131 );
132 }
133}