1use crate::ValidationResult;
5use serde::{Deserialize, Serialize};
6use unicode_width::UnicodeWidthStr;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CharacterLimits {
11 max_length: Option<usize>,
13
14 min_length: Option<usize>,
16
17 warning_threshold: Option<usize>,
19
20 count_mode: CountMode,
22}
23
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
26pub enum CountMode {
27 #[default]
29 Characters,
30
31 DisplayWidth,
33
34 Bytes,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum LimitCheckResult {
41 Ok,
43
44 Warning { current: usize, max: usize },
46
47 Exceeded { current: usize, max: usize },
49
50 TooShort { current: usize, min: usize },
52}
53
54impl CharacterLimits {
55 pub fn new(max_length: usize) -> Self {
57 Self {
58 max_length: Some(max_length),
59 min_length: None,
60 warning_threshold: None,
61 count_mode: CountMode::default(),
62 }
63 }
64
65 pub fn new_range(min_length: usize, max_length: usize) -> Self {
67 Self {
68 max_length: Some(max_length),
69 min_length: Some(min_length),
70 warning_threshold: None,
71 count_mode: CountMode::default(),
72 }
73 }
74
75 pub fn new_min(min_length: usize) -> Self {
77 Self {
78 max_length: None,
79 min_length: Some(min_length),
80 warning_threshold: None,
81 count_mode: CountMode::default(),
82 }
83 }
84
85 pub fn new_warning(threshold: usize) -> Self {
87 Self {
88 max_length: None,
89 min_length: None,
90 warning_threshold: Some(threshold),
91 count_mode: CountMode::default(),
92 }
93 }
94
95 pub fn with_warning_threshold(mut self, threshold: usize) -> Self {
97 self.warning_threshold = Some(threshold);
98 self
99 }
100
101 pub fn with_count_mode(mut self, mode: CountMode) -> Self {
103 self.count_mode = mode;
104 self
105 }
106
107 pub fn max_length(&self) -> Option<usize> {
109 self.max_length
110 }
111
112 pub fn min_length(&self) -> Option<usize> {
114 self.min_length
115 }
116
117 pub fn warning_threshold(&self) -> Option<usize> {
119 self.warning_threshold
120 }
121
122 pub fn count_mode(&self) -> CountMode {
124 self.count_mode
125 }
126
127 fn count(&self, text: &str) -> usize {
129 match self.count_mode {
130 CountMode::Characters => text.chars().count(),
131 CountMode::DisplayWidth => text.width(),
132 CountMode::Bytes => text.len(),
133 }
134 }
135
136 pub fn validate_insertion(
138 &self,
139 current_text: &str,
140 position: usize,
141 character: char,
142 ) -> Option<ValidationResult> {
143 let mut new_text = String::with_capacity(current_text.len() + character.len_utf8());
144 let mut chars = current_text.chars();
145
146 let clamped_pos = position.min(current_text.chars().count());
147 for _ in 0..clamped_pos {
148 if let Some(ch) = chars.next() {
149 new_text.push(ch);
150 }
151 }
152
153 new_text.push(character);
154
155 for ch in chars {
156 new_text.push(ch);
157 }
158
159 let new_count = self.count(&new_text);
160 let current_count = self.count(current_text);
161
162 if let Some(max) = self.max_length {
163 if new_count > max {
164 return Some(ValidationResult::error(format!(
165 "Character limit exceeded: {new_count}/{max}"
166 )));
167 }
168
169 if let Some(warning_threshold) = self.warning_threshold {
170 if new_count >= warning_threshold && current_count < warning_threshold {
171 return Some(ValidationResult::warning(format!(
172 "Approaching character limit: {new_count}/{max}"
173 )));
174 }
175 }
176 }
177
178 None }
180
181 pub fn validate_content(&self, text: &str) -> Option<ValidationResult> {
183 let count = self.count(text);
184
185 if let Some(min) = self.min_length {
186 if count < min {
187 return Some(ValidationResult::warning(format!(
188 "Minimum length not met: {count}/{min}"
189 )));
190 }
191 }
192
193 if let Some(max) = self.max_length {
194 if count > max {
195 return Some(ValidationResult::error(format!(
196 "Character limit exceeded: {count}/{max}"
197 )));
198 }
199
200 if let Some(warning_threshold) = self.warning_threshold {
201 if count >= warning_threshold {
202 return Some(ValidationResult::warning(format!(
203 "Approaching character limit: {count}/{max}"
204 )));
205 }
206 }
207 }
208
209 None }
211
212 pub fn check_limits(&self, text: &str) -> LimitCheckResult {
214 let count = self.count(text);
215
216 if let Some(max) = self.max_length {
217 if count > max {
218 return LimitCheckResult::Exceeded {
219 current: count,
220 max,
221 };
222 }
223
224 if let Some(warning_threshold) = self.warning_threshold {
225 if count >= warning_threshold {
226 return LimitCheckResult::Warning {
227 current: count,
228 max,
229 };
230 }
231 }
232 }
233
234 if let Some(min) = self.min_length {
236 if count < min {
237 return LimitCheckResult::TooShort {
238 current: count,
239 min,
240 };
241 }
242 }
243
244 LimitCheckResult::Ok
245 }
246
247 pub fn status_text(&self, text: &str) -> Option<String> {
249 match self.check_limits(text) {
250 LimitCheckResult::Ok => {
251 self.max_length
253 .map(|max| format!("{}/{}", self.count(text), max))
254 }
255 LimitCheckResult::Warning { current, max } => {
256 Some(format!("{current}/{max} (approaching limit)"))
257 }
258 LimitCheckResult::Exceeded { current, max } => {
259 Some(format!("{current}/{max} (exceeded)"))
260 }
261 LimitCheckResult::TooShort { current, min } => Some(format!("{current}/{min} minimum")),
262 }
263 }
264 pub fn allows_field_switch(&self, text: &str) -> bool {
265 if let Some(min) = self.min_length {
266 let count = self.count(text);
267 count == 0 || count >= min
269 } else {
270 true }
272 }
273
274 pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
276 if let Some(min) = self.min_length {
277 let count = self.count(text);
278 if count > 0 && count < min {
279 return Some(format!(
280 "Field must be empty or have at least {min} characters (currently: {count})"
281 ));
282 }
283 }
284 None
285 }
286}
287
288pub fn count_text(text: &str, mode: CountMode) -> usize {
289 match mode {
290 CountMode::Characters => text.chars().count(),
291 CountMode::DisplayWidth => text.width(),
292 CountMode::Bytes => text.len(),
293 }
294}
295
296impl Default for CharacterLimits {
297 fn default() -> Self {
298 Self {
299 max_length: Some(30), min_length: None,
301 warning_threshold: None,
302 count_mode: CountMode::default(),
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_character_limits_creation() {
313 let limits = CharacterLimits::new(10);
314 assert_eq!(limits.max_length(), Some(10));
315 assert_eq!(limits.min_length(), None);
316
317 let range_limits = CharacterLimits::new_range(5, 15);
318 assert_eq!(range_limits.min_length(), Some(5));
319 assert_eq!(range_limits.max_length(), Some(15));
320 }
321
322 #[test]
323 fn test_default_limits() {
324 let limits = CharacterLimits::default();
325 assert_eq!(limits.max_length(), Some(30));
326 }
327
328 #[test]
329 fn test_character_counting() {
330 let limits = CharacterLimits::new(5);
331
332 assert_eq!(limits.count("hello"), 5);
334 assert_eq!(limits.count("héllo"), 5); let limits = limits.with_count_mode(CountMode::DisplayWidth);
338 assert_eq!(limits.count("hello"), 5);
339
340 let limits = limits.with_count_mode(CountMode::Bytes);
342 assert_eq!(limits.count("hello"), 5);
343 assert_eq!(limits.count("héllo"), 6); }
345
346 #[test]
347 fn test_insertion_validation() {
348 let limits = CharacterLimits::new(5);
349
350 let result = limits.validate_insertion("test", 4, 'x');
352 assert!(result.is_none()); let result = limits.validate_insertion("tests", 5, 'x');
356 assert!(result.is_some());
357 assert!(!result.unwrap().is_acceptable());
358 }
359
360 #[test]
361 fn test_content_validation() {
362 let limits = CharacterLimits::new_range(3, 10);
363
364 let result = limits.validate_content("hi");
366 assert!(result.is_some());
367 assert!(result.unwrap().is_acceptable()); let result = limits.validate_content("hello");
371 assert!(result.is_none());
372
373 let result = limits.validate_content("hello world!");
375 assert!(result.is_some());
376 assert!(!result.unwrap().is_acceptable()); }
378
379 #[test]
380 fn test_warning_threshold() {
381 let limits = CharacterLimits::new(10).with_warning_threshold(8);
382
383 let result = limits.validate_insertion("123456", 6, 'x');
385 assert!(result.is_none());
386
387 let result = limits.validate_insertion("1234567", 7, 'x');
389 assert!(result.is_some()); assert!(result.unwrap().is_acceptable()); let result = limits.validate_insertion("12345678", 8, 'x');
393 assert!(result.is_none());
394 }
395
396 #[test]
397 fn test_status_text() {
398 let limits = CharacterLimits::new(10);
399
400 assert_eq!(limits.status_text("hello"), Some("5/10".to_string()));
401
402 let limits = limits.with_warning_threshold(8);
403 assert_eq!(
404 limits.status_text("12345678"),
405 Some("8/10 (approaching limit)".to_string())
406 );
407 assert_eq!(
408 limits.status_text("1234567890x"),
409 Some("11/10 (exceeded)".to_string())
410 );
411 }
412
413 #[test]
414 fn test_field_switch_blocking() {
415 let limits = CharacterLimits::new_range(3, 10);
416
417 assert!(limits.allows_field_switch(""));
419 assert!(limits.field_switch_block_reason("").is_none());
420
421 assert!(!limits.allows_field_switch("hi"));
423 assert!(limits.field_switch_block_reason("hi").is_some());
424 assert!(limits
425 .field_switch_block_reason("hi")
426 .unwrap()
427 .contains("at least 3 characters"));
428
429 assert!(limits.allows_field_switch("hello"));
431 assert!(limits.field_switch_block_reason("hello").is_none());
432
433 assert!(limits.allows_field_switch("this is way too long"));
435 assert!(limits
436 .field_switch_block_reason("this is way too long")
437 .is_none());
438 }
439
440 #[test]
441 fn test_field_switch_no_minimum() {
442 let limits = CharacterLimits::new(10); assert!(limits.allows_field_switch(""));
446 assert!(limits.allows_field_switch("a"));
447 assert!(limits.allows_field_switch("hello"));
448
449 assert!(limits.field_switch_block_reason("").is_none());
450 assert!(limits.field_switch_block_reason("a").is_none());
451 }
452}