1use std::collections::HashSet;
7use std::collections::hash_map::DefaultHasher;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10use std::sync::Mutex;
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct InputId(Uuid);
19
20impl InputId {
21 #[must_use]
23 pub fn new() -> Self {
24 Self(Uuid::new_v4())
25 }
26
27 #[must_use]
29 pub fn from_uuid(uuid: Uuid) -> Self {
30 Self(uuid)
31 }
32
33 #[must_use]
35 pub fn as_uuid(&self) -> &Uuid {
36 &self.0
37 }
38}
39
40impl Default for InputId {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl fmt::Display for InputId {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}", self.0)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54pub enum InputState {
55 Submitted,
57 Admitted,
59 Processing,
61 Completed,
63 Rejected,
65}
66
67impl InputState {
68 #[must_use]
70 pub fn is_terminal(&self) -> bool {
71 matches!(self, Self::Completed | Self::Rejected)
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct InputRecord {
78 pub id: InputId,
80 pub session_id: Uuid,
82 pub state: InputState,
84 pub content: String,
86 pub fingerprint: u64,
88 pub rejection_reason: Option<String>,
90 pub submitted_at: DateTime<Utc>,
92 pub updated_at: DateTime<Utc>,
94}
95
96impl InputRecord {
97 #[must_use]
99 pub fn new(session_id: Uuid, content: String) -> Self {
100 let fingerprint = Self::compute_fingerprint(&content);
101 let now = Utc::now();
102 Self {
103 id: InputId::new(),
104 session_id,
105 state: InputState::Submitted,
106 content,
107 fingerprint,
108 rejection_reason: None,
109 submitted_at: now,
110 updated_at: now,
111 }
112 }
113
114 fn compute_fingerprint(content: &str) -> u64 {
116 let mut hasher = DefaultHasher::new();
117 content.trim().hash(&mut hasher);
118 hasher.finish()
119 }
120
121 pub fn update_state(&mut self, state: InputState) {
123 self.state = state;
124 self.updated_at = Utc::now();
125 }
126
127 pub fn reject(&mut self, reason: String) {
129 self.state = InputState::Rejected;
130 self.rejection_reason = Some(reason);
131 self.updated_at = Utc::now();
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum InputEvent {
138 Submitted {
140 input_id: InputId,
142 session_id: Uuid,
144 content: String,
146 fingerprint: u64,
148 timestamp: DateTime<Utc>,
150 },
151 Admitted {
153 input_id: InputId,
155 timestamp: DateTime<Utc>,
157 },
158 Rejected {
160 input_id: InputId,
162 reason: String,
164 timestamp: DateTime<Utc>,
166 },
167 Processing {
169 input_id: InputId,
171 timestamp: DateTime<Utc>,
173 },
174 Completed {
176 input_id: InputId,
178 timestamp: DateTime<Utc>,
180 },
181}
182
183impl InputEvent {
184 #[must_use]
186 pub fn input_id(&self) -> InputId {
187 match self {
188 Self::Submitted { input_id, .. }
189 | Self::Admitted { input_id, .. }
190 | Self::Rejected { input_id, .. }
191 | Self::Processing { input_id, .. }
192 | Self::Completed { input_id, .. } => *input_id,
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[non_exhaustive]
200pub struct InputAdmissionConfig {
201 pub enabled: bool,
203 pub reject_empty: bool,
205 pub deduplicate: bool,
207 pub max_length: usize,
209}
210
211impl Default for InputAdmissionConfig {
212 fn default() -> Self {
213 Self {
214 enabled: true,
215 reject_empty: true,
216 deduplicate: true,
217 max_length: 0,
218 }
219 }
220}
221
222impl InputAdmissionConfig {
223 #[must_use]
225 pub fn with_enabled(mut self, enabled: bool) -> Self {
226 self.enabled = enabled;
227 self
228 }
229
230 #[must_use]
232 pub fn with_reject_empty(mut self, reject_empty: bool) -> Self {
233 self.reject_empty = reject_empty;
234 self
235 }
236
237 #[must_use]
239 pub fn with_deduplicate(mut self, deduplicate: bool) -> Self {
240 self.deduplicate = deduplicate;
241 self
242 }
243
244 #[must_use]
246 pub fn with_max_length(mut self, max_length: usize) -> Self {
247 self.max_length = max_length;
248 self
249 }
250}
251
252pub struct InputAdmission {
266 config: InputAdmissionConfig,
267 seen_fingerprints: Mutex<HashSet<u64>>,
268}
269
270impl InputAdmission {
271 #[must_use]
273 pub fn new(config: InputAdmissionConfig) -> Self {
274 Self {
275 config,
276 seen_fingerprints: Mutex::new(HashSet::new()),
277 }
278 }
279
280 pub fn admit(&self, record: &mut InputRecord) -> Result<Vec<InputEvent>, InputAdmissionError> {
289 if !self.config.enabled {
290 record.update_state(InputState::Admitted);
291 return Ok(vec![InputEvent::Admitted {
292 input_id: record.id,
293 timestamp: Utc::now(),
294 }]);
295 }
296
297 let mut events = vec![InputEvent::Submitted {
298 input_id: record.id,
299 session_id: record.session_id,
300 content: record.content.clone(),
301 fingerprint: record.fingerprint,
302 timestamp: record.submitted_at,
303 }];
304
305 if self.config.reject_empty && record.content.trim().is_empty() {
307 record.reject("empty input".to_string());
308 events.push(InputEvent::Rejected {
309 input_id: record.id,
310 reason: "empty input".to_string(),
311 timestamp: Utc::now(),
312 });
313 return Ok(events);
314 }
315
316 if self.config.max_length > 0 && record.content.len() > self.config.max_length {
318 let reason = format!("input exceeds maximum length of {}", self.config.max_length);
319 record.reject(reason.clone());
320 events.push(InputEvent::Rejected {
321 input_id: record.id,
322 reason,
323 timestamp: Utc::now(),
324 });
325 return Ok(events);
326 }
327
328 if self.config.deduplicate {
330 let mut seen = self
331 .seen_fingerprints
332 .lock()
333 .map_err(|_| InputAdmissionError::LockPoisoned)?;
334 if !seen.insert(record.fingerprint) {
335 record.reject("duplicate input".to_string());
336 events.push(InputEvent::Rejected {
337 input_id: record.id,
338 reason: "duplicate input".to_string(),
339 timestamp: Utc::now(),
340 });
341 return Ok(events);
342 }
343 }
344
345 record.update_state(InputState::Admitted);
347 events.push(InputEvent::Admitted {
348 input_id: record.id,
349 timestamp: Utc::now(),
350 });
351
352 Ok(events)
353 }
354
355 #[must_use]
357 pub fn mark_processing(&self, record: &mut InputRecord) -> InputEvent {
358 record.update_state(InputState::Processing);
359 InputEvent::Processing {
360 input_id: record.id,
361 timestamp: Utc::now(),
362 }
363 }
364
365 #[must_use]
367 pub fn mark_completed(&self, record: &mut InputRecord) -> InputEvent {
368 record.update_state(InputState::Completed);
369 InputEvent::Completed {
370 input_id: record.id,
371 timestamp: Utc::now(),
372 }
373 }
374
375 pub fn clear_cache(&self) -> Result<(), InputAdmissionError> {
382 let mut seen = self
383 .seen_fingerprints
384 .lock()
385 .map_err(|_| InputAdmissionError::LockPoisoned)?;
386 seen.clear();
387 Ok(())
388 }
389}
390
391#[derive(Debug, Clone)]
393pub enum InputAdmissionError {
394 LockPoisoned,
396}
397
398impl fmt::Display for InputAdmissionError {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 match self {
401 Self::LockPoisoned => write!(f, "input admission lock poisoned"),
402 }
403 }
404}
405
406impl std::error::Error for InputAdmissionError {}
407
408#[cfg(test)]
409#[allow(clippy::unwrap_used, clippy::expect_used)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn input_id_generation() {
415 let id1 = InputId::new();
416 let id2 = InputId::new();
417 assert_ne!(id1, id2);
418 }
419
420 #[test]
421 fn input_state_terminal() {
422 assert!(!InputState::Submitted.is_terminal());
423 assert!(!InputState::Admitted.is_terminal());
424 assert!(!InputState::Processing.is_terminal());
425 assert!(InputState::Completed.is_terminal());
426 assert!(InputState::Rejected.is_terminal());
427 }
428
429 #[test]
430 fn input_record_creation() {
431 let session_id = Uuid::new_v4();
432 let record = InputRecord::new(session_id, "hello world".to_string());
433 assert_eq!(record.state, InputState::Submitted);
434 assert_eq!(record.content, "hello world");
435 assert!(record.rejection_reason.is_none());
436 }
437
438 #[test]
439 fn admission_admits_valid_input() {
440 let config = InputAdmissionConfig::default();
441 let admission = InputAdmission::new(config);
442 let session_id = Uuid::new_v4();
443 let mut record = InputRecord::new(session_id, "hello".to_string());
444
445 let events = admission.admit(&mut record).unwrap();
446 assert_eq!(record.state, InputState::Admitted);
447 assert_eq!(events.len(), 2); }
449
450 #[test]
451 fn admission_rejects_empty_input() {
452 let config = InputAdmissionConfig::default();
453 let admission = InputAdmission::new(config);
454 let session_id = Uuid::new_v4();
455 let mut record = InputRecord::new(session_id, " ".to_string());
456
457 let events = admission.admit(&mut record).unwrap();
458 assert_eq!(record.state, InputState::Rejected);
459 assert_eq!(record.rejection_reason.as_deref(), Some("empty input"));
460 assert_eq!(events.len(), 2); }
462
463 #[test]
464 fn admission_rejects_duplicate_input() {
465 let config = InputAdmissionConfig::default();
466 let admission = InputAdmission::new(config);
467 let session_id = Uuid::new_v4();
468
469 let mut record1 = InputRecord::new(session_id, "hello".to_string());
470 let events1 = admission.admit(&mut record1).unwrap();
471 assert_eq!(record1.state, InputState::Admitted);
472 assert_eq!(events1.len(), 2);
473
474 let mut record2 = InputRecord::new(session_id, "hello".to_string());
475 let events2 = admission.admit(&mut record2).unwrap();
476 assert_eq!(record2.state, InputState::Rejected);
477 assert_eq!(record2.rejection_reason.as_deref(), Some("duplicate input"));
478 assert_eq!(events2.len(), 2); }
480
481 #[test]
482 fn admission_rejects_over_length_input() {
483 let config = InputAdmissionConfig::default().with_max_length(5);
484 let admission = InputAdmission::new(config);
485 let session_id = Uuid::new_v4();
486 let mut record = InputRecord::new(session_id, "hello world".to_string());
487
488 let events = admission.admit(&mut record).unwrap();
489 assert_eq!(record.state, InputState::Rejected);
490 assert!(
491 record
492 .rejection_reason
493 .as_ref()
494 .unwrap()
495 .contains("maximum length")
496 );
497 assert_eq!(events.len(), 2); }
499
500 #[test]
501 fn admission_disabled_admits_all() {
502 let config = InputAdmissionConfig::default().with_enabled(false);
503 let admission = InputAdmission::new(config);
504 let session_id = Uuid::new_v4();
505 let mut record = InputRecord::new(session_id, String::new());
506
507 let events = admission.admit(&mut record).unwrap();
508 assert_eq!(record.state, InputState::Admitted);
509 assert_eq!(events.len(), 1); }
511
512 #[test]
513 fn admission_dedup_disabled_allows_duplicates() {
514 let config = InputAdmissionConfig::default().with_deduplicate(false);
515 let admission = InputAdmission::new(config);
516 let session_id = Uuid::new_v4();
517
518 let mut record1 = InputRecord::new(session_id, "hello".to_string());
519 admission.admit(&mut record1).unwrap();
520
521 let mut record2 = InputRecord::new(session_id, "hello".to_string());
522 let events = admission.admit(&mut record2).unwrap();
523 assert_eq!(record2.state, InputState::Admitted);
524 assert_eq!(events.len(), 2); }
526
527 #[test]
528 fn mark_processing_and_completed() {
529 let config = InputAdmissionConfig::default();
530 let admission = InputAdmission::new(config);
531 let session_id = Uuid::new_v4();
532 let mut record = InputRecord::new(session_id, "hello".to_string());
533 admission.admit(&mut record).unwrap();
534
535 let event = admission.mark_processing(&mut record);
536 assert_eq!(record.state, InputState::Processing);
537 assert!(matches!(event, InputEvent::Processing { .. }));
538
539 let event = admission.mark_completed(&mut record);
540 assert_eq!(record.state, InputState::Completed);
541 assert!(matches!(event, InputEvent::Completed { .. }));
542 }
543
544 #[test]
545 fn clear_cache_allows_duplicate() {
546 let config = InputAdmissionConfig::default();
547 let admission = InputAdmission::new(config);
548 let session_id = Uuid::new_v4();
549
550 let mut record1 = InputRecord::new(session_id, "hello".to_string());
551 admission.admit(&mut record1).unwrap();
552
553 admission.clear_cache().unwrap();
554
555 let mut record2 = InputRecord::new(session_id, "hello".to_string());
556 let events = admission.admit(&mut record2).unwrap();
557 assert_eq!(record2.state, InputState::Admitted);
558 assert_eq!(events.len(), 2);
559 }
560}