1use crate::plugin::{ActionId, ActionMap};
2use garde::{
3 Path, Report, Validate,
4 error::{Kind, PathComponentKind},
5};
6
7static NAME_SEPARATORS: [char; 2] = ['-', '_'];
9
10pub fn validate_id(value: &str, _context: &()) -> garde::Result {
12 let parts = value.split('.');
13
14 for part in parts {
15 if !part.starts_with(|char: char| char.is_ascii_alphabetic()) {
17 return Err(garde::Error::new(
18 "segment must start with a ascii alphabetic character",
19 ));
20 }
21
22 if !part
24 .chars()
25 .all(|char| char.is_alphanumeric() || NAME_SEPARATORS.contains(&char))
26 {
27 return Err(garde::Error::new(
28 "name domain segment must only contain alpha numeric values and _ or -",
29 ));
30 }
31
32 if part.ends_with(NAME_SEPARATORS) {
34 return Err(garde::Error::new(
35 "name domain segment must not end with _ or -",
36 ));
37 }
38 }
39
40 Ok(())
41}
42
43pub fn validate_name(value: &str, _context: &()) -> garde::Result {
45 if !value.starts_with(|char: char| char.is_ascii_alphabetic()) {
47 return Err(garde::Error::new(
48 "name must start with a ascii alphabetic character",
49 ));
50 }
51
52 if !value
54 .chars()
55 .all(|char| char.is_alphanumeric() || NAME_SEPARATORS.contains(&char))
56 {
57 return Err(garde::Error::new(
58 "name must only contain alpha numeric values and _ or -",
59 ));
60 }
61
62 if value.ends_with(NAME_SEPARATORS) {
64 return Err(garde::Error::new("name must not end with _ or -"));
65 }
66
67 Ok(())
68}
69
70impl Validate for ActionMap {
71 type Context = ();
72
73 fn validate_into(&self, ctx: &(), mut parent: &mut dyn FnMut() -> Path, report: &mut Report) {
74 for (key, value) in self.0.iter() {
75 let mut path = garde::util::nested_path!(parent, key);
76 value.validate_into(ctx, &mut path, report);
77 }
78 }
79}
80
81impl PathComponentKind for ActionId {
82 fn component_kind() -> Kind {
83 Kind::Key
84 }
85}
86
87pub fn validate_color(value: &str, _context: &()) -> garde::Result {
95 let value = value.trim().to_lowercase();
96
97 if value.starts_with('#') {
99 return validate_hex_color(&value);
100 }
101
102 if value.starts_with("rgb(") {
104 return validate_rgb_color(&value);
105 }
106
107 if value.starts_with("rgba(") {
109 return validate_rgba_color(&value);
110 }
111
112 if value.starts_with("hsl(") {
114 return validate_hsl_color(&value);
115 }
116
117 if value.starts_with("hsla(") {
119 return validate_hsla_color(&value);
120 }
121
122 Err(garde::Error::new("invalid color value"))
123}
124
125fn validate_hex_color(value: &str) -> garde::Result {
127 let value = value
128 .strip_prefix('#')
129 .ok_or_else(|| garde::Error::new("hex color must start with #"))?;
130
131 match value.len() {
132 3 | 4 | 6 | 8 => {}
133 _ => {
134 return Err(garde::Error::new(
135 "hex color must be 3, 4, 6, or 8 hex digits",
136 ));
137 }
138 }
139
140 if !value.chars().all(|c| c.is_ascii_hexdigit()) {
141 return Err(garde::Error::new("hex color contains invalid characters"));
142 }
143
144 Ok(())
145}
146
147fn validate_rgb_color(value: &str) -> garde::Result {
149 let value = value
151 .strip_prefix("rgb(")
152 .ok_or_else(|| garde::Error::new("rgb color must start with rgb("))?;
153
154 let value = value
156 .strip_suffix(")")
157 .ok_or_else(|| garde::Error::new("unclosed rgb color"))?;
158
159 let parts: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
160
161 if parts.len() != 3 {
162 return Err(garde::Error::new("invalid rgb color"));
163 }
164
165 for part in parts {
166 parse_rgb_component(part)?;
167 }
168
169 Ok(())
170}
171
172fn validate_rgba_color(value: &str) -> garde::Result {
174 let value = value
176 .strip_prefix("rgba(")
177 .ok_or_else(|| garde::Error::new("rgba color must start with rgba("))?;
178
179 let value = value
181 .strip_suffix(")")
182 .ok_or_else(|| garde::Error::new("unclosed rgba color"))?;
183
184 let parts: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
185
186 if parts.len() != 4 {
187 return Err(garde::Error::new("invalid rgba color"));
188 }
189
190 for part in &parts[..3] {
192 parse_rgb_component(part)?;
193 }
194
195 parse_alpha(parts[3])?;
197
198 Ok(())
199}
200
201fn validate_hsl_color(value: &str) -> garde::Result {
203 let value = value
205 .strip_prefix("hsl(")
206 .ok_or_else(|| garde::Error::new("hsl color must start with hsl("))?;
207
208 let value = value
210 .strip_suffix(")")
211 .ok_or_else(|| garde::Error::new("unclosed hsl color"))?;
212
213 let parts: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
214
215 if parts.len() != 3 {
216 return Err(garde::Error::new("invalid hsl color"));
217 }
218
219 parse_hue(parts[0])?;
220 parse_percentage(parts[1])?;
221 parse_percentage(parts[2])?;
222
223 Ok(())
224}
225
226fn validate_hsla_color(value: &str) -> garde::Result {
228 let value = value
230 .strip_prefix("hsla(")
231 .ok_or_else(|| garde::Error::new("hsla color must start with hsla("))?;
232
233 let value = value
235 .strip_suffix(")")
236 .ok_or_else(|| garde::Error::new("unclosed hsla color"))?;
237
238 let parts: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
239
240 if parts.len() != 4 {
241 return Err(garde::Error::new("invalid hsla color"));
242 }
243
244 parse_hue(parts[0])?;
245 parse_percentage(parts[1])?;
246 parse_percentage(parts[2])?;
247 parse_alpha(parts[3])?;
248
249 Ok(())
250}
251
252fn parse_rgb_component(s: &str) -> garde::Result {
254 if s.ends_with('%') {
255 return parse_percentage(s);
256 }
257
258 let v: u16 = s.parse().map_err(|_| garde::Error::new("invalid number"))?;
259 if v > 255 {
260 return Err(garde::Error::new("rgb exceeded 255 bound"));
261 }
262
263 Ok(())
264}
265
266fn parse_alpha(s: &str) -> garde::Result {
268 if s.parse::<f64>()
269 .is_ok_and(|value| (0.0..=1.0).contains(&value))
270 {
271 return Ok(());
272 }
273
274 Err(garde::Error::new("invalid alpha"))
275}
276
277fn parse_hue(s: &str) -> garde::Result {
279 let value: u16 = s.parse().map_err(|_| garde::Error::new("invalid hue"))?;
280 if value > 360 {
281 return Err(garde::Error::new("hue must not be greater than 360"));
282 }
283
284 Ok(())
285}
286
287fn parse_percentage(s: &str) -> garde::Result {
289 let number = s
290 .strip_suffix('%')
291 .ok_or_else(|| garde::Error::new("missing % sign"))?;
292
293 let value: u8 = number
294 .parse()
295 .map_err(|_| garde::Error::new("invalid percent"))?;
296
297 if value > 100 {
298 return Err(garde::Error::new("percent > 100"));
299 }
300
301 Ok(())
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn validate_id_allows_simple_valid_id() {
310 assert!(validate_id("plugin.test", &()).is_ok());
311 assert!(validate_id("abc.def123", &()).is_ok());
312 assert!(validate_id("abc-def.ghi_jkl", &()).is_ok());
313 }
314
315 #[test]
316 fn validate_id_fails_if_segment_does_not_start_with_letter() {
317 assert!(validate_id("1plugin.test", &()).is_err());
318 assert!(validate_id("plugin.1test", &()).is_err());
319 assert!(validate_id(".test", &()).is_err());
320 }
321
322 #[test]
323 fn validate_id_fails_if_segment_contains_invalid_characters() {
324 assert!(validate_id("plugin.te$t", &()).is_err());
325 assert!(validate_id("plugin.te st", &()).is_err());
326 assert!(validate_id("plugin.te+st", &()).is_err());
327 }
328
329 #[test]
330 fn validate_id_fails_if_segment_ends_with_separator() {
331 assert!(validate_id("plugin.test_", &()).is_err());
332 assert!(validate_id("plugin.test-", &()).is_err());
333 assert!(validate_id("abc_.def", &()).is_err());
334 }
335
336 #[test]
337 fn validate_name_allows_valid_name() {
338 assert!(validate_name("ActionName", &()).is_ok());
339 assert!(validate_name("my_action-1", &()).is_ok());
340 }
341
342 #[test]
343 fn validate_name_fails_if_not_starting_with_letter() {
344 assert!(validate_name("1Action", &()).is_err());
345 assert!(validate_name("_Action", &()).is_err());
346 assert!(validate_name("-Action", &()).is_err());
347 }
348
349 #[test]
350 fn validate_name_fails_on_invalid_characters() {
351 assert!(validate_name("Action!", &()).is_err());
352 assert!(validate_name("Action Name", &()).is_err());
353 assert!(validate_name("Action@", &()).is_err());
354 }
355
356 #[test]
357 fn validate_name_fails_if_ends_with_separator() {
358 assert!(validate_name("Action_", &()).is_err());
359 assert!(validate_name("Action-", &()).is_err());
360 }
361
362 fn color_ok(value: &str) {
363 assert!(
364 validate_color(value, &()).is_ok(),
365 "Expected OK for {value}"
366 );
367 }
368
369 fn color_err(value: &str) {
370 assert!(
371 validate_color(value, &()).is_err(),
372 "Expected ERR for {value}"
373 );
374 }
375
376 #[test]
377 fn test_valid_hex_colors() {
378 color_ok("#fff");
379 color_ok("#ffff");
380 color_ok("#ffffff");
381 color_ok("#ffffffff");
382 color_ok("#ABC"); color_ok(" #123456 "); }
385
386 #[test]
387 fn test_invalid_hex_colors() {
388 color_err("fff"); color_err("#ff"); color_err("#fffff"); color_err("#ggg"); }
393
394 #[test]
395 fn test_valid_rgb_colors() {
396 color_ok("rgb(0,0,0)");
397 color_ok("rgb(255, 255, 255)");
398 color_ok("rgb(50%, 20%, 100%)");
399 }
400
401 #[test]
402 fn test_invalid_rgb_colors() {
403 color_err("rgb()");
404 color_err("rgb(255,255)"); color_err("rgb(255,255,255,0)"); color_err("rgb(300,0,0)"); }
408
409 #[test]
410 fn test_valid_rgba_colors() {
411 color_ok("rgba(0,0,0,0)");
412 color_ok("rgba(255,255,255,1)");
413 color_ok("rgba(100, 150, 200, 0.5)");
414 color_ok("rgba(10%,20%,30%,0.75)");
415 }
416
417 #[test]
418 fn test_invalid_rgba_colors() {
419 color_err("rgba(255,255,255)"); color_err("rgba(255,255,255,1,0)"); color_err("rgba(255,255,255,2)"); color_err("rgba(255,255,255,-0.1)"); }
424
425 #[test]
426 fn test_valid_hsl_colors() {
427 color_ok("hsl(0,0%,0%)");
428 color_ok("hsl(360,100%,50%)");
429 color_ok("hsl(180, 50%, 25%)");
430 }
431
432 #[test]
433 fn test_invalid_hsl_colors() {
434 color_err("hsl()");
435 color_err("hsl(361,50%,50%)"); color_err("hsl(180,101%,50%)"); color_err("hsl(180,50,50)"); }
439
440 #[test]
441 fn test_valid_hsla_colors() {
442 color_ok("hsla(0,0%,0%,0)");
443 color_ok("hsla(360,100%,50%,1)");
444 color_ok("hsla(180, 50%, 25%, 0.75)");
445 }
446
447 #[test]
448 fn test_invalid_hsla_colors() {
449 color_err("hsla(180,50%,50%)"); color_err("hsla(180,50%,50%,2)"); color_err("hsla(361,50%,50%,0.5)"); color_err("hsla(180,50,50%,0.5)"); }
454
455 #[test]
456 fn test_invalid_general_cases() {
457 color_err("blue"); color_err(""); color_err("123"); }
461}