1use validator::Validate;
6
7use crate::error::{PhyTraceError, PhyTraceResult};
8use crate::models::event::UdmEvent;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ValidationLevel {
13 None,
15 Basic,
17 Full,
19}
20
21#[derive(Debug, Clone)]
23pub struct Validator {
24 level: ValidationLevel,
25}
26
27impl Default for Validator {
28 fn default() -> Self {
29 Self {
30 level: ValidationLevel::Basic,
31 }
32 }
33}
34
35impl Validator {
36 pub fn new(level: ValidationLevel) -> Self {
38 Self { level }
39 }
40
41 pub fn validate(&self, event: &UdmEvent) -> PhyTraceResult<()> {
43 match self.level {
44 ValidationLevel::None => Ok(()),
45 ValidationLevel::Basic => self.validate_basic(event),
46 ValidationLevel::Full => self.validate_full(event),
47 }
48 }
49
50 fn validate_basic(&self, event: &UdmEvent) -> PhyTraceResult<()> {
52 if event.event_id.is_empty() {
54 return Err(PhyTraceError::Validation(
55 "event_id is required".to_string(),
56 ));
57 }
58
59 if event.udm_version.is_empty() {
60 return Err(PhyTraceError::Validation(
61 "udm_version is required".to_string(),
62 ));
63 }
64
65 if event.sdk_version.is_empty() {
66 return Err(PhyTraceError::Validation(
67 "sdk_version is required".to_string(),
68 ));
69 }
70
71 Ok(())
72 }
73
74 fn validate_full(&self, event: &UdmEvent) -> PhyTraceResult<()> {
76 self.validate_basic(event)?;
78
79 event
81 .validate()
82 .map_err(|e| PhyTraceError::Validation(format!("Event validation failed: {}", e)))?;
83
84 if let Some(ref identity) = event.identity {
86 self.validate_identity(identity)?;
87 }
88
89 if let Some(ref location) = event.location {
90 self.validate_location(location)?;
91 }
92
93 if let Some(ref power) = event.power {
94 self.validate_power(power)?;
95 }
96
97 if let Some(ref safety) = event.safety {
98 self.validate_safety(safety)?;
99 }
100
101 Ok(())
102 }
103
104 fn validate_identity(
106 &self,
107 identity: &crate::models::domains::IdentityDomain,
108 ) -> PhyTraceResult<()> {
109 if let Some(ref source_id) = identity.source_id {
111 if source_id.is_empty() {
112 return Err(PhyTraceError::Validation(
113 "identity.source_id cannot be empty".to_string(),
114 ));
115 }
116 }
117 Ok(())
118 }
119
120 fn validate_location(
122 &self,
123 location: &crate::models::domains::LocationDomain,
124 ) -> PhyTraceResult<()> {
125 if let Some(lat) = location.latitude {
127 if !(-90.0..=90.0).contains(&lat) {
128 return Err(PhyTraceError::Validation(format!(
129 "latitude must be between -90 and 90, got {}",
130 lat
131 )));
132 }
133 }
134
135 if let Some(lon) = location.longitude {
136 if !(-180.0..=180.0).contains(&lon) {
137 return Err(PhyTraceError::Validation(format!(
138 "longitude must be between -180 and 180, got {}",
139 lon
140 )));
141 }
142 }
143
144 if let Some(heading) = location.heading_deg {
146 if !(0.0..=360.0).contains(&heading) && !(-180.0..=180.0).contains(&heading) {
147 return Err(PhyTraceError::Validation(format!(
148 "heading_deg should be in range [0, 360] or [-180, 180], got {}",
149 heading
150 )));
151 }
152 }
153
154 Ok(())
155 }
156
157 fn validate_power(&self, power: &crate::models::domains::PowerDomain) -> PhyTraceResult<()> {
159 if let Some(ref battery) = power.battery {
160 if let Some(soc) = battery.state_of_charge_pct {
162 if !(0.0..=100.0).contains(&soc) {
163 return Err(PhyTraceError::Validation(format!(
164 "battery.state_of_charge_pct must be between 0 and 100, got {}",
165 soc
166 )));
167 }
168 }
169
170 if let Some(health) = battery.state_of_health_pct {
172 if !(0.0..=100.0).contains(&health) {
173 return Err(PhyTraceError::Validation(format!(
174 "battery.state_of_health_pct must be between 0 and 100, got {}",
175 health
176 )));
177 }
178 }
179
180 if let Some(voltage) = battery.voltage_v {
182 if voltage < 0.0 {
183 return Err(PhyTraceError::Validation(format!(
184 "battery.voltage_v must be non-negative, got {}",
185 voltage
186 )));
187 }
188 }
189 }
190
191 Ok(())
192 }
193
194 fn validate_safety(&self, safety: &crate::models::domains::SafetyDomain) -> PhyTraceResult<()> {
196 if let Some(ref proximity) = safety.proximity {
198 if let Some(closest) = proximity.closest_distance_m {
199 if closest < 0.0 {
200 return Err(PhyTraceError::Validation(format!(
201 "safety.proximity.closest_distance_m must be non-negative, got {}",
202 closest
203 )));
204 }
205 }
206 }
207
208 Ok(())
209 }
210}
211
212pub fn validate_event(event: &UdmEvent) -> PhyTraceResult<()> {
214 Validator::default().validate(event)
215}
216
217pub fn validate_event_full(event: &UdmEvent) -> PhyTraceResult<()> {
219 Validator::new(ValidationLevel::Full).validate(event)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::models::domains::power::Battery;
226 use crate::models::domains::safety::ProximityInfo;
227 use crate::models::domains::{IdentityDomain, LocationDomain, PowerDomain, SafetyDomain};
228 use crate::models::enums::SourceType;
229
230 fn valid_event() -> UdmEvent {
231 UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
232 source_id: Some("robot-001".to_string()),
233 ..Default::default()
234 })
235 }
236
237 #[test]
238 fn test_basic_validation_passes() {
239 let event = valid_event();
240 let validator = Validator::new(ValidationLevel::Basic);
241 validator.validate(&event).unwrap();
242 }
243
244 #[test]
245 fn test_full_validation_passes() {
246 let event = valid_event();
247 let validator = Validator::new(ValidationLevel::Full);
248 validator.validate(&event).unwrap();
249 }
250
251 #[test]
252 fn test_empty_event_id_fails() {
253 let mut event = valid_event();
254 event.event_id = String::new();
255
256 let validator = Validator::new(ValidationLevel::Basic);
257 validator.validate(&event).unwrap_err();
258 }
259
260 #[test]
261 fn test_invalid_latitude_fails() {
262 let event = valid_event().with_location(LocationDomain {
263 latitude: Some(100.0), ..Default::default()
265 });
266
267 let validator = Validator::new(ValidationLevel::Full);
268 validator.validate(&event).unwrap_err();
269 }
270
271 #[test]
272 fn test_invalid_soc_fails() {
273 let event = valid_event().with_power(PowerDomain {
274 battery: Some(Battery {
275 state_of_charge_pct: Some(150.0), ..Default::default()
277 }),
278 ..Default::default()
279 });
280
281 let validator = Validator::new(ValidationLevel::Full);
282 validator.validate(&event).unwrap_err();
283 }
284
285 #[test]
286 fn test_validation_none_always_passes() {
287 let mut event = valid_event();
288 event.event_id = String::new(); let validator = Validator::new(ValidationLevel::None);
291 validator.validate(&event).unwrap();
292 }
293
294 #[test]
297 fn test_default_validator() {
298 let validator = Validator::default();
299 let event = valid_event();
300 validator.validate(&event).unwrap();
301 }
302
303 #[test]
304 fn test_convenience_validate_event() {
305 let event = valid_event();
306 validate_event(&event).unwrap();
307 }
308
309 #[test]
310 fn test_convenience_validate_event_full() {
311 let event = valid_event();
312 validate_event_full(&event).unwrap();
313 }
314
315 #[test]
316 fn test_empty_udm_version_fails() {
317 let mut event = valid_event();
318 event.udm_version = String::new();
319
320 let validator = Validator::new(ValidationLevel::Basic);
321 let result = validator.validate(&event);
322 assert!(result.unwrap_err().to_string().contains("udm_version"));
323 }
324
325 #[test]
326 fn test_empty_sdk_version_fails() {
327 let mut event = valid_event();
328 event.sdk_version = String::new();
329
330 let validator = Validator::new(ValidationLevel::Basic);
331 let result = validator.validate(&event);
332 assert!(result.unwrap_err().to_string().contains("sdk_version"));
333 }
334
335 #[test]
336 fn test_empty_identity_source_id_fails() {
337 let event = valid_event().with_identity(IdentityDomain {
338 source_id: Some(String::new()), ..Default::default()
340 });
341
342 let validator = Validator::new(ValidationLevel::Full);
343 let result = validator.validate(&event);
344 assert!(result
345 .unwrap_err()
346 .to_string()
347 .contains("identity.source_id"));
348 }
349
350 #[test]
351 fn test_identity_without_source_id_passes() {
352 let event = valid_event().with_identity(IdentityDomain {
354 source_id: None,
355 ..Default::default()
356 });
357
358 let validator = Validator::new(ValidationLevel::Full);
359 validator.validate(&event).unwrap();
360 }
361
362 #[test]
363 fn test_invalid_negative_latitude_fails() {
364 let event = valid_event().with_location(LocationDomain {
365 latitude: Some(-100.0), ..Default::default()
367 });
368
369 let validator = Validator::new(ValidationLevel::Full);
370 let result = validator.validate(&event);
371 assert!(result.unwrap_err().to_string().contains("latitude"));
372 }
373
374 #[test]
375 fn test_invalid_longitude_too_high_fails() {
376 let event = valid_event().with_location(LocationDomain {
377 longitude: Some(200.0), ..Default::default()
379 });
380
381 let validator = Validator::new(ValidationLevel::Full);
382 let result = validator.validate(&event);
383 assert!(result.unwrap_err().to_string().contains("longitude"));
384 }
385
386 #[test]
387 fn test_invalid_longitude_too_low_fails() {
388 let event = valid_event().with_location(LocationDomain {
389 longitude: Some(-200.0), ..Default::default()
391 });
392
393 let validator = Validator::new(ValidationLevel::Full);
394 let result = validator.validate(&event);
395 assert!(result.unwrap_err().to_string().contains("longitude"));
396 }
397
398 #[test]
399 fn test_invalid_heading_fails() {
400 let event = valid_event().with_location(LocationDomain {
401 heading_deg: Some(400.0), ..Default::default()
403 });
404
405 let validator = Validator::new(ValidationLevel::Full);
406 let result = validator.validate(&event);
407 assert!(result.unwrap_err().to_string().contains("heading_deg"));
408 }
409
410 #[test]
411 fn test_valid_heading_0_360_passes() {
412 let event = valid_event().with_location(LocationDomain {
413 heading_deg: Some(270.0), ..Default::default()
415 });
416
417 let validator = Validator::new(ValidationLevel::Full);
418 validator.validate(&event).unwrap();
419 }
420
421 #[test]
422 fn test_valid_heading_negative_passes() {
423 let event = valid_event().with_location(LocationDomain {
424 heading_deg: Some(-90.0), ..Default::default()
426 });
427
428 let validator = Validator::new(ValidationLevel::Full);
429 validator.validate(&event).unwrap();
430 }
431
432 #[test]
433 fn test_valid_location_with_all_fields() {
434 let event = valid_event().with_location(LocationDomain {
435 latitude: Some(45.0),
436 longitude: Some(-122.0),
437 heading_deg: Some(180.0),
438 ..Default::default()
439 });
440
441 let validator = Validator::new(ValidationLevel::Full);
442 validator.validate(&event).unwrap();
443 }
444
445 #[test]
446 fn test_invalid_soc_negative_fails() {
447 let event = valid_event().with_power(PowerDomain {
448 battery: Some(Battery {
449 state_of_charge_pct: Some(-10.0), ..Default::default()
451 }),
452 ..Default::default()
453 });
454
455 let validator = Validator::new(ValidationLevel::Full);
456 let result = validator.validate(&event);
457 assert!(result
458 .unwrap_err()
459 .to_string()
460 .contains("state_of_charge_pct"));
461 }
462
463 #[test]
464 fn test_invalid_soh_too_high_fails() {
465 let event = valid_event().with_power(PowerDomain {
466 battery: Some(Battery {
467 state_of_health_pct: Some(110.0), ..Default::default()
469 }),
470 ..Default::default()
471 });
472
473 let validator = Validator::new(ValidationLevel::Full);
474 let result = validator.validate(&event);
475 assert!(result
476 .unwrap_err()
477 .to_string()
478 .contains("state_of_health_pct"));
479 }
480
481 #[test]
482 fn test_invalid_soh_negative_fails() {
483 let event = valid_event().with_power(PowerDomain {
484 battery: Some(Battery {
485 state_of_health_pct: Some(-5.0), ..Default::default()
487 }),
488 ..Default::default()
489 });
490
491 let validator = Validator::new(ValidationLevel::Full);
492 let result = validator.validate(&event);
493 assert!(result
494 .unwrap_err()
495 .to_string()
496 .contains("state_of_health_pct"));
497 }
498
499 #[test]
500 fn test_invalid_voltage_negative_fails() {
501 let event = valid_event().with_power(PowerDomain {
502 battery: Some(Battery {
503 voltage_v: Some(-12.0), ..Default::default()
505 }),
506 ..Default::default()
507 });
508
509 let validator = Validator::new(ValidationLevel::Full);
510 let result = validator.validate(&event);
511 assert!(result.unwrap_err().to_string().contains("voltage_v"));
512 }
513
514 #[test]
515 fn test_valid_power_with_all_fields() {
516 let event = valid_event().with_power(PowerDomain {
517 battery: Some(Battery {
518 state_of_charge_pct: Some(75.0),
519 state_of_health_pct: Some(95.0),
520 voltage_v: Some(48.0),
521 ..Default::default()
522 }),
523 ..Default::default()
524 });
525
526 let validator = Validator::new(ValidationLevel::Full);
527 validator.validate(&event).unwrap();
528 }
529
530 #[test]
531 fn test_power_without_battery_passes() {
532 let event = valid_event().with_power(PowerDomain {
533 battery: None,
534 ..Default::default()
535 });
536
537 let validator = Validator::new(ValidationLevel::Full);
538 validator.validate(&event).unwrap();
539 }
540
541 #[test]
542 fn test_invalid_proximity_distance_negative_fails() {
543 let event = valid_event().with_safety(SafetyDomain {
544 proximity: Some(ProximityInfo {
545 closest_distance_m: Some(-1.0), ..Default::default()
547 }),
548 ..Default::default()
549 });
550
551 let validator = Validator::new(ValidationLevel::Full);
552 let result = validator.validate(&event);
553 assert!(result
554 .unwrap_err()
555 .to_string()
556 .contains("closest_distance_m"));
557 }
558
559 #[test]
560 fn test_valid_proximity_distance_passes() {
561 let event = valid_event().with_safety(SafetyDomain {
562 proximity: Some(ProximityInfo {
563 closest_distance_m: Some(2.5),
564 ..Default::default()
565 }),
566 ..Default::default()
567 });
568
569 let validator = Validator::new(ValidationLevel::Full);
570 validator.validate(&event).unwrap();
571 }
572
573 #[test]
574 fn test_safety_without_proximity_passes() {
575 let event = valid_event().with_safety(SafetyDomain {
576 proximity: None,
577 ..Default::default()
578 });
579
580 let validator = Validator::new(ValidationLevel::Full);
581 validator.validate(&event).unwrap();
582 }
583
584 #[test]
585 fn test_proximity_without_closest_distance_passes() {
586 let event = valid_event().with_safety(SafetyDomain {
587 proximity: Some(ProximityInfo {
588 closest_distance_m: None,
589 humans_nearby: Some(2),
590 ..Default::default()
591 }),
592 ..Default::default()
593 });
594
595 let validator = Validator::new(ValidationLevel::Full);
596 validator.validate(&event).unwrap();
597 }
598
599 #[test]
600 fn test_full_validation_with_all_domains() {
601 let event = valid_event()
602 .with_identity(IdentityDomain {
603 source_id: Some("robot-001".to_string()),
604 ..Default::default()
605 })
606 .with_location(LocationDomain {
607 latitude: Some(37.7749),
608 longitude: Some(-122.4194),
609 heading_deg: Some(45.0),
610 ..Default::default()
611 })
612 .with_power(PowerDomain {
613 battery: Some(Battery {
614 state_of_charge_pct: Some(80.0),
615 state_of_health_pct: Some(98.0),
616 voltage_v: Some(24.0),
617 ..Default::default()
618 }),
619 ..Default::default()
620 })
621 .with_safety(SafetyDomain {
622 proximity: Some(ProximityInfo {
623 closest_distance_m: Some(3.0),
624 ..Default::default()
625 }),
626 ..Default::default()
627 });
628
629 let validator = Validator::new(ValidationLevel::Full);
630 validator.validate(&event).unwrap();
631 }
632
633 #[test]
634 fn test_edge_case_latitude_boundaries() {
635 let event_min = valid_event().with_location(LocationDomain {
637 latitude: Some(-90.0),
638 ..Default::default()
639 });
640 let event_max = valid_event().with_location(LocationDomain {
641 latitude: Some(90.0),
642 ..Default::default()
643 });
644
645 let validator = Validator::new(ValidationLevel::Full);
646 validator.validate(&event_min).unwrap();
647 validator.validate(&event_max).unwrap();
648 }
649
650 #[test]
651 fn test_edge_case_longitude_boundaries() {
652 let event_min = valid_event().with_location(LocationDomain {
654 longitude: Some(-180.0),
655 ..Default::default()
656 });
657 let event_max = valid_event().with_location(LocationDomain {
658 longitude: Some(180.0),
659 ..Default::default()
660 });
661
662 let validator = Validator::new(ValidationLevel::Full);
663 validator.validate(&event_min).unwrap();
664 validator.validate(&event_max).unwrap();
665 }
666
667 #[test]
668 fn test_edge_case_soc_boundaries() {
669 let event_min = valid_event().with_power(PowerDomain {
671 battery: Some(Battery {
672 state_of_charge_pct: Some(0.0),
673 ..Default::default()
674 }),
675 ..Default::default()
676 });
677 let event_max = valid_event().with_power(PowerDomain {
678 battery: Some(Battery {
679 state_of_charge_pct: Some(100.0),
680 ..Default::default()
681 }),
682 ..Default::default()
683 });
684
685 let validator = Validator::new(ValidationLevel::Full);
686 validator.validate(&event_min).unwrap();
687 validator.validate(&event_max).unwrap();
688 }
689
690 #[test]
691 fn test_edge_case_zero_voltage_passes() {
692 let event = valid_event().with_power(PowerDomain {
693 battery: Some(Battery {
694 voltage_v: Some(0.0), ..Default::default()
696 }),
697 ..Default::default()
698 });
699
700 let validator = Validator::new(ValidationLevel::Full);
701 validator.validate(&event).unwrap();
702 }
703
704 #[test]
705 fn test_edge_case_zero_proximity_passes() {
706 let event = valid_event().with_safety(SafetyDomain {
707 proximity: Some(ProximityInfo {
708 closest_distance_m: Some(0.0), ..Default::default()
710 }),
711 ..Default::default()
712 });
713
714 let validator = Validator::new(ValidationLevel::Full);
715 validator.validate(&event).unwrap();
716 }
717}