rustack_ses_core/
validation.rs1use rustack_ses_model::error::{SesError, SesErrorCode};
6
7const MAX_TAG_LENGTH: usize = 255;
9
10pub fn validate_tag_name(name: &str) -> Result<(), SesError> {
21 if name.is_empty() {
22 return Err(SesError::with_message(
23 SesErrorCode::InvalidParameterValue,
24 "Tag name must not be empty.",
25 ));
26 }
27 if name.len() > MAX_TAG_LENGTH {
28 return Err(SesError::with_message(
29 SesErrorCode::InvalidParameterValue,
30 format!(
31 "Tag name must be at most {MAX_TAG_LENGTH} characters, got {}.",
32 name.len()
33 ),
34 ));
35 }
36 let check_part = name.strip_prefix("ses:").unwrap_or(name);
38 if !check_part
39 .chars()
40 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
41 {
42 return Err(SesError::with_message(
43 SesErrorCode::InvalidParameterValue,
44 format!("Tag name contains invalid characters: {name}"),
45 ));
46 }
47 Ok(())
48}
49
50pub fn validate_tag_value(value: &str) -> Result<(), SesError> {
61 if value.is_empty() {
62 return Err(SesError::with_message(
63 SesErrorCode::InvalidParameterValue,
64 "Tag value must not be empty.",
65 ));
66 }
67 if value.len() > MAX_TAG_LENGTH {
68 return Err(SesError::with_message(
69 SesErrorCode::InvalidParameterValue,
70 format!(
71 "Tag value must be at most {MAX_TAG_LENGTH} characters, got {}.",
72 value.len()
73 ),
74 ));
75 }
76 if !value
77 .chars()
78 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '@')
79 {
80 return Err(SesError::with_message(
81 SesErrorCode::InvalidParameterValue,
82 format!("Tag value contains invalid characters: {value}"),
83 ));
84 }
85 Ok(())
86}
87
88pub fn validate_tags(tags: &[(String, String)]) -> Result<(), SesError> {
94 for (name, value) in tags {
95 validate_tag_name(name)?;
96 validate_tag_value(value)?;
97 }
98 Ok(())
99}
100
101pub fn validate_email_address(email: &str) -> Result<(), SesError> {
110 if !email.contains('@') {
111 return Err(SesError::with_message(
112 SesErrorCode::InvalidParameterValue,
113 format!("Invalid email address: {email}"),
114 ));
115 }
116 Ok(())
117}
118
119#[must_use]
123pub fn extract_domain(email: &str) -> Option<&str> {
124 email.split('@').nth(1)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_should_accept_valid_tag_name() {
133 assert!(validate_tag_name("campaign").is_ok());
134 assert!(validate_tag_name("my-tag").is_ok());
135 assert!(validate_tag_name("my_tag").is_ok());
136 assert!(validate_tag_name("Tag123").is_ok());
137 }
138
139 #[test]
140 fn test_should_accept_ses_prefix_tag_name() {
141 assert!(validate_tag_name("ses:campaign").is_ok());
142 assert!(validate_tag_name("ses:feedback-id").is_ok());
143 }
144
145 #[test]
146 fn test_should_reject_empty_tag_name() {
147 assert!(validate_tag_name("").is_err());
148 }
149
150 #[test]
151 fn test_should_reject_too_long_tag_name() {
152 let long_name = "a".repeat(256);
153 assert!(validate_tag_name(&long_name).is_err());
154 }
155
156 #[test]
157 fn test_should_reject_invalid_chars_in_tag_name() {
158 assert!(validate_tag_name("tag name").is_err());
159 assert!(validate_tag_name("tag.name").is_err());
160 assert!(validate_tag_name("tag@name").is_err());
161 }
162
163 #[test]
164 fn test_should_accept_valid_tag_value() {
165 assert!(validate_tag_value("welcome").is_ok());
166 assert!(validate_tag_value("test-value").is_ok());
167 assert!(validate_tag_value("user@example.com").is_ok());
168 assert!(validate_tag_value("value_123").is_ok());
169 assert!(validate_tag_value("v1.2.3").is_ok());
170 }
171
172 #[test]
173 fn test_should_reject_empty_tag_value() {
174 assert!(validate_tag_value("").is_err());
175 }
176
177 #[test]
178 fn test_should_reject_too_long_tag_value() {
179 let long_value = "a".repeat(256);
180 assert!(validate_tag_value(&long_value).is_err());
181 }
182
183 #[test]
184 fn test_should_reject_invalid_chars_in_tag_value() {
185 assert!(validate_tag_value("value with spaces").is_err());
186 assert!(validate_tag_value("value<html>").is_err());
187 }
188
189 #[test]
190 fn test_should_validate_tag_pairs() {
191 let tags = vec![
192 ("campaign".to_owned(), "welcome".to_owned()),
193 ("source".to_owned(), "test".to_owned()),
194 ];
195 assert!(validate_tags(&tags).is_ok());
196 }
197
198 #[test]
199 fn test_should_reject_invalid_tag_in_list() {
200 let tags = vec![
201 ("campaign".to_owned(), "welcome".to_owned()),
202 (String::new(), "value".to_owned()),
203 ];
204 assert!(validate_tags(&tags).is_err());
205 }
206
207 #[test]
208 fn test_should_accept_valid_email() {
209 assert!(validate_email_address("user@example.com").is_ok());
210 assert!(validate_email_address("a@b").is_ok());
211 }
212
213 #[test]
214 fn test_should_reject_email_without_at() {
215 assert!(validate_email_address("userexample.com").is_err());
216 assert!(validate_email_address("").is_err());
217 }
218
219 #[test]
220 fn test_should_extract_domain() {
221 assert_eq!(extract_domain("user@example.com"), Some("example.com"));
222 assert_eq!(extract_domain("noat"), None);
223 }
224}