Skip to main content

peat_schema/validation/
core.rs

1//! Core validators for basic Peat Protocol types
2//!
3//! Validates: Capability, NodeConfig, NodeState, CellConfig, CellState
4
5use super::{ValidationError, ValidationResult};
6use crate::capability::v1::Capability;
7use crate::cell::v1::{CellConfig, CellState};
8use crate::node::v1::{NodeConfig, NodeState};
9
10/// Validate a capability message
11pub fn validate_capability(cap: &Capability) -> ValidationResult<()> {
12    // Check confidence is in valid range
13    if cap.confidence < 0.0 || cap.confidence > 1.0 {
14        return Err(ValidationError::InvalidConfidence(cap.confidence));
15    }
16
17    // Check required fields
18    if cap.id.is_empty() {
19        return Err(ValidationError::MissingField("id".to_string()));
20    }
21
22    if cap.name.is_empty() {
23        return Err(ValidationError::MissingField("name".to_string()));
24    }
25
26    Ok(())
27}
28
29/// Validate a node configuration
30pub fn validate_node_config(config: &NodeConfig) -> ValidationResult<()> {
31    // Check required fields
32    if config.id.is_empty() {
33        return Err(ValidationError::MissingField("id".to_string()));
34    }
35
36    if config.platform_type.is_empty() {
37        return Err(ValidationError::MissingField("platform_type".to_string()));
38    }
39
40    // Validate all capabilities
41    for cap in &config.capabilities {
42        validate_capability(cap)?;
43    }
44
45    // Check communication range is positive
46    if config.comm_range_m <= 0.0 {
47        return Err(ValidationError::InvalidValue(
48            "comm_range_m must be positive".to_string(),
49        ));
50    }
51
52    // Check max speed is positive
53    if config.max_speed_mps <= 0.0 {
54        return Err(ValidationError::InvalidValue(
55            "max_speed_mps must be positive".to_string(),
56        ));
57    }
58
59    Ok(())
60}
61
62/// Validate a node state
63pub fn validate_node_state(state: &NodeState) -> ValidationResult<()> {
64    // Check position has valid coordinates
65    if let Some(pos) = &state.position {
66        if pos.latitude < -90.0 || pos.latitude > 90.0 {
67            return Err(ValidationError::InvalidValue(
68                "latitude must be between -90 and 90".to_string(),
69            ));
70        }
71        if pos.longitude < -180.0 || pos.longitude > 180.0 {
72            return Err(ValidationError::InvalidValue(
73                "longitude must be between -180 and 180".to_string(),
74            ));
75        }
76    }
77
78    Ok(())
79}
80
81/// Validate a cell configuration
82pub fn validate_cell_config(config: &CellConfig) -> ValidationResult<()> {
83    // Check required fields
84    if config.id.is_empty() {
85        return Err(ValidationError::MissingField("id".to_string()));
86    }
87
88    // Check max_size > min_size
89    if config.max_size < config.min_size {
90        return Err(ValidationError::ConstraintViolation(
91            "max_size must be >= min_size".to_string(),
92        ));
93    }
94
95    // Check minimum size is at least 2
96    if config.min_size < 2 {
97        return Err(ValidationError::ConstraintViolation(
98            "min_size must be at least 2".to_string(),
99        ));
100    }
101
102    Ok(())
103}
104
105/// Validate a cell state
106pub fn validate_cell_state(state: &CellState) -> ValidationResult<()> {
107    // Validate config
108    if let Some(config) = &state.config {
109        validate_cell_config(config)?;
110
111        // Check member count constraints
112        let member_count = state.members.len();
113        if member_count > config.max_size as usize {
114            return Err(ValidationError::ConstraintViolation(format!(
115                "member count ({}) exceeds max_size ({})",
116                member_count, config.max_size
117            )));
118        }
119    }
120
121    // Validate all capabilities
122    for cap in &state.capabilities {
123        validate_capability(cap)?;
124    }
125
126    // If leader_id is set, it must be in members list
127    if let Some(leader_id) = &state.leader_id {
128        if !state.members.contains(leader_id) {
129            return Err(ValidationError::ConstraintViolation(
130                "leader_id must be in members list".to_string(),
131            ));
132        }
133    }
134
135    Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::capability::v1::CapabilityType;
142
143    #[test]
144    fn test_validate_capability_success() {
145        let cap = Capability {
146            id: "cap-1".to_string(),
147            name: "Camera".to_string(),
148            capability_type: CapabilityType::Sensor as i32,
149            confidence: 0.9,
150            metadata_json: String::new(),
151            registered_at: None,
152        };
153
154        assert!(validate_capability(&cap).is_ok());
155    }
156
157    #[test]
158    fn test_validate_capability_invalid_confidence() {
159        let cap = Capability {
160            id: "cap-1".to_string(),
161            name: "Camera".to_string(),
162            capability_type: CapabilityType::Sensor as i32,
163            confidence: 1.5, // Invalid
164            metadata_json: String::new(),
165            registered_at: None,
166        };
167
168        assert!(validate_capability(&cap).is_err());
169    }
170
171    #[test]
172    fn test_validate_capability_missing_id() {
173        let cap = Capability {
174            id: String::new(), // Missing
175            name: "Camera".to_string(),
176            capability_type: CapabilityType::Sensor as i32,
177            confidence: 0.9,
178            metadata_json: String::new(),
179            registered_at: None,
180        };
181
182        assert!(validate_capability(&cap).is_err());
183    }
184
185    #[test]
186    fn test_validate_cell_config_invalid_sizes() {
187        let config = CellConfig {
188            id: "cell-1".to_string(),
189            max_size: 2,
190            min_size: 5, // Invalid: min > max
191            created_at: None,
192        };
193
194        assert!(validate_cell_config(&config).is_err());
195    }
196}