Skip to main content

lmn_core/request_template/validators/
string.rs

1use serde::Deserialize;
2
3use crate::request_template::definition::TemplateDef;
4use crate::request_template::error::TemplateError;
5use crate::request_template::validators::Validator;
6
7const MAX_STRING_LENGTH: usize = 10_000;
8
9// ── Raw ───────────────────────────────────────────────────────────────────────
10
11#[derive(Deserialize, Default)]
12pub struct RawStringDetails {
13    pub uppercase_count: Option<usize>,
14    pub lowercase_count: Option<usize>,
15    pub special_chars: Option<Vec<String>>,
16    pub choice: Option<Vec<String>>,
17}
18
19// ── Validated ─────────────────────────────────────────────────────────────────
20
21pub struct StringDef {
22    pub strategy: StringStrategy,
23}
24
25pub enum StringStrategy {
26    Choice(Vec<String>),
27    Generated(StringGenConfig),
28}
29
30pub struct StringGenConfig {
31    pub length: LengthSpec,
32    pub uppercase_count: usize,
33    pub lowercase_count: usize,
34    pub special_chars: Vec<char>,
35}
36
37pub enum LengthSpec {
38    Exact(usize),
39    Range { min: usize, max: usize },
40}
41
42// ── Validator ─────────────────────────────────────────────────────────────────
43
44pub struct StringValidator {
45    pub exact: Option<f64>,
46    pub min: Option<f64>,
47    pub max: Option<f64>,
48    pub details: Option<RawStringDetails>,
49}
50
51impl Validator for StringValidator {
52    fn validate(self, name: &str) -> Result<TemplateDef, TemplateError> {
53        let details = self.details.unwrap_or_default();
54
55        if let Some(choices) = details.choice {
56            if choices.is_empty() {
57                return Err(TemplateError::InvalidConstraint(format!(
58                    "'{name}': choice list must not be empty"
59                )));
60            }
61            return Ok(TemplateDef::String(StringDef {
62                strategy: StringStrategy::Choice(choices),
63            }));
64        }
65
66        let length = validate_length_spec(self.exact, self.min, self.max, name)?;
67
68        let min_len = match &length {
69            LengthSpec::Exact(n) => *n,
70            LengthSpec::Range { min, .. } => *min,
71        };
72
73        let uppercase_count = details.uppercase_count.unwrap_or(0);
74        let lowercase_count = details.lowercase_count.unwrap_or(0);
75
76        if uppercase_count + lowercase_count > min_len {
77            return Err(TemplateError::InvalidConstraint(format!(
78                "'{name}': uppercase_count ({uppercase_count}) + lowercase_count \
79                 ({lowercase_count}) exceeds minimum length ({min_len})"
80            )));
81        }
82
83        let special_chars = details
84            .special_chars
85            .unwrap_or_default()
86            .into_iter()
87            .filter_map(|s| s.chars().next())
88            .collect();
89
90        Ok(TemplateDef::String(StringDef {
91            strategy: StringStrategy::Generated(StringGenConfig {
92                length,
93                uppercase_count,
94                lowercase_count,
95                special_chars,
96            }),
97        }))
98    }
99}
100
101fn validate_length_spec(
102    exact: Option<f64>,
103    min: Option<f64>,
104    max: Option<f64>,
105    name: &str,
106) -> Result<LengthSpec, TemplateError> {
107    if let Some(v) = exact {
108        let n = v as usize;
109        if n > MAX_STRING_LENGTH {
110            return Err(TemplateError::InvalidConstraint(format!(
111                "'{name}': exact length {n} exceeds maximum allowed ({MAX_STRING_LENGTH})"
112            )));
113        }
114        return Ok(LengthSpec::Exact(n));
115    }
116
117    let min_v = min.map(|v| v as usize).unwrap_or(1);
118    let max_v = max.map(|v| v as usize).unwrap_or(min_v);
119
120    if min_v > max_v {
121        return Err(TemplateError::InvalidConstraint(format!(
122            "'{name}': min length ({min_v}) > max length ({max_v})"
123        )));
124    }
125    if max_v > MAX_STRING_LENGTH {
126        return Err(TemplateError::InvalidConstraint(format!(
127            "'{name}': max length ({max_v}) exceeds maximum allowed ({MAX_STRING_LENGTH})"
128        )));
129    }
130
131    Ok(LengthSpec::Range {
132        min: min_v,
133        max: max_v,
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::request_template::validators::Validator;
141
142    fn v(
143        exact: Option<f64>,
144        min: Option<f64>,
145        max: Option<f64>,
146        details: Option<RawStringDetails>,
147    ) -> StringValidator {
148        StringValidator {
149            exact,
150            min,
151            max,
152            details,
153        }
154    }
155
156    #[test]
157    fn validates_choice_list() {
158        let d = RawStringDetails {
159            choice: Some(vec!["a".into(), "b".into()]),
160            ..Default::default()
161        };
162        assert!(v(None, None, None, Some(d)).validate("x").is_ok());
163    }
164
165    #[test]
166    fn rejects_empty_choice_list() {
167        let d = RawStringDetails {
168            choice: Some(vec![]),
169            ..Default::default()
170        };
171        assert!(v(None, None, None, Some(d)).validate("x").is_err());
172    }
173
174    #[test]
175    fn validates_exact_length() {
176        assert!(v(Some(5.0), None, None, None).validate("x").is_ok());
177    }
178
179    #[test]
180    fn rejects_exact_exceeds_max_allowed() {
181        assert!(v(Some(10_001.0), None, None, None).validate("x").is_err());
182    }
183
184    #[test]
185    fn validates_range() {
186        assert!(v(None, Some(3.0), Some(8.0), None).validate("x").is_ok());
187    }
188
189    #[test]
190    fn rejects_min_greater_than_max() {
191        assert!(v(None, Some(10.0), Some(5.0), None).validate("x").is_err());
192    }
193
194    #[test]
195    fn rejects_char_counts_exceeding_min_length() {
196        let d = RawStringDetails {
197            uppercase_count: Some(5),
198            lowercase_count: Some(5),
199            ..Default::default()
200        };
201        assert!(v(Some(3.0), None, None, Some(d)).validate("x").is_err());
202    }
203
204    #[test]
205    fn validate_length_spec_exact() {
206        assert!(matches!(
207            validate_length_spec(Some(5.0), None, None, "x").unwrap(),
208            LengthSpec::Exact(5)
209        ));
210    }
211
212    #[test]
213    fn validate_length_spec_range() {
214        assert!(matches!(
215            validate_length_spec(None, Some(2.0), Some(8.0), "x").unwrap(),
216            LengthSpec::Range { min: 2, max: 8 }
217        ));
218    }
219}