peat_schema/validation/
track.rs1use super::{ValidationError, ValidationResult};
6use crate::track::v1::{Track, TrackPosition, TrackUpdate, UpdateType};
7
8pub fn validate_track_update(update: &TrackUpdate) -> ValidationResult<()> {
15 if update.update_type == UpdateType::Unspecified as i32 {
17 return Err(ValidationError::InvalidValue(
18 "update_type must be specified".to_string(),
19 ));
20 }
21
22 let track = update
24 .track
25 .as_ref()
26 .ok_or_else(|| ValidationError::MissingField("track".to_string()))?;
27
28 validate_track(track)?;
29
30 if update.timestamp.is_none() {
32 return Err(ValidationError::MissingField("timestamp".to_string()));
33 }
34
35 if update.update_type == UpdateType::Merge as i32 && update.previous_track_id.is_empty() {
37 return Err(ValidationError::MissingField(
38 "previous_track_id (required for MERGE updates)".to_string(),
39 ));
40 }
41
42 Ok(())
43}
44
45pub fn validate_track(track: &Track) -> ValidationResult<()> {
53 if track.track_id.is_empty() {
55 return Err(ValidationError::MissingField("track_id".to_string()));
56 }
57
58 if track.confidence < 0.0 || track.confidence > 1.0 {
60 return Err(ValidationError::InvalidConfidence(track.confidence));
61 }
62
63 let position = track
65 .position
66 .as_ref()
67 .ok_or_else(|| ValidationError::MissingField("position".to_string()))?;
68
69 validate_track_position(position)?;
70
71 let source = track
73 .source
74 .as_ref()
75 .ok_or_else(|| ValidationError::MissingField("source".to_string()))?;
76
77 if source.platform_id.is_empty() {
78 return Err(ValidationError::MissingField(
79 "source.platform_id".to_string(),
80 ));
81 }
82
83 Ok(())
84}
85
86fn validate_track_position(pos: &TrackPosition) -> ValidationResult<()> {
88 if pos.latitude < -90.0 || pos.latitude > 90.0 {
90 return Err(ValidationError::InvalidValue(format!(
91 "latitude {} must be between -90 and 90",
92 pos.latitude
93 )));
94 }
95
96 if pos.longitude < -180.0 || pos.longitude > 180.0 {
98 return Err(ValidationError::InvalidValue(format!(
99 "longitude {} must be between -180 and 180",
100 pos.longitude
101 )));
102 }
103
104 if pos.cep_m < 0.0 {
106 return Err(ValidationError::InvalidValue(format!(
107 "cep_m {} must be non-negative",
108 pos.cep_m
109 )));
110 }
111
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::common::v1::Timestamp;
119 use crate::track::v1::{SourceType, TrackSource, TrackState};
120
121 fn valid_track() -> Track {
122 Track {
123 track_id: "TRK-001".to_string(),
124 classification: "person".to_string(),
125 confidence: 0.92,
126 position: Some(TrackPosition {
127 latitude: 38.8977,
128 longitude: -77.0365,
129 altitude: 10.0,
130 cep_m: 5.0,
131 vertical_error_m: 2.0,
132 }),
133 velocity: None,
134 state: TrackState::Confirmed as i32,
135 source: Some(TrackSource {
136 platform_id: "Alpha-3".to_string(),
137 sensor_id: "camera-1".to_string(),
138 model_version: "1.2.0".to_string(),
139 source_type: SourceType::AiModel as i32,
140 }),
141 attributes_json: r#"{"color": "red"}"#.to_string(),
142 first_seen: None,
143 last_seen: None,
144 observation_count: 5,
145 }
146 }
147
148 fn valid_track_update() -> TrackUpdate {
149 TrackUpdate {
150 update_type: UpdateType::New as i32,
151 track: Some(valid_track()),
152 previous_track_id: String::new(),
153 timestamp: Some(Timestamp {
154 seconds: 1702000000,
155 nanos: 0,
156 }),
157 }
158 }
159
160 #[test]
161 fn test_valid_track_update() {
162 let update = valid_track_update();
163 assert!(validate_track_update(&update).is_ok());
164 }
165
166 #[test]
167 fn test_missing_track() {
168 let mut update = valid_track_update();
169 update.track = None;
170 let err = validate_track_update(&update).unwrap_err();
171 assert!(matches!(err, ValidationError::MissingField(f) if f == "track"));
172 }
173
174 #[test]
175 fn test_missing_timestamp() {
176 let mut update = valid_track_update();
177 update.timestamp = None;
178 let err = validate_track_update(&update).unwrap_err();
179 assert!(matches!(err, ValidationError::MissingField(f) if f == "timestamp"));
180 }
181
182 #[test]
183 fn test_unspecified_update_type() {
184 let mut update = valid_track_update();
185 update.update_type = UpdateType::Unspecified as i32;
186 let err = validate_track_update(&update).unwrap_err();
187 assert!(matches!(err, ValidationError::InvalidValue(_)));
188 }
189
190 #[test]
191 fn test_merge_without_previous_track_id() {
192 let mut update = valid_track_update();
193 update.update_type = UpdateType::Merge as i32;
194 update.previous_track_id = String::new();
195 let err = validate_track_update(&update).unwrap_err();
196 assert!(matches!(err, ValidationError::MissingField(_)));
197 }
198
199 #[test]
200 fn test_invalid_latitude() {
201 let mut update = valid_track_update();
202 if let Some(ref mut track) = update.track {
203 if let Some(ref mut pos) = track.position {
204 pos.latitude = 100.0; }
206 }
207 let err = validate_track_update(&update).unwrap_err();
208 assert!(matches!(err, ValidationError::InvalidValue(_)));
209 }
210
211 #[test]
212 fn test_invalid_confidence() {
213 let mut update = valid_track_update();
214 if let Some(ref mut track) = update.track {
215 track.confidence = -0.5; }
217 let err = validate_track_update(&update).unwrap_err();
218 assert!(matches!(err, ValidationError::InvalidConfidence(_)));
219 }
220}