1use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
9pub struct PositionFilter {
10 pub positions: PositionRange,
12 pub filter: CharacterFilter,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub enum PositionRange {
19 Single(usize),
21 Range(usize, usize),
23 From(usize),
25 Multiple(Vec<usize>),
27}
28
29pub enum CharacterFilter {
31 Alphabetic,
33 Numeric,
35 Alphanumeric,
37 Exact(char),
39 OneOf(Vec<char>),
41 Custom(Arc<dyn Fn(char) -> bool + Send + Sync>),
43}
44
45impl std::fmt::Debug for CharacterFilter {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 CharacterFilter::Alphabetic => write!(f, "Alphabetic"),
50 CharacterFilter::Numeric => write!(f, "Numeric"),
51 CharacterFilter::Alphanumeric => write!(f, "Alphanumeric"),
52 CharacterFilter::Exact(ch) => write!(f, "Exact('{ch}')"),
53 CharacterFilter::OneOf(chars) => write!(f, "OneOf({chars:?})"),
54 CharacterFilter::Custom(_) => write!(f, "Custom(<function>)"),
55 }
56 }
57}
58
59impl Clone for CharacterFilter {
60 fn clone(&self) -> Self {
61 match self {
62 CharacterFilter::Alphabetic => CharacterFilter::Alphabetic,
63 CharacterFilter::Numeric => CharacterFilter::Numeric,
64 CharacterFilter::Alphanumeric => CharacterFilter::Alphanumeric,
65 CharacterFilter::Exact(ch) => CharacterFilter::Exact(*ch),
66 CharacterFilter::OneOf(chars) => CharacterFilter::OneOf(chars.clone()),
67 CharacterFilter::Custom(func) => CharacterFilter::Custom(Arc::clone(func)),
68 }
69 }
70}
71
72impl PositionRange {
73 pub fn contains(&self, position: usize) -> bool {
75 match self {
76 PositionRange::Single(pos) => position == *pos,
77 PositionRange::Range(start, end) => position >= *start && position <= *end,
78 PositionRange::From(start) => position >= *start,
79 PositionRange::Multiple(positions) => positions.contains(&position),
80 }
81 }
82
83 pub fn positions_up_to(&self, max_length: usize) -> Vec<usize> {
85 match self {
86 PositionRange::Single(pos) => {
87 if *pos < max_length {
88 vec![*pos]
89 } else {
90 vec![]
91 }
92 }
93 PositionRange::Range(start, end) => {
94 let actual_end = (*end).min(max_length.saturating_sub(1));
95 if *start <= actual_end {
96 (*start..=actual_end).collect()
97 } else {
98 vec![]
99 }
100 }
101 PositionRange::From(start) => {
102 if *start < max_length {
103 (*start..max_length).collect()
104 } else {
105 vec![]
106 }
107 }
108 PositionRange::Multiple(positions) => positions
109 .iter()
110 .filter(|&&pos| pos < max_length)
111 .copied()
112 .collect(),
113 }
114 }
115}
116
117impl CharacterFilter {
118 pub fn accepts(&self, ch: char) -> bool {
120 match self {
121 CharacterFilter::Alphabetic => ch.is_alphabetic(),
122 CharacterFilter::Numeric => ch.is_numeric(),
123 CharacterFilter::Alphanumeric => ch.is_alphanumeric(),
124 CharacterFilter::Exact(expected) => ch == *expected,
125 CharacterFilter::OneOf(chars) => chars.contains(&ch),
126 CharacterFilter::Custom(func) => func(ch),
127 }
128 }
129
130 pub fn description(&self) -> String {
132 match self {
133 CharacterFilter::Alphabetic => "alphabetic characters (a-z, A-Z)".to_string(),
134 CharacterFilter::Numeric => "numeric characters (0-9)".to_string(),
135 CharacterFilter::Alphanumeric => "alphanumeric characters (a-z, A-Z, 0-9)".to_string(),
136 CharacterFilter::Exact(ch) => format!("exactly '{ch}'"),
137 CharacterFilter::OneOf(chars) => {
138 let char_list: String = chars.iter().collect();
139 format!("one of: {char_list}")
140 }
141 CharacterFilter::Custom(_) => "custom filter".to_string(),
142 }
143 }
144}
145
146impl PositionFilter {
147 pub fn new(positions: PositionRange, filter: CharacterFilter) -> Self {
149 Self { positions, filter }
150 }
151
152 pub fn validate_position(&self, position: usize, character: char) -> bool {
154 if self.positions.contains(position) {
155 self.filter.accepts(character)
156 } else {
157 true }
159 }
160
161 pub fn error_message(&self, position: usize, character: char) -> Option<String> {
163 if self.positions.contains(position) && !self.filter.accepts(character) {
164 Some(format!(
165 "Position {} requires {} but got '{}'",
166 position,
167 self.filter.description(),
168 character
169 ))
170 } else {
171 None
172 }
173 }
174}
175
176#[derive(Debug, Clone, Default)]
178pub struct PatternFilters {
179 filters: Vec<PositionFilter>,
180}
181
182impl PatternFilters {
183 pub fn new() -> Self {
185 Self::default()
186 }
187
188 pub fn add_filter(mut self, filter: PositionFilter) -> Self {
190 self.filters.push(filter);
191 self
192 }
193
194 pub fn add_filters(mut self, filters: Vec<PositionFilter>) -> Self {
196 self.filters.extend(filters);
197 self
198 }
199
200 pub fn validate_char_at_position(
202 &self,
203 position: usize,
204 character: char,
205 ) -> Result<(), String> {
206 for filter in &self.filters {
207 if let Some(error) = filter.error_message(position, character) {
208 return Err(error);
209 }
210 }
211 Ok(())
212 }
213
214 pub fn validate_text(&self, text: &str) -> Result<(), String> {
216 for (position, character) in text.char_indices() {
217 self.validate_char_at_position(position, character)?
218 }
219 Ok(())
220 }
221
222 pub fn has_filters(&self) -> bool {
224 !self.filters.is_empty()
225 }
226
227 pub fn filters(&self) -> &[PositionFilter] {
229 &self.filters
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_position_range_contains() {
239 assert!(PositionRange::Single(3).contains(3));
240 assert!(!PositionRange::Single(3).contains(2));
241
242 assert!(PositionRange::Range(1, 4).contains(3));
243 assert!(!PositionRange::Range(1, 4).contains(5));
244
245 assert!(PositionRange::From(2).contains(5));
246 assert!(!PositionRange::From(2).contains(1));
247
248 assert!(PositionRange::Multiple(vec![0, 2, 5]).contains(2));
249 assert!(!PositionRange::Multiple(vec![0, 2, 5]).contains(3));
250 }
251
252 #[test]
253 fn test_position_range_positions_up_to() {
254 assert_eq!(PositionRange::Single(3).positions_up_to(5), vec![3]);
255 assert_eq!(PositionRange::Single(5).positions_up_to(3), vec![]);
256
257 assert_eq!(PositionRange::Range(1, 3).positions_up_to(5), vec![1, 2, 3]);
258 assert_eq!(PositionRange::Range(1, 5).positions_up_to(3), vec![1, 2]);
259
260 assert_eq!(PositionRange::From(2).positions_up_to(5), vec![2, 3, 4]);
261
262 assert_eq!(
263 PositionRange::Multiple(vec![0, 2, 5]).positions_up_to(4),
264 vec![0, 2]
265 );
266 }
267
268 #[test]
269 fn test_character_filter_accepts() {
270 assert!(CharacterFilter::Alphabetic.accepts('a'));
271 assert!(CharacterFilter::Alphabetic.accepts('Z'));
272 assert!(!CharacterFilter::Alphabetic.accepts('1'));
273
274 assert!(CharacterFilter::Numeric.accepts('5'));
275 assert!(!CharacterFilter::Numeric.accepts('a'));
276
277 assert!(CharacterFilter::Alphanumeric.accepts('a'));
278 assert!(CharacterFilter::Alphanumeric.accepts('5'));
279 assert!(!CharacterFilter::Alphanumeric.accepts('-'));
280
281 assert!(CharacterFilter::Exact('x').accepts('x'));
282 assert!(!CharacterFilter::Exact('x').accepts('y'));
283
284 assert!(CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('b'));
285 assert!(!CharacterFilter::OneOf(vec!['a', 'b', 'c']).accepts('d'));
286 }
287
288 #[test]
289 fn test_position_filter_validation() {
290 let filter = PositionFilter::new(PositionRange::Range(0, 1), CharacterFilter::Alphabetic);
291
292 assert!(filter.validate_position(0, 'A'));
293 assert!(filter.validate_position(1, 'b'));
294 assert!(!filter.validate_position(0, '1'));
295 assert!(filter.validate_position(2, '1')); }
297
298 #[test]
299 fn test_pattern_filters_validation() {
300 let patterns = PatternFilters::new()
301 .add_filter(PositionFilter::new(
302 PositionRange::Range(0, 1),
303 CharacterFilter::Alphabetic,
304 ))
305 .add_filter(PositionFilter::new(
306 PositionRange::Range(2, 4),
307 CharacterFilter::Numeric,
308 ));
309
310 assert!(patterns.validate_text("AB123").is_ok());
312
313 assert!(patterns.validate_text("A1123").is_err());
315
316 assert!(patterns.validate_text("AB1A3").is_err());
318 }
319
320 #[test]
321 fn test_custom_filter() {
322 let pattern = PatternFilters::new().add_filter(PositionFilter::new(
323 PositionRange::From(0),
324 CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())),
325 ));
326
327 assert!(pattern.validate_text("hello").is_ok());
328 assert!(pattern.validate_text("Hello").is_err()); }
330}