use crate::ValidationResult;
use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CharacterLimits {
max_length: Option<usize>,
min_length: Option<usize>,
warning_threshold: Option<usize>,
count_mode: CountMode,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub enum CountMode {
#[default]
Characters,
DisplayWidth,
Bytes,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LimitCheckResult {
Ok,
Warning { current: usize, max: usize },
Exceeded { current: usize, max: usize },
TooShort { current: usize, min: usize },
}
impl CharacterLimits {
pub fn new(max_length: usize) -> Self {
Self {
max_length: Some(max_length),
min_length: None,
warning_threshold: None,
count_mode: CountMode::default(),
}
}
pub fn new_range(min_length: usize, max_length: usize) -> Self {
Self {
max_length: Some(max_length),
min_length: Some(min_length),
warning_threshold: None,
count_mode: CountMode::default(),
}
}
pub fn new_min(min_length: usize) -> Self {
Self {
max_length: None,
min_length: Some(min_length),
warning_threshold: None,
count_mode: CountMode::default(),
}
}
pub fn new_warning(threshold: usize) -> Self {
Self {
max_length: None,
min_length: None,
warning_threshold: Some(threshold),
count_mode: CountMode::default(),
}
}
pub fn with_warning_threshold(mut self, threshold: usize) -> Self {
self.warning_threshold = Some(threshold);
self
}
pub fn with_count_mode(mut self, mode: CountMode) -> Self {
self.count_mode = mode;
self
}
pub fn max_length(&self) -> Option<usize> {
self.max_length
}
pub fn min_length(&self) -> Option<usize> {
self.min_length
}
pub fn warning_threshold(&self) -> Option<usize> {
self.warning_threshold
}
pub fn count_mode(&self) -> CountMode {
self.count_mode
}
fn count(&self, text: &str) -> usize {
match self.count_mode {
CountMode::Characters => text.chars().count(),
CountMode::DisplayWidth => text.width(),
CountMode::Bytes => text.len(),
}
}
pub fn validate_insertion(
&self,
current_text: &str,
position: usize,
character: char,
) -> Option<ValidationResult> {
let mut new_text = String::with_capacity(current_text.len() + character.len_utf8());
let mut chars = current_text.chars();
let clamped_pos = position.min(current_text.chars().count());
for _ in 0..clamped_pos {
if let Some(ch) = chars.next() {
new_text.push(ch);
}
}
new_text.push(character);
for ch in chars {
new_text.push(ch);
}
let new_count = self.count(&new_text);
let current_count = self.count(current_text);
if let Some(max) = self.max_length {
if new_count > max {
return Some(ValidationResult::error(format!(
"Character limit exceeded: {new_count}/{max}"
)));
}
if let Some(warning_threshold) = self.warning_threshold {
if new_count >= warning_threshold && current_count < warning_threshold {
return Some(ValidationResult::warning(format!(
"Approaching character limit: {new_count}/{max}"
)));
}
}
}
None }
pub fn validate_content(&self, text: &str) -> Option<ValidationResult> {
let count = self.count(text);
if let Some(min) = self.min_length {
if count < min {
return Some(ValidationResult::warning(format!(
"Minimum length not met: {count}/{min}"
)));
}
}
if let Some(max) = self.max_length {
if count > max {
return Some(ValidationResult::error(format!(
"Character limit exceeded: {count}/{max}"
)));
}
if let Some(warning_threshold) = self.warning_threshold {
if count >= warning_threshold {
return Some(ValidationResult::warning(format!(
"Approaching character limit: {count}/{max}"
)));
}
}
}
None }
pub fn check_limits(&self, text: &str) -> LimitCheckResult {
let count = self.count(text);
if let Some(max) = self.max_length {
if count > max {
return LimitCheckResult::Exceeded {
current: count,
max,
};
}
if let Some(warning_threshold) = self.warning_threshold {
if count >= warning_threshold {
return LimitCheckResult::Warning {
current: count,
max,
};
}
}
}
if let Some(min) = self.min_length {
if count < min {
return LimitCheckResult::TooShort {
current: count,
min,
};
}
}
LimitCheckResult::Ok
}
pub fn status_text(&self, text: &str) -> Option<String> {
match self.check_limits(text) {
LimitCheckResult::Ok => {
self.max_length
.map(|max| format!("{}/{}", self.count(text), max))
}
LimitCheckResult::Warning { current, max } => {
Some(format!("{current}/{max} (approaching limit)"))
}
LimitCheckResult::Exceeded { current, max } => {
Some(format!("{current}/{max} (exceeded)"))
}
LimitCheckResult::TooShort { current, min } => Some(format!("{current}/{min} minimum")),
}
}
pub fn allows_field_switch(&self, text: &str) -> bool {
if let Some(min) = self.min_length {
let count = self.count(text);
count == 0 || count >= min
} else {
true }
}
pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
if let Some(min) = self.min_length {
let count = self.count(text);
if count > 0 && count < min {
return Some(format!(
"Field must be empty or have at least {min} characters (currently: {count})"
));
}
}
None
}
}
pub fn count_text(text: &str, mode: CountMode) -> usize {
match mode {
CountMode::Characters => text.chars().count(),
CountMode::DisplayWidth => text.width(),
CountMode::Bytes => text.len(),
}
}
impl Default for CharacterLimits {
fn default() -> Self {
Self {
max_length: Some(30), min_length: None,
warning_threshold: None,
count_mode: CountMode::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_character_limits_creation() {
let limits = CharacterLimits::new(10);
assert_eq!(limits.max_length(), Some(10));
assert_eq!(limits.min_length(), None);
let range_limits = CharacterLimits::new_range(5, 15);
assert_eq!(range_limits.min_length(), Some(5));
assert_eq!(range_limits.max_length(), Some(15));
}
#[test]
fn test_default_limits() {
let limits = CharacterLimits::default();
assert_eq!(limits.max_length(), Some(30));
}
#[test]
fn test_character_counting() {
let limits = CharacterLimits::new(5);
assert_eq!(limits.count("hello"), 5);
assert_eq!(limits.count("héllo"), 5);
let limits = limits.with_count_mode(CountMode::DisplayWidth);
assert_eq!(limits.count("hello"), 5);
let limits = limits.with_count_mode(CountMode::Bytes);
assert_eq!(limits.count("hello"), 5);
assert_eq!(limits.count("héllo"), 6); }
#[test]
fn test_insertion_validation() {
let limits = CharacterLimits::new(5);
let result = limits.validate_insertion("test", 4, 'x');
assert!(result.is_none());
let result = limits.validate_insertion("tests", 5, 'x');
assert!(result.is_some());
assert!(!result.unwrap().is_acceptable());
}
#[test]
fn test_content_validation() {
let limits = CharacterLimits::new_range(3, 10);
let result = limits.validate_content("hi");
assert!(result.is_some());
assert!(result.unwrap().is_acceptable());
let result = limits.validate_content("hello");
assert!(result.is_none());
let result = limits.validate_content("hello world!");
assert!(result.is_some());
assert!(!result.unwrap().is_acceptable()); }
#[test]
fn test_warning_threshold() {
let limits = CharacterLimits::new(10).with_warning_threshold(8);
let result = limits.validate_insertion("123456", 6, 'x');
assert!(result.is_none());
let result = limits.validate_insertion("1234567", 7, 'x');
assert!(result.is_some()); assert!(result.unwrap().is_acceptable());
let result = limits.validate_insertion("12345678", 8, 'x');
assert!(result.is_none());
}
#[test]
fn test_status_text() {
let limits = CharacterLimits::new(10);
assert_eq!(limits.status_text("hello"), Some("5/10".to_string()));
let limits = limits.with_warning_threshold(8);
assert_eq!(
limits.status_text("12345678"),
Some("8/10 (approaching limit)".to_string())
);
assert_eq!(
limits.status_text("1234567890x"),
Some("11/10 (exceeded)".to_string())
);
}
#[test]
fn test_field_switch_blocking() {
let limits = CharacterLimits::new_range(3, 10);
assert!(limits.allows_field_switch(""));
assert!(limits.field_switch_block_reason("").is_none());
assert!(!limits.allows_field_switch("hi"));
assert!(limits.field_switch_block_reason("hi").is_some());
assert!(limits
.field_switch_block_reason("hi")
.unwrap()
.contains("at least 3 characters"));
assert!(limits.allows_field_switch("hello"));
assert!(limits.field_switch_block_reason("hello").is_none());
assert!(limits.allows_field_switch("this is way too long"));
assert!(limits
.field_switch_block_reason("this is way too long")
.is_none());
}
#[test]
fn test_field_switch_no_minimum() {
let limits = CharacterLimits::new(10);
assert!(limits.allows_field_switch(""));
assert!(limits.allows_field_switch("a"));
assert!(limits.allows_field_switch("hello"));
assert!(limits.field_switch_block_reason("").is_none());
assert!(limits.field_switch_block_reason("a").is_none());
}
}