1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FieldType {
11 String,
13 Date,
15 Char,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum FilterOp {
22 Eq,
24 Ne,
26 Lt,
28 Le,
30 Gt,
32 Ge,
34 Like,
36}
37
38impl FilterOp {
39 pub fn parse(s: &str) -> (Self, &str) {
41 if let Some(rest) = s.strip_prefix(">=") {
42 (FilterOp::Ge, rest)
43 } else if let Some(rest) = s.strip_prefix("<=") {
44 (FilterOp::Le, rest)
45 } else if let Some(rest) = s.strip_prefix("!=") {
46 (FilterOp::Ne, rest)
47 } else if let Some(rest) = s.strip_prefix('>') {
48 (FilterOp::Gt, rest)
49 } else if let Some(rest) = s.strip_prefix('<') {
50 (FilterOp::Lt, rest)
51 } else if let Some(rest) = s.strip_prefix('=') {
52 (FilterOp::Eq, rest)
53 } else {
54 (FilterOp::Eq, s)
56 }
57 }
58
59 pub fn valid_for(&self, field_type: FieldType) -> bool {
61 match field_type {
62 FieldType::String => matches!(self, FilterOp::Eq | FilterOp::Ne | FilterOp::Like),
63 FieldType::Date => true, FieldType::Char => matches!(self, FilterOp::Eq | FilterOp::Ne),
65 }
66 }
67
68 pub fn sql(&self) -> &'static str {
70 match self {
71 FilterOp::Eq => "=",
72 FilterOp::Ne => "!=",
73 FilterOp::Lt => "<",
74 FilterOp::Le => "<=",
75 FilterOp::Gt => ">",
76 FilterOp::Ge => ">=",
77 FilterOp::Like => "LIKE",
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FilterExpr {
85 pub field: String,
86 pub op: FilterOp,
87 pub value: String,
88}
89
90impl FilterExpr {
91 pub fn parse(s: &str) -> Option<Self> {
93 let op_chars = ['>', '<', '=', '!'];
95 let op_pos = s.find(|c: char| op_chars.contains(&c))?;
96
97 let field = s[..op_pos].trim().to_lowercase();
98 let rest = &s[op_pos..];
99 let (op, value_str) = FilterOp::parse(rest);
100 let value = value_str.trim().to_string();
101
102 if field.is_empty() || value.is_empty() {
103 return None;
104 }
105
106 Some(FilterExpr { field, op, value })
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct FieldDef {
113 pub name: &'static str,
115 pub column: &'static str,
117 pub field_type: FieldType,
119 pub aliases: &'static [&'static str],
121}
122
123pub struct FieldRegistry {
125 fields: HashMap<&'static str, FieldDef>,
126}
127
128impl FieldRegistry {
129 pub fn new() -> Self {
131 let mut fields = HashMap::new();
132
133 let defs = [
134 FieldDef {
135 name: "call_sign",
136 column: "l.call_sign",
137 field_type: FieldType::String,
138 aliases: &["callsign", "call"],
139 },
140 FieldDef {
141 name: "name",
142 column: "e.entity_name",
143 field_type: FieldType::String,
144 aliases: &["entity_name", "licensee"],
145 },
146 FieldDef {
147 name: "first_name",
148 column: "e.first_name",
149 field_type: FieldType::String,
150 aliases: &["first"],
151 },
152 FieldDef {
153 name: "last_name",
154 column: "e.last_name",
155 field_type: FieldType::String,
156 aliases: &["last"],
157 },
158 FieldDef {
159 name: "city",
160 column: "e.city",
161 field_type: FieldType::String,
162 aliases: &[],
163 },
164 FieldDef {
165 name: "state",
166 column: "e.state",
167 field_type: FieldType::String,
168 aliases: &[],
169 },
170 FieldDef {
171 name: "zip_code",
172 column: "e.zip_code",
173 field_type: FieldType::String,
174 aliases: &["zip"],
175 },
176 FieldDef {
177 name: "frn",
178 column: "e.frn",
179 field_type: FieldType::String,
180 aliases: &[],
181 },
182 FieldDef {
183 name: "status",
184 column: "l.license_status",
185 field_type: FieldType::Char,
186 aliases: &["license_status"],
187 },
188 FieldDef {
189 name: "class",
190 column: "a.operator_class",
191 field_type: FieldType::Char,
192 aliases: &["operator_class"],
193 },
194 FieldDef {
195 name: "service",
196 column: "l.radio_service_code",
197 field_type: FieldType::String,
198 aliases: &["radio_service", "radio_service_code"],
199 },
200 FieldDef {
201 name: "grant_date",
202 column: "l.grant_date",
203 field_type: FieldType::Date,
204 aliases: &["granted"],
205 },
206 FieldDef {
207 name: "expired_date",
208 column: "l.expired_date",
209 field_type: FieldType::Date,
210 aliases: &["expires", "expiration"],
211 },
212 FieldDef {
213 name: "cancellation_date",
214 column: "l.cancellation_date",
215 field_type: FieldType::Date,
216 aliases: &["cancelled"],
217 },
218 ];
219
220 for def in defs {
221 fields.insert(def.name, def.clone());
223 for &alias in def.aliases {
225 fields.insert(alias, def.clone());
226 }
227 }
228
229 FieldRegistry { fields }
230 }
231
232 pub fn get(&self, name: &str) -> Option<&FieldDef> {
234 self.fields.get(name.to_lowercase().as_str())
235 }
236
237 pub fn field_names(&self) -> Vec<&'static str> {
239 let mut names: Vec<_> = self.fields.values().map(|f| f.name).collect();
240 names.sort();
241 names.dedup();
242 names
243 }
244}
245
246impl Default for FieldRegistry {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_filter_op_parse() {
258 assert_eq!(FilterOp::parse(">=2025"), (FilterOp::Ge, "2025"));
259 assert_eq!(FilterOp::parse("<=2025"), (FilterOp::Le, "2025"));
260 assert_eq!(FilterOp::parse(">2025"), (FilterOp::Gt, "2025"));
261 assert_eq!(FilterOp::parse("<2025"), (FilterOp::Lt, "2025"));
262 assert_eq!(FilterOp::parse("=TX"), (FilterOp::Eq, "TX"));
263 assert_eq!(FilterOp::parse("TX"), (FilterOp::Eq, "TX"));
264 }
265
266 #[test]
267 fn test_filter_expr_parse() {
268 let expr = FilterExpr::parse("grant_date>2025-01-01").unwrap();
269 assert_eq!(expr.field, "grant_date");
270 assert_eq!(expr.op, FilterOp::Gt);
271 assert_eq!(expr.value, "2025-01-01");
272
273 let expr = FilterExpr::parse("state=TX").unwrap();
274 assert_eq!(expr.field, "state");
275 assert_eq!(expr.op, FilterOp::Eq);
276 assert_eq!(expr.value, "TX");
277 }
278
279 #[test]
280 fn test_field_registry() {
281 let reg = FieldRegistry::new();
282
283 assert!(reg.get("call_sign").is_some());
285 assert!(reg.get("grant_date").is_some());
286
287 assert!(reg.get("callsign").is_some());
289 assert!(reg.get("granted").is_some());
290 assert!(reg.get("zip").is_some());
291
292 assert!(reg.get("unknown_field").is_none());
294 }
295
296 #[test]
297 fn test_op_validity() {
298 assert!(FilterOp::Eq.valid_for(FieldType::String));
300 assert!(FilterOp::Like.valid_for(FieldType::String));
301 assert!(!FilterOp::Gt.valid_for(FieldType::String));
302
303 assert!(FilterOp::Gt.valid_for(FieldType::Date));
305 assert!(FilterOp::Le.valid_for(FieldType::Date));
306
307 assert!(FilterOp::Eq.valid_for(FieldType::Char));
309 assert!(!FilterOp::Gt.valid_for(FieldType::Char));
310 }
311
312 #[test]
313 fn test_filter_op_parse_not_equal() {
314 assert_eq!(FilterOp::parse("!=value"), (FilterOp::Ne, "value"));
316 }
317
318 #[test]
319 fn test_filter_op_sql() {
320 assert_eq!(FilterOp::Eq.sql(), "=");
322 assert_eq!(FilterOp::Ne.sql(), "!=");
323 assert_eq!(FilterOp::Lt.sql(), "<");
324 assert_eq!(FilterOp::Le.sql(), "<=");
325 assert_eq!(FilterOp::Gt.sql(), ">");
326 assert_eq!(FilterOp::Ge.sql(), ">=");
327 assert_eq!(FilterOp::Like.sql(), "LIKE");
328 }
329
330 #[test]
331 fn test_field_registry_field_names() {
332 let reg = FieldRegistry::new();
333 let names = reg.field_names();
334
335 assert!(names.contains(&"call_sign"));
337 assert!(names.contains(&"city"));
338 assert!(names.contains(&"state"));
339 assert!(names.contains(&"grant_date"));
340 assert!(names.contains(&"expired_date"));
341
342 let unique_count = names.len();
345 let mut sorted_names = names.clone();
346 sorted_names.sort();
347 sorted_names.dedup();
348 assert_eq!(unique_count, sorted_names.len());
349 }
350
351 #[test]
352 fn test_field_registry_default() {
353 let reg: FieldRegistry = FieldRegistry::default();
355
356 assert!(reg.get("call_sign").is_some());
358 assert!(reg.get("callsign").is_some()); }
360
361 #[test]
362 fn test_filter_expr_parse_invalid() {
363 assert!(FilterExpr::parse("nooperator").is_none());
365
366 assert!(FilterExpr::parse("=value").is_none());
368
369 assert!(FilterExpr::parse("field=").is_none());
371 }
372
373 #[test]
374 fn test_filter_expr_parse_not_equal() {
375 let expr = FilterExpr::parse("status!=A").unwrap();
376 assert_eq!(expr.field, "status");
377 assert_eq!(expr.op, FilterOp::Ne);
378 assert_eq!(expr.value, "A");
379 }
380
381 #[test]
382 fn test_op_validity_ne() {
383 assert!(FilterOp::Ne.valid_for(FieldType::String));
385 assert!(FilterOp::Ne.valid_for(FieldType::Date));
386 assert!(FilterOp::Ne.valid_for(FieldType::Char));
387 }
388}