1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use validator::Validate;
11
12use crate::models::domains::{
13 ActuatorsDomain, AiDomain, AudioDomain, CommunicationDomain, ComplianceDomain, ComputeDomain,
14 ContextDomain, CoordinationDomain, EnvironmentInteractionDomain, HriDomain, IdentityDomain,
15 LocationDomain, MaintenanceDomain, ManipulationDomain, MotionDomain, NavigationDomain,
16 OperationalDomain, PayloadDomain, PerceptionDomain, PowerDomain, SafetyDomain,
17 SimulationDomain, ThermalDomain,
18};
19use crate::models::enums::{EventType, SourceType};
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
52pub struct UdmEvent {
53 pub event_id: String,
58
59 #[serde(alias = "timestamp")]
62 pub captured_at: DateTime<Utc>,
63
64 #[serde(default)]
66 pub event_type: EventType,
67
68 #[serde(default)]
71 pub source_id: String,
72
73 pub source_type: SourceType,
75
76 #[serde(alias = "schema_version")]
78 pub udm_version: String,
79
80 pub sdk_version: String,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
88 pub identity: Option<IdentityDomain>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub location: Option<LocationDomain>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub motion: Option<MotionDomain>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub power: Option<PowerDomain>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub operational: Option<OperationalDomain>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub navigation: Option<NavigationDomain>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub perception: Option<PerceptionDomain>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub safety: Option<SafetyDomain>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub actuators: Option<ActuatorsDomain>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub communication: Option<CommunicationDomain>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub compute: Option<ComputeDomain>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub ai: Option<AiDomain>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub maintenance: Option<MaintenanceDomain>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub context: Option<ContextDomain>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub payload: Option<PayloadDomain>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub manipulation: Option<ManipulationDomain>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub hri: Option<HriDomain>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub coordination: Option<CoordinationDomain>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub simulation: Option<SimulationDomain>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub thermal: Option<ThermalDomain>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub audio: Option<AudioDomain>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub environment_interaction: Option<EnvironmentInteractionDomain>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub compliance: Option<ComplianceDomain>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
183 pub provenance: Option<Provenance>,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub extensions: Option<serde_json::Value>,
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192pub struct Provenance {
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub signature: Option<String>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub key_id: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub algorithm: Option<String>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub signed_fields: Option<Vec<String>>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub signed_at: Option<DateTime<Utc>>,
212}
213
214impl UdmEvent {
215 pub fn new(source_type: SourceType) -> Self {
219 Self {
220 event_id: Uuid::now_v7().to_string(),
221 captured_at: Utc::now(),
222 event_type: EventType::default(),
223 source_type,
224 udm_version: crate::UDM_VERSION.to_string(),
225 sdk_version: crate::SDK_VERSION.to_string(),
226 ..Default::default()
227 }
228 }
229
230 pub fn with_event_type(mut self, event_type: EventType) -> Self {
232 self.event_type = event_type;
233 self
234 }
235
236 pub fn with_identity(mut self, identity: IdentityDomain) -> Self {
238 self.identity = Some(identity);
239 self
240 }
241
242 pub fn with_location(mut self, location: LocationDomain) -> Self {
244 self.location = Some(location);
245 self
246 }
247
248 pub fn with_motion(mut self, motion: MotionDomain) -> Self {
250 self.motion = Some(motion);
251 self
252 }
253
254 pub fn with_power(mut self, power: PowerDomain) -> Self {
256 self.power = Some(power);
257 self
258 }
259
260 pub fn with_operational(mut self, operational: OperationalDomain) -> Self {
262 self.operational = Some(operational);
263 self
264 }
265
266 pub fn with_navigation(mut self, navigation: NavigationDomain) -> Self {
268 self.navigation = Some(navigation);
269 self
270 }
271
272 pub fn with_perception(mut self, perception: PerceptionDomain) -> Self {
274 self.perception = Some(perception);
275 self
276 }
277
278 pub fn with_safety(mut self, safety: SafetyDomain) -> Self {
280 self.safety = Some(safety);
281 self
282 }
283
284 pub fn with_actuators(mut self, actuators: ActuatorsDomain) -> Self {
286 self.actuators = Some(actuators);
287 self
288 }
289
290 pub fn with_communication(mut self, communication: CommunicationDomain) -> Self {
292 self.communication = Some(communication);
293 self
294 }
295
296 pub fn with_compute(mut self, compute: ComputeDomain) -> Self {
298 self.compute = Some(compute);
299 self
300 }
301
302 pub fn with_ai(mut self, ai: AiDomain) -> Self {
304 self.ai = Some(ai);
305 self
306 }
307
308 pub fn with_maintenance(mut self, maintenance: MaintenanceDomain) -> Self {
310 self.maintenance = Some(maintenance);
311 self
312 }
313
314 pub fn with_context(mut self, context: ContextDomain) -> Self {
316 self.context = Some(context);
317 self
318 }
319
320 pub fn with_payload(mut self, payload: PayloadDomain) -> Self {
322 self.payload = Some(payload);
323 self
324 }
325
326 pub fn with_manipulation(mut self, manipulation: ManipulationDomain) -> Self {
328 self.manipulation = Some(manipulation);
329 self
330 }
331
332 pub fn with_hri(mut self, hri: HriDomain) -> Self {
334 self.hri = Some(hri);
335 self
336 }
337
338 pub fn with_coordination(mut self, coordination: CoordinationDomain) -> Self {
340 self.coordination = Some(coordination);
341 self
342 }
343
344 pub fn with_simulation(mut self, simulation: SimulationDomain) -> Self {
346 self.simulation = Some(simulation);
347 self
348 }
349
350 pub fn with_thermal(mut self, thermal: ThermalDomain) -> Self {
352 self.thermal = Some(thermal);
353 self
354 }
355
356 pub fn with_audio(mut self, audio: AudioDomain) -> Self {
358 self.audio = Some(audio);
359 self
360 }
361
362 pub fn with_environment_interaction(
364 mut self,
365 environment_interaction: EnvironmentInteractionDomain,
366 ) -> Self {
367 self.environment_interaction = Some(environment_interaction);
368 self
369 }
370
371 pub fn with_compliance(mut self, compliance: ComplianceDomain) -> Self {
373 self.compliance = Some(compliance);
374 self
375 }
376
377 pub fn with_extensions(mut self, extensions: serde_json::Value) -> Self {
379 self.extensions = Some(extensions);
380 self
381 }
382
383 pub fn inject_license_metadata(&mut self, metadata: &crate::core::license::LicenseMetadata) {
387 let license_data = serde_json::json!({
388 "_license_status": metadata.status,
389 "_license_tenant_id": metadata.tenant_id,
390 "_license_plan": metadata.plan,
391 "_quarantine": metadata.quarantine,
392 "_grace_period_start": metadata.grace_started_at,
393 "_grace_period_expires": metadata.grace_expires_at,
394 "_last_online_check": metadata.last_online_check,
395 });
396
397 if let Some(ref mut extensions) = self.extensions {
398 if let Some(obj) = extensions.as_object_mut() {
399 for (key, value) in license_data.as_object().unwrap() {
400 obj.insert(key.clone(), value.clone());
401 }
402 }
403 } else {
404 self.extensions = Some(license_data);
405 }
406 }
407
408 pub fn with_provenance(mut self, provenance: Provenance) -> Self {
410 self.provenance = Some(provenance);
411 self
412 }
413
414 pub fn has_domain_data(&self) -> bool {
416 self.identity.is_some()
417 || self.location.is_some()
418 || self.motion.is_some()
419 || self.power.is_some()
420 || self.operational.is_some()
421 || self.navigation.is_some()
422 || self.perception.is_some()
423 || self.safety.is_some()
424 || self.actuators.is_some()
425 || self.communication.is_some()
426 || self.compute.is_some()
427 || self.ai.is_some()
428 || self.maintenance.is_some()
429 || self.context.is_some()
430 || self.payload.is_some()
431 || self.manipulation.is_some()
432 || self.hri.is_some()
433 || self.coordination.is_some()
434 || self.simulation.is_some()
435 || self.thermal.is_some()
436 || self.audio.is_some()
437 || self.environment_interaction.is_some()
438 || self.compliance.is_some()
439 }
440
441 pub fn estimated_size(&self) -> usize {
443 serde_json::to_string(self).map(|s| s.len()).unwrap_or(0)
444 }
445
446 pub fn to_json(&self) -> Result<String, serde_json::Error> {
448 serde_json::to_string(self)
449 }
450
451 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
453 serde_json::to_string_pretty(self)
454 }
455
456 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
458 serde_json::from_str(json)
459 }
460}
461
462impl Provenance {
463 pub fn new(algorithm: impl Into<String>) -> Self {
465 Self {
466 algorithm: Some(algorithm.into()),
467 signed_at: Some(Utc::now()),
468 ..Default::default()
469 }
470 }
471
472 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
474 self.signature = Some(signature.into());
475 self
476 }
477
478 pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
480 self.key_id = Some(key_id.into());
481 self
482 }
483
484 pub fn with_signed_fields(mut self, fields: Vec<String>) -> Self {
486 self.signed_fields = Some(fields);
487 self
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn test_event_creation() {
497 let event = UdmEvent::new(SourceType::Amr);
498
499 assert!(!event.event_id.is_empty());
500 assert_eq!(event.source_type, SourceType::Amr);
501 assert_eq!(event.event_type, EventType::TelemetryPeriodic);
502 assert!(!event.has_domain_data());
503 }
504
505 #[test]
506 fn test_event_with_domains() {
507 use crate::models::domains::location::LocalCoordinates;
508
509 let event = UdmEvent::new(SourceType::Amr)
510 .with_identity(IdentityDomain {
511 source_id: Some("robot-001".to_string()),
512 ..Default::default()
513 })
514 .with_location(LocationDomain {
515 local: Some(LocalCoordinates {
516 x_m: Some(10.0),
517 y_m: Some(20.0),
518 ..Default::default()
519 }),
520 ..Default::default()
521 });
522
523 assert!(event.has_domain_data());
524 assert_eq!(
525 event.identity.as_ref().unwrap().source_id,
526 Some("robot-001".to_string())
527 );
528 }
529
530 #[test]
531 fn test_event_serialization() {
532 let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::StateTransition);
533
534 let json = event.to_json().unwrap();
535 assert!(json.contains("\"event_id\""));
536 assert!(json.contains("\"state_transition\""));
537
538 let deserialized = UdmEvent::from_json(&json).unwrap();
539 assert_eq!(deserialized.event_type, EventType::StateTransition);
540 }
541
542 #[test]
543 fn test_provenance() {
544 let provenance = Provenance::new("hmac-sha256")
545 .with_signature("abc123")
546 .with_key_id("key-001");
547
548 assert_eq!(provenance.algorithm, Some("hmac-sha256".to_string()));
549 assert_eq!(provenance.signature, Some("abc123".to_string()));
550 }
551
552 #[test]
553 fn test_event_with_extensions() {
554 let extensions = serde_json::json!({
555 "custom_field": "custom_value",
556 "nested": { "key": 42 }
557 });
558
559 let event = UdmEvent::new(SourceType::Custom).with_extensions(extensions.clone());
560
561 assert_eq!(event.extensions, Some(extensions));
562 }
563
564 #[test]
569 fn test_event_default() {
570 let event = UdmEvent::default();
571 assert!(event.event_id.is_empty());
572 assert!(event.identity.is_none());
573 assert!(event.location.is_none());
574 }
575
576 #[test]
577 fn test_event_with_event_type() {
578 let event = UdmEvent::new(SourceType::Amr).with_event_type(EventType::SafetyViolation);
579 assert_eq!(event.event_type, EventType::SafetyViolation);
580 }
581
582 #[test]
583 fn test_event_with_all_domains() {
584 let event = UdmEvent::new(SourceType::Amr)
585 .with_identity(IdentityDomain::default())
586 .with_location(LocationDomain::default())
587 .with_motion(MotionDomain::default())
588 .with_power(PowerDomain::default())
589 .with_operational(OperationalDomain::default())
590 .with_navigation(NavigationDomain::default())
591 .with_perception(PerceptionDomain::default())
592 .with_safety(SafetyDomain::default())
593 .with_actuators(ActuatorsDomain::default())
594 .with_communication(CommunicationDomain::default())
595 .with_compute(ComputeDomain::default())
596 .with_ai(AiDomain::default())
597 .with_maintenance(MaintenanceDomain::default())
598 .with_context(ContextDomain::default())
599 .with_payload(PayloadDomain::default())
600 .with_manipulation(ManipulationDomain::default())
601 .with_hri(HriDomain::default())
602 .with_coordination(CoordinationDomain::default())
603 .with_simulation(SimulationDomain::default())
604 .with_thermal(ThermalDomain::default())
605 .with_audio(AudioDomain::default())
606 .with_environment_interaction(EnvironmentInteractionDomain::default())
607 .with_compliance(ComplianceDomain::default());
608
609 assert!(event.has_domain_data());
610 assert!(event.identity.is_some());
611 assert!(event.location.is_some());
612 assert!(event.motion.is_some());
613 assert!(event.power.is_some());
614 assert!(event.operational.is_some());
615 assert!(event.navigation.is_some());
616 assert!(event.perception.is_some());
617 assert!(event.safety.is_some());
618 assert!(event.actuators.is_some());
619 assert!(event.communication.is_some());
620 assert!(event.compute.is_some());
621 assert!(event.ai.is_some());
622 assert!(event.maintenance.is_some());
623 assert!(event.context.is_some());
624 assert!(event.payload.is_some());
625 assert!(event.manipulation.is_some());
626 assert!(event.hri.is_some());
627 assert!(event.coordination.is_some());
628 assert!(event.simulation.is_some());
629 assert!(event.thermal.is_some());
630 assert!(event.audio.is_some());
631 assert!(event.environment_interaction.is_some());
632 assert!(event.compliance.is_some());
633 }
634
635 #[test]
636 fn test_event_with_provenance() {
637 let prov = Provenance::new("hmac-sha256")
638 .with_signature("sig123")
639 .with_key_id("key-001");
640
641 let event = UdmEvent::new(SourceType::Amr).with_provenance(prov);
642
643 assert!(event.provenance.is_some());
644 assert_eq!(
645 event.provenance.as_ref().unwrap().signature,
646 Some("sig123".to_string())
647 );
648 }
649
650 #[test]
651 fn test_event_has_domain_data_single() {
652 let event_identity =
654 UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain::default());
655 assert!(event_identity.has_domain_data());
656
657 let event_motion = UdmEvent::new(SourceType::Amr).with_motion(MotionDomain::default());
658 assert!(event_motion.has_domain_data());
659
660 let event_ai = UdmEvent::new(SourceType::Amr).with_ai(AiDomain::default());
661 assert!(event_ai.has_domain_data());
662 }
663
664 #[test]
665 fn test_event_estimated_size() {
666 let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
667 source_id: Some("robot-001".to_string()),
668 ..Default::default()
669 });
670
671 let size = event.estimated_size();
672 assert!(size > 0);
673 }
674
675 #[test]
676 fn test_event_to_json() {
677 let event = UdmEvent::new(SourceType::Amr);
678 let json = event.to_json().unwrap();
679
680 assert!(json.contains("event_id"));
681 assert!(json.contains("captured_at"));
682 assert!(json.contains("source_type"));
683 }
684
685 #[test]
686 fn test_event_to_json_pretty() {
687 let event = UdmEvent::new(SourceType::Amr);
688 let json = event.to_json_pretty().unwrap();
689
690 assert!(json.contains("\n"));
692 }
693
694 #[test]
695 fn test_event_from_json() {
696 let event = UdmEvent::new(SourceType::Drone);
697 let json = event.to_json().unwrap();
698
699 let deserialized = UdmEvent::from_json(&json).unwrap();
700 assert_eq!(deserialized.source_type, SourceType::Drone);
701 }
702
703 #[test]
704 fn test_event_from_json_invalid() {
705 let result = UdmEvent::from_json("invalid json");
706 assert!(result.is_err());
707 }
708
709 #[test]
714 fn test_provenance_default() {
715 let prov = Provenance::default();
716 assert!(prov.signature.is_none());
717 assert!(prov.key_id.is_none());
718 assert!(prov.algorithm.is_none());
719 assert!(prov.signed_fields.is_none());
720 assert!(prov.signed_at.is_none());
721 }
722
723 #[test]
724 fn test_provenance_new() {
725 let prov = Provenance::new("hmac-sha256");
726 assert_eq!(prov.algorithm, Some("hmac-sha256".to_string()));
727 assert!(prov.signed_at.is_some());
728 }
729
730 #[test]
731 fn test_provenance_with_signature() {
732 let prov = Provenance::new("hmac-sha256").with_signature("abc123def456");
733 assert_eq!(prov.signature, Some("abc123def456".to_string()));
734 }
735
736 #[test]
737 fn test_provenance_with_key_id() {
738 let prov = Provenance::new("hmac-sha256").with_key_id("my-key-001");
739 assert_eq!(prov.key_id, Some("my-key-001".to_string()));
740 }
741
742 #[test]
743 fn test_provenance_with_signed_fields() {
744 let fields = vec![
745 "event_id".to_string(),
746 "timestamp".to_string(),
747 "identity".to_string(),
748 ];
749 let prov = Provenance::new("hmac-sha256").with_signed_fields(fields.clone());
750 assert_eq!(prov.signed_fields, Some(fields));
751 }
752
753 #[test]
754 fn test_provenance_chained_builders() {
755 let prov = Provenance::new("hmac-sha256")
756 .with_signature("sig")
757 .with_key_id("key")
758 .with_signed_fields(vec!["event_id".to_string()]);
759
760 assert!(prov.algorithm.is_some());
761 assert!(prov.signature.is_some());
762 assert!(prov.key_id.is_some());
763 assert!(prov.signed_fields.is_some());
764 }
765
766 #[test]
771 fn test_event_full_serialization_roundtrip() {
772 let event = UdmEvent::new(SourceType::Amr)
773 .with_event_type(EventType::TelemetryPeriodic)
774 .with_identity(IdentityDomain {
775 source_id: Some("robot-001".to_string()),
776 ..Default::default()
777 })
778 .with_location(LocationDomain::default())
779 .with_provenance(Provenance::new("hmac-sha256").with_signature("test"));
780
781 let json = event.to_json().unwrap();
782 let deserialized = UdmEvent::from_json(&json).unwrap();
783
784 assert_eq!(deserialized.source_type, SourceType::Amr);
785 assert!(deserialized.identity.is_some());
786 assert!(deserialized.location.is_some());
787 assert!(deserialized.provenance.is_some());
788 }
789
790 #[test]
791 fn test_event_clone() {
792 let event = UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
793 source_id: Some("robot-001".to_string()),
794 ..Default::default()
795 });
796
797 let cloned = event.clone();
798 assert_eq!(cloned.event_id, event.event_id);
799 assert_eq!(cloned.source_type, event.source_type);
800 }
801}