use crate::models::{Capability, CapabilityExt};
use std::time::{SystemTime, UNIX_EPOCH};
pub use peat_schema::zone::v1::{ZoneConfig, ZoneState, ZoneStats};
pub trait ZoneConfigExt {
fn new(id: String, max_cells: u32) -> Self;
fn with_min_cells(self, min_cells: u32) -> Self;
}
impl ZoneConfigExt for ZoneConfig {
fn new(id: String, max_cells: u32) -> Self {
Self {
id,
max_cells,
min_cells: 2, created_at: Some(peat_schema::common::v1::Timestamp {
seconds: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
nanos: 0,
}),
}
}
fn with_min_cells(mut self, min_cells: u32) -> Self {
self.min_cells = min_cells;
self
}
}
pub trait ZoneStateExt {
fn new(config: ZoneConfig) -> Self;
fn add_cell(&mut self, cell_id: String) -> bool;
fn remove_cell(&mut self, cell_id: &str) -> bool;
fn set_coordinator(&mut self, coordinator_id: String, timestamp: u64) -> bool;
fn remove_coordinator(&mut self, timestamp: u64) -> bool;
fn add_capability(&mut self, capability: Capability);
fn is_valid(&self) -> bool;
fn is_full(&self) -> bool;
fn cell_count(&self) -> usize;
fn contains_cell(&self, cell_id: &str) -> bool;
fn merge(&mut self, other: &ZoneState);
fn update_timestamp(&mut self);
fn stats(&self) -> ZoneStats;
}
impl ZoneStateExt for ZoneState {
fn new(config: ZoneConfig) -> Self {
Self {
config: Some(config),
coordinator_id: None,
cells: Vec::new(),
aggregated_capabilities: Vec::new(),
timestamp: Some(peat_schema::common::v1::Timestamp {
seconds: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
nanos: 0,
}),
}
}
fn add_cell(&mut self, cell_id: String) -> bool {
if self.is_full() {
return false;
}
if self.cells.iter().any(|id| id == &cell_id) {
return false;
}
self.cells.push(cell_id);
self.update_timestamp();
true
}
fn remove_cell(&mut self, cell_id: &str) -> bool {
if let Some(pos) = self.cells.iter().position(|id| id == cell_id) {
self.cells.remove(pos);
self.update_timestamp();
true
} else {
false
}
}
fn set_coordinator(&mut self, coordinator_id: String, timestamp: u64) -> bool {
let current_ts = self.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
if timestamp < current_ts {
return false;
}
self.coordinator_id = Some(coordinator_id);
self.timestamp = Some(peat_schema::common::v1::Timestamp {
seconds: timestamp,
nanos: 0,
});
true
}
fn remove_coordinator(&mut self, timestamp: u64) -> bool {
let current_ts = self.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
if timestamp < current_ts {
return false;
}
self.coordinator_id = None;
self.timestamp = Some(peat_schema::common::v1::Timestamp {
seconds: timestamp,
nanos: 0,
});
true
}
fn add_capability(&mut self, capability: Capability) {
if !self
.aggregated_capabilities
.iter()
.any(|c| c.get_capability_type() == capability.get_capability_type())
{
self.aggregated_capabilities.push(capability);
self.update_timestamp();
}
}
fn is_valid(&self) -> bool {
if let Some(ref config) = self.config {
self.cells.len() >= config.min_cells as usize
} else {
false
}
}
fn is_full(&self) -> bool {
if let Some(ref config) = self.config {
self.cells.len() >= config.max_cells as usize
} else {
false
}
}
fn cell_count(&self) -> usize {
self.cells.len()
}
fn contains_cell(&self, cell_id: &str) -> bool {
self.cells.iter().any(|id| id == cell_id)
}
fn merge(&mut self, other: &ZoneState) {
let self_id = self.config.as_ref().map(|c| &c.id);
let other_id = other.config.as_ref().map(|c| &c.id);
assert_eq!(self_id, other_id, "Cannot merge zones with different IDs");
let self_ts = self.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
let other_ts = other.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
if other_ts > self_ts {
self.coordinator_id = other.coordinator_id.clone();
self.timestamp = other.timestamp;
}
for cell_id in &other.cells {
if !self.cells.iter().any(|id| id == cell_id) {
self.cells.push(cell_id.clone());
}
}
for capability in &other.aggregated_capabilities {
if !self
.aggregated_capabilities
.iter()
.any(|c| c.get_capability_type() == capability.get_capability_type())
{
self.aggregated_capabilities.push(capability.clone());
}
}
}
fn update_timestamp(&mut self) {
self.timestamp = Some(peat_schema::common::v1::Timestamp {
seconds: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
nanos: 0,
});
}
fn stats(&self) -> ZoneStats {
let zone_id = self
.config
.as_ref()
.map(|c| c.id.clone())
.unwrap_or_default();
ZoneStats {
zone_id,
cell_count: self.cells.len() as u32,
total_nodes: 0, unique_capability_count: self.aggregated_capabilities.len() as u32,
is_valid: self.is_valid(),
is_full: self.is_full(),
calculated_at: Some(peat_schema::common::v1::Timestamp {
seconds: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
nanos: 0,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::CapabilityType;
#[test]
fn test_zone_config_creation() {
let config = ZoneConfig::new("zone_test".to_string(), 10);
assert_eq!(config.id, "zone_test");
assert_eq!(config.max_cells, 10);
assert_eq!(config.min_cells, 2);
}
#[test]
fn test_zone_config_custom_min() {
let config = ZoneConfig::new("zone_test".to_string(), 10).with_min_cells(3);
assert_eq!(config.min_cells, 3);
}
#[test]
fn test_zone_state_creation() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let zone = ZoneState::new(config);
assert_eq!(zone.config.as_ref().unwrap().id, "zone_1");
assert_eq!(zone.cells.len(), 0);
assert!(zone.coordinator_id.is_none());
assert!(!zone.is_valid()); }
#[test]
fn test_add_cell() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
assert!(zone.add_cell("cell_1".to_string()));
assert!(zone.add_cell("cell_2".to_string()));
assert_eq!(zone.cell_count(), 2);
assert!(!zone.add_cell("cell_1".to_string()));
assert_eq!(zone.cell_count(), 2);
}
#[test]
fn test_remove_cell() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
zone.add_cell("cell_1".to_string());
zone.add_cell("cell_2".to_string());
assert!(zone.remove_cell("cell_1"));
assert_eq!(zone.cell_count(), 1);
assert!(!zone.remove_cell("cell_3"));
}
#[test]
fn test_zone_capacity() {
let config = ZoneConfig::new("zone_1".to_string(), 3);
let mut zone = ZoneState::new(config);
assert!(zone.add_cell("cell_1".to_string()));
assert!(zone.add_cell("cell_2".to_string()));
assert!(zone.add_cell("cell_3".to_string()));
assert!(zone.is_full());
assert!(!zone.add_cell("cell_4".to_string())); }
#[test]
fn test_zone_validity() {
let config = ZoneConfig::new("zone_1".to_string(), 5).with_min_cells(2);
let mut zone = ZoneState::new(config);
assert!(!zone.is_valid());
zone.add_cell("cell_1".to_string());
assert!(!zone.is_valid());
zone.add_cell("cell_2".to_string());
assert!(zone.is_valid()); }
#[test]
fn test_set_coordinator() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
let initial_ts = zone.timestamp.as_ref().unwrap().seconds;
let ts1 = initial_ts + 100;
assert!(zone.set_coordinator("node_1".to_string(), ts1));
assert_eq!(zone.coordinator_id, Some("node_1".to_string()));
assert!(!zone.set_coordinator("node_2".to_string(), initial_ts + 50));
assert_eq!(zone.coordinator_id, Some("node_1".to_string()));
assert!(zone.set_coordinator("node_2".to_string(), ts1 + 100));
assert_eq!(zone.coordinator_id, Some("node_2".to_string()));
}
#[test]
fn test_remove_coordinator() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
let initial_ts = zone.timestamp.as_ref().unwrap().seconds;
zone.set_coordinator("node_1".to_string(), initial_ts + 100);
assert!(!zone.remove_coordinator(initial_ts + 50));
assert_eq!(zone.coordinator_id, Some("node_1".to_string()));
assert!(zone.remove_coordinator(initial_ts + 200));
assert_eq!(zone.coordinator_id, None);
}
#[test]
fn test_add_capability() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
let cap1 = Capability::new(
"cap_1".to_string(),
"Sensor Capability".to_string(),
CapabilityType::Sensor,
0.9,
);
zone.add_capability(cap1.clone());
assert_eq!(zone.aggregated_capabilities.len(), 1);
zone.add_capability(cap1.clone());
assert_eq!(zone.aggregated_capabilities.len(), 1);
let cap2 = Capability::new(
"cap_2".to_string(),
"Payload Capability".to_string(),
CapabilityType::Payload,
0.8,
);
zone.add_capability(cap2);
assert_eq!(zone.aggregated_capabilities.len(), 2);
}
#[test]
fn test_contains_cell() {
let config = ZoneConfig::new("zone_1".to_string(), 5);
let mut zone = ZoneState::new(config);
zone.add_cell("cell_1".to_string());
assert!(zone.contains_cell("cell_1"));
assert!(!zone.contains_cell("cell_2"));
}
#[test]
fn test_merge_zones() {
let config1 = ZoneConfig::new("zone_1".to_string(), 10);
let mut zone1 = ZoneState::new(config1);
let initial_ts = zone1.timestamp.as_ref().unwrap().seconds;
zone1.add_cell("cell_1".to_string());
zone1.add_cell("cell_2".to_string());
zone1.set_coordinator("node_1".to_string(), initial_ts + 100);
let config2 = ZoneConfig::new("zone_1".to_string(), 10);
let mut zone2 = ZoneState::new(config2);
zone2.add_cell("cell_2".to_string()); zone2.add_cell("cell_3".to_string()); zone2.set_coordinator("node_2".to_string(), initial_ts + 200);
zone1.merge(&zone2);
assert_eq!(zone1.cell_count(), 3);
assert!(zone1.contains_cell("cell_1"));
assert!(zone1.contains_cell("cell_2"));
assert!(zone1.contains_cell("cell_3"));
assert_eq!(zone1.coordinator_id, Some("node_2".to_string()));
}
#[test]
fn test_merge_capabilities() {
let config1 = ZoneConfig::new("zone_1".to_string(), 10);
let mut zone1 = ZoneState::new(config1);
let cap1 = Capability::new(
"cap_1".to_string(),
"Sensor".to_string(),
CapabilityType::Sensor,
0.9,
);
zone1.add_capability(cap1);
let config2 = ZoneConfig::new("zone_1".to_string(), 10);
let mut zone2 = ZoneState::new(config2);
let cap2 = Capability::new(
"cap_2".to_string(),
"Payload".to_string(),
CapabilityType::Payload,
0.8,
);
zone2.add_capability(cap2);
zone1.merge(&zone2);
assert_eq!(zone1.aggregated_capabilities.len(), 2);
}
#[test]
#[should_panic(expected = "Cannot merge zones with different IDs")]
fn test_merge_different_zones_panics() {
let config1 = ZoneConfig::new("zone_1".to_string(), 10);
let mut zone1 = ZoneState::new(config1);
let config2 = ZoneConfig::new("zone_2".to_string(), 10);
let zone2 = ZoneState::new(config2);
zone1.merge(&zone2); }
#[test]
fn test_zone_stats() {
let config = ZoneConfig::new("zone_test".to_string(), 5).with_min_cells(2);
let mut zone = ZoneState::new(config);
let initial_ts = zone.timestamp.as_ref().unwrap().seconds;
zone.add_cell("cell_1".to_string());
zone.add_cell("cell_2".to_string());
zone.set_coordinator("node_1".to_string(), initial_ts + 100);
let stats = zone.stats();
assert_eq!(stats.zone_id, "zone_test");
assert_eq!(stats.cell_count, 2);
assert!(stats.is_valid);
assert!(!stats.is_full);
}
}