Skip to main content

proxmox_api/types/
bounded_string.rs

1use 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}