lmn_core/request_template/validators/
string.rs1use 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#[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
19pub 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
42pub 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}