Skip to main content

peat_schema/validation/
track.rs

1//! Track validators
2//!
3//! Validates Track and TrackUpdate messages for Peat Protocol.
4
5use super::{ValidationError, ValidationResult};
6use crate::track::v1::{Track, TrackPosition, TrackUpdate, UpdateType};
7
8/// Validate a TrackUpdate message
9///
10/// Validates:
11/// - update_type is specified (not unspecified)
12/// - track is present and valid
13/// - timestamp is present
14pub fn validate_track_update(update: &TrackUpdate) -> ValidationResult<()> {
15    // Check update_type is specified
16    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    // Track is required
23    let track = update
24        .track
25        .as_ref()
26        .ok_or_else(|| ValidationError::MissingField("track".to_string()))?;
27
28    validate_track(track)?;
29
30    // Timestamp is required
31    if update.timestamp.is_none() {
32        return Err(ValidationError::MissingField("timestamp".to_string()));
33    }
34
35    // For merge operations, previous_track_id is required
36    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
45/// Validate a Track message
46///
47/// Validates:
48/// - track_id is present
49/// - confidence is in valid range (0.0 - 1.0)
50/// - position is present and valid
51/// - source is present
52pub fn validate_track(track: &Track) -> ValidationResult<()> {
53    // Check required fields
54    if track.track_id.is_empty() {
55        return Err(ValidationError::MissingField("track_id".to_string()));
56    }
57
58    // Confidence must be in valid range
59    if track.confidence < 0.0 || track.confidence > 1.0 {
60        return Err(ValidationError::InvalidConfidence(track.confidence));
61    }
62
63    // Position is required
64    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    // Source is required
72    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
86/// Validate a TrackPosition
87fn validate_track_position(pos: &TrackPosition) -> ValidationResult<()> {
88    // Latitude must be -90 to 90
89    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    // Longitude must be -180 to 180
97    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    // CEP must be non-negative
105    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; // Invalid
205            }
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; // Invalid
216        }
217        let err = validate_track_update(&update).unwrap_err();
218        assert!(matches!(err, ValidationError::InvalidConfidence(_)));
219    }
220}