tp_lib_core/detections/
validate.rs1use std::collections::HashMap;
11
12use crate::models::{
13 Detection, DetectionKind, DetectionRecord, DetectionStatus, DiscardReason, Netelement,
14 TimestampOrRange,
15};
16
17use super::error::DetectionError;
18
19#[derive(Debug, Clone, Default)]
22pub struct ValidationOutcome {
23 pub kept: Vec<Detection>,
24 pub duplicate_records: Vec<DetectionRecord>,
25}
26
27pub fn validate_detections(
29 detections: Vec<Detection>,
30 netelements: &[Netelement],
31) -> Result<ValidationOutcome, DetectionError> {
32 let known_ids: std::collections::HashSet<&str> =
33 netelements.iter().map(|n| n.id.as_str()).collect();
34
35 let mut seen: HashMap<(String, String), usize> = HashMap::new();
36 let mut kept: Vec<Detection> = Vec::with_capacity(detections.len());
37 let mut duplicate_records = Vec::new();
38
39 for det in detections.into_iter() {
40 match &det {
41 Detection::Punctual(p) => {
42 if let Some(loc) = &p.location {
43 if !known_ids.contains(loc.netelement_id.as_str()) {
44 return Err(DetectionError::UnknownNetelement {
45 source_file: p.source_file.clone(),
46 source_row: p.source_row,
47 netelement_id: loc.netelement_id.clone(),
48 });
49 }
50 if !(0.0..=1.0).contains(&loc.intrinsic) {
51 return Err(DetectionError::InvalidIntrinsic {
52 source_file: p.source_file.clone(),
53 source_row: p.source_row,
54 value: loc.intrinsic,
55 });
56 }
57 }
58
59 let ts_key = p.timestamp.to_rfc3339();
60 let ne_key = p
61 .location
62 .as_ref()
63 .map(|l| l.netelement_id.clone())
64 .unwrap_or_default();
65
66 if !ne_key.is_empty() {
67 for (prior_ts, prior_ne) in seen.keys() {
68 if prior_ts == &ts_key && prior_ne != &ne_key && !prior_ne.is_empty() {
69 return Err(DetectionError::ConflictingDetections {
70 timestamp: p.timestamp,
71 netelement_a: prior_ne.clone(),
72 netelement_b: ne_key.clone(),
73 });
74 }
75 }
76 }
77
78 let key = (ts_key, ne_key);
79 if !key.1.is_empty() {
80 if let Some(&kept_index) = seen.get(&key) {
81 duplicate_records.push(DetectionRecord {
82 source_file: p.source_file.clone(),
83 source_row: p.source_row,
84 kind: DetectionKind::Punctual,
85 timestamp: TimestampOrRange::Single {
86 timestamp: p.timestamp,
87 },
88 status: DetectionStatus::Discarded {
89 reason: DiscardReason::DuplicateOfPriorDetection { kept_index },
90 },
91 id: p.id.clone(),
92 source: p.source.clone(),
93 metadata: p.metadata.clone(),
94 });
95 continue;
96 }
97 seen.insert(key, kept.len());
98 }
99 kept.push(det);
100 }
101 Detection::Linear(l) => {
102 if l.t_to < l.t_from {
103 return Err(DetectionError::InvalidTimeRange {
104 source_file: l.source_file.clone(),
105 source_row: l.source_row,
106 t_from: l.t_from,
107 t_to: l.t_to,
108 });
109 }
110 if !known_ids.contains(l.netelement_id.as_str()) {
111 return Err(DetectionError::UnknownNetelement {
112 source_file: l.source_file.clone(),
113 source_row: l.source_row,
114 netelement_id: l.netelement_id.clone(),
115 });
116 }
117 if !(0.0..=1.0).contains(&l.start_intrinsic) {
118 return Err(DetectionError::InvalidIntrinsic {
119 source_file: l.source_file.clone(),
120 source_row: l.source_row,
121 value: l.start_intrinsic,
122 });
123 }
124 if !(0.0..=1.0).contains(&l.end_intrinsic) {
125 return Err(DetectionError::InvalidIntrinsic {
126 source_file: l.source_file.clone(),
127 source_row: l.source_row,
128 value: l.end_intrinsic,
129 });
130 }
131 kept.push(det);
132 }
133 }
134 }
135
136 Ok(ValidationOutcome {
137 kept,
138 duplicate_records,
139 })
140}