proxmox_api/types/
bounded_string.rs1use std::{
2 hash::{Hash, Hasher},
3 sync::{LazyLock, RwLock},
4};
5
6static REGEX_CACHE: LazyLock<RwLock<std::collections::HashMap<u64, regex::Regex>>> =
7 LazyLock::new(|| RwLock::new(std::collections::HashMap::new()));
8
9fn get_or_compile_regex(pattern: &str) -> Result<regex::Regex, BoundedStringError> {
10 let mut h = std::hash::DefaultHasher::new();
11 {
12 let cache = REGEX_CACHE.read().unwrap();
13 pattern.hash(&mut h);
14 if let Some(regex) = cache.get(&h.finish()) {
15 return Ok(regex.clone());
16 }
17 }
18 let regex = regex::Regex::new(pattern).map_err(BoundedStringError::RegexCreationError)?;
19 let result = regex.clone();
20 pattern.hash(&mut h);
21 REGEX_CACHE.write().unwrap().insert(h.finish(), regex);
22 Ok(result)
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum BoundedStringError {
27 TooLong,
28 TooShort,
29 PatternMismatch,
30 RegexCreationError(regex::Error),
31}
32
33impl std::fmt::Display for BoundedStringError {
34 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
35 match self {
36 BoundedStringError::TooLong => write!(f, "Given string is too long"),
37 BoundedStringError::TooShort => write!(f, "Given string is too short"),
38 BoundedStringError::PatternMismatch => write!(f, "Given string does not match pattern"),
39 BoundedStringError::RegexCreationError(err) => {
40 write!(f, "Regex creation error: {}", err)
41 }
42 }
43 }
44}
45
46impl std::error::Error for BoundedStringError {}
47
48pub trait BoundedString {
49 const MIN_LENGTH: Option<usize> = None;
50 const MAX_LENGTH: Option<usize> = None;
51 const DEFAULT: Option<&'static str> = None;
52 const PATTERN: Option<&'static str> = None;
53 const TYPE_DESCRIPTION: &'static str;
54
55 fn get_value(&self) -> &str;
56
57 fn new(value: String) -> Result<Self, BoundedStringError>
58 where
59 Self: Sized;
60
61 fn validate(value: &str) -> Result<(), BoundedStringError> {
62 if let Some(min_len) = Self::MIN_LENGTH
63 && value.len() < min_len
64 {
65 return Err(BoundedStringError::TooShort);
66 } else if let Some(max) = Self::MAX_LENGTH
67 && value.len() > max
68 {
69 return Err(BoundedStringError::TooLong);
70 } else if let Some(pattern) = Self::PATTERN {
71 let regex = get_or_compile_regex(pattern)?;
72 if !regex.is_match(value) {
73 return Err(BoundedStringError::PatternMismatch);
74 }
75 }
76 Ok(())
77 }
78}
79
80use serde::{Deserialize, Deserializer, Serializer};
81
82pub fn serialize_bounded_string<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
83where
84 S: Serializer,
85 T: BoundedString,
86{
87 serializer.serialize_str(value.get_value())
88}
89
90pub fn deserialize_bounded_string<'de, T, D>(deserializer: D) -> Result<T, D::Error>
91where
92 D: Deserializer<'de>,
93 T: BoundedString + TryFrom<String, Error = BoundedStringError>,
94{
95 let value = String::deserialize(deserializer)?;
96 T::try_from(value).map_err(|e| {
97 serde::de::Error::custom(format!(
98 "could not parse as {} with error: {}",
99 T::TYPE_DESCRIPTION,
100 e
101 ))
102 })
103}