use crate::composition::rules::{CompositionContext, CompositionResult, CompositionRule};
use crate::models::capability::{Capability, CapabilityType};
use crate::models::CapabilityExt;
use crate::Result;
use async_trait::async_trait;
use serde_json::{json, Value};
pub struct TeamSpeedConstraintRule {
min_platforms: usize,
}
impl TeamSpeedConstraintRule {
pub fn new(min_platforms: usize) -> Self {
Self { min_platforms }
}
}
impl Default for TeamSpeedConstraintRule {
fn default() -> Self {
Self::new(2)
}
}
#[async_trait]
impl CompositionRule for TeamSpeedConstraintRule {
fn name(&self) -> &str {
"team_speed_constraint"
}
fn description(&self) -> &str {
"Determines team movement speed based on slowest member constraint"
}
fn applies_to(&self, capabilities: &[Capability]) -> bool {
let mobility_count = capabilities
.iter()
.filter(|c| {
c.get_capability_type() == CapabilityType::Mobility
&& serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("max_speed").cloned())
.is_some()
})
.count();
mobility_count >= self.min_platforms
}
async fn compose(
&self,
capabilities: &[Capability],
_context: &CompositionContext,
) -> Result<CompositionResult> {
let mobility_caps: Vec<&Capability> = capabilities
.iter()
.filter(|c| {
c.get_capability_type() == CapabilityType::Mobility
&& serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("max_speed").cloned())
.is_some()
})
.collect();
if mobility_caps.len() < self.min_platforms {
return Ok(CompositionResult::new(vec![], 0.0));
}
let speeds: Vec<f64> = mobility_caps
.iter()
.filter_map(|c| {
serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
})
.collect();
let team_speed = speeds.iter().cloned().fold(f64::INFINITY, f64::min);
let slowest = mobility_caps
.iter()
.min_by(|a, b| {
let speed_a = serde_json::from_str::<Value>(&a.metadata_json)
.ok()
.and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
.unwrap_or(0.0);
let speed_b = serde_json::from_str::<Value>(&b.metadata_json)
.ok()
.and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
.unwrap_or(0.0);
speed_a.partial_cmp(&speed_b).unwrap()
})
.unwrap();
let team_confidence = slowest.confidence;
let mut composed = Capability::new(
format!("constraint_team_speed_{}", uuid::Uuid::new_v4()),
"Team Speed".to_string(),
CapabilityType::Emergent,
team_confidence,
);
composed.metadata_json = serde_json::to_string(&json!({
"composition_type": "constraint",
"pattern": "team_speed",
"team_speed": team_speed,
"platform_count": mobility_caps.len(),
"limiting_platform": slowest.id,
"individual_speeds": speeds,
"description": "Team movement speed constrained by slowest member"
}))
.unwrap_or_default();
let contributor_ids: Vec<String> = mobility_caps.iter().map(|c| c.id.clone()).collect();
Ok(CompositionResult::new(vec![composed], team_confidence)
.with_contributors(contributor_ids))
}
}
pub struct CommunicationRangeConstraintRule {
min_nodes: usize,
has_mesh: bool,
}
impl CommunicationRangeConstraintRule {
pub fn new(min_nodes: usize, has_mesh: bool) -> Self {
Self {
min_nodes,
has_mesh,
}
}
}
impl Default for CommunicationRangeConstraintRule {
fn default() -> Self {
Self::new(2, false) }
}
#[async_trait]
impl CompositionRule for CommunicationRangeConstraintRule {
fn name(&self) -> &str {
"communication_range_constraint"
}
fn description(&self) -> &str {
"Determines effective communication range based on mesh capability"
}
fn applies_to(&self, capabilities: &[Capability]) -> bool {
let comm_count = capabilities
.iter()
.filter(|c| {
c.get_capability_type() == CapabilityType::Communication
&& serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("range").cloned())
.is_some()
})
.count();
comm_count >= self.min_nodes
}
async fn compose(
&self,
capabilities: &[Capability],
_context: &CompositionContext,
) -> Result<CompositionResult> {
let comm_caps: Vec<&Capability> = capabilities
.iter()
.filter(|c| {
c.get_capability_type() == CapabilityType::Communication
&& serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("range").cloned())
.is_some()
})
.collect();
if comm_caps.len() < self.min_nodes {
return Ok(CompositionResult::new(vec![], 0.0));
}
let ranges: Vec<f64> = comm_caps
.iter()
.filter_map(|c| {
serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("range").and_then(|r| r.as_f64()))
})
.collect();
let (effective_range, limiting_factor) = if self.has_mesh {
let max_range = ranges.iter().cloned().fold(0.0, f64::max);
(max_range, "mesh_enabled".to_string())
} else {
let min_range = ranges.iter().cloned().fold(f64::INFINITY, f64::min);
(min_range, "direct_comms_only".to_string())
};
let limiting_node = if self.has_mesh {
comm_caps
.iter()
.max_by(|a, b| {
let range_a = serde_json::from_str::<Value>(&a.metadata_json)
.ok()
.and_then(|v| v.get("range").and_then(|r| r.as_f64()))
.unwrap_or(0.0);
let range_b = serde_json::from_str::<Value>(&b.metadata_json)
.ok()
.and_then(|v| v.get("range").and_then(|r| r.as_f64()))
.unwrap_or(0.0);
range_a.partial_cmp(&range_b).unwrap()
})
.unwrap()
} else {
comm_caps
.iter()
.min_by(|a, b| {
let range_a = serde_json::from_str::<Value>(&a.metadata_json)
.ok()
.and_then(|v| v.get("range").and_then(|r| r.as_f64()))
.unwrap_or(0.0);
let range_b = serde_json::from_str::<Value>(&b.metadata_json)
.ok()
.and_then(|v| v.get("range").and_then(|r| r.as_f64()))
.unwrap_or(0.0);
range_a.partial_cmp(&range_b).unwrap()
})
.unwrap()
};
let avg_confidence: f32 =
comm_caps.iter().map(|c| c.confidence).sum::<f32>() / comm_caps.len() as f32;
let mut composed = Capability::new(
format!("constraint_comm_range_{}", uuid::Uuid::new_v4()),
"Team Communication Range".to_string(),
CapabilityType::Emergent,
avg_confidence,
);
composed.metadata_json = serde_json::to_string(&json!({
"composition_type": "constraint",
"pattern": "communication_range",
"effective_range": effective_range,
"mesh_enabled": self.has_mesh,
"limiting_factor": limiting_factor,
"limiting_node": limiting_node.id,
"node_count": comm_caps.len(),
"individual_ranges": ranges,
"description": if self.has_mesh {
"Extended range through mesh networking"
} else {
"Range constrained by weakest link"
}
}))
.unwrap_or_default();
let contributor_ids: Vec<String> = comm_caps.iter().map(|c| c.id.clone()).collect();
Ok(CompositionResult::new(vec![composed], avg_confidence)
.with_contributors(contributor_ids))
}
}
pub struct MissionDurationConstraintRule {
min_platforms: usize,
}
impl MissionDurationConstraintRule {
pub fn new(min_platforms: usize) -> Self {
Self { min_platforms }
}
}
impl Default for MissionDurationConstraintRule {
fn default() -> Self {
Self::new(2)
}
}
#[async_trait]
impl CompositionRule for MissionDurationConstraintRule {
fn name(&self) -> &str {
"mission_duration_constraint"
}
fn description(&self) -> &str {
"Determines maximum mission duration based on shortest platform endurance"
}
fn applies_to(&self, capabilities: &[Capability]) -> bool {
let platforms_with_endurance = capabilities
.iter()
.filter(|c| {
serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("endurance_minutes").cloned())
.is_some()
})
.count();
platforms_with_endurance >= self.min_platforms
}
async fn compose(
&self,
capabilities: &[Capability],
_context: &CompositionContext,
) -> Result<CompositionResult> {
let platforms: Vec<&Capability> = capabilities
.iter()
.filter(|c| {
serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("endurance_minutes").cloned())
.is_some()
})
.collect();
if platforms.len() < self.min_platforms {
return Ok(CompositionResult::new(vec![], 0.0));
}
let endurances: Vec<f64> = platforms
.iter()
.filter_map(|c| {
serde_json::from_str::<Value>(&c.metadata_json)
.ok()
.and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
})
.collect();
let mission_duration = endurances.iter().cloned().fold(f64::INFINITY, f64::min);
let limiting_platform = platforms
.iter()
.min_by(|a, b| {
let endurance_a = serde_json::from_str::<Value>(&a.metadata_json)
.ok()
.and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
.unwrap_or(0.0);
let endurance_b = serde_json::from_str::<Value>(&b.metadata_json)
.ok()
.and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
.unwrap_or(0.0);
endurance_a.partial_cmp(&endurance_b).unwrap()
})
.unwrap();
let mission_confidence = limiting_platform.confidence;
let mut composed = Capability::new(
format!("constraint_mission_duration_{}", uuid::Uuid::new_v4()),
"Team Mission Duration".to_string(),
CapabilityType::Emergent,
mission_confidence,
);
composed.metadata_json = serde_json::to_string(&json!({
"composition_type": "constraint",
"pattern": "mission_duration",
"mission_duration_minutes": mission_duration,
"platform_count": platforms.len(),
"limiting_platform": limiting_platform.id,
"individual_endurances": endurances,
"description": "Mission duration constrained by shortest endurance"
}))
.unwrap_or_default();
let contributor_ids: Vec<String> = platforms.iter().map(|c| c.id.clone()).collect();
Ok(CompositionResult::new(vec![composed], mission_confidence)
.with_contributors(contributor_ids))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn test_team_speed_constraint() {
let rule = TeamSpeedConstraintRule::default();
let mut fast_platform = Capability::new(
"fast1".to_string(),
"Fast Drone".to_string(),
CapabilityType::Mobility,
0.9,
);
fast_platform.metadata_json =
serde_json::to_string(&json!({"max_speed": 20.0})).unwrap_or_default();
let mut slow_platform = Capability::new(
"slow1".to_string(),
"Slow Ground Vehicle".to_string(),
CapabilityType::Mobility,
0.85,
);
slow_platform.metadata_json =
serde_json::to_string(&json!({"max_speed": 5.0})).unwrap_or_default();
let caps = vec![fast_platform, slow_platform];
let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
assert!(rule.applies_to(&caps));
let result = rule.compose(&caps, &context).await.unwrap();
assert!(result.has_compositions());
let composed = &result.composed_capabilities[0];
assert_eq!(composed.name, "Team Speed");
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
assert_eq!(metadata["team_speed"].as_f64().unwrap(), 5.0);
assert_eq!(metadata["limiting_platform"].as_str().unwrap(), "slow1");
assert_eq!(composed.confidence, 0.85);
}
#[tokio::test]
async fn test_communication_range_without_mesh() {
let rule = CommunicationRangeConstraintRule::new(2, false);
let mut long_range = Capability::new(
"comm1".to_string(),
"Long Range Radio".to_string(),
CapabilityType::Communication,
0.9,
);
long_range.metadata_json =
serde_json::to_string(&json!({"range": 1000.0})).unwrap_or_default();
let mut short_range = Capability::new(
"comm2".to_string(),
"Short Range Radio".to_string(),
CapabilityType::Communication,
0.85,
);
short_range.metadata_json =
serde_json::to_string(&json!({"range": 200.0})).unwrap_or_default();
let caps = vec![long_range, short_range];
let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
let result = rule.compose(&caps, &context).await.unwrap();
assert!(result.has_compositions());
let composed = &result.composed_capabilities[0];
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
assert_eq!(metadata["effective_range"].as_f64().unwrap(), 200.0);
assert!(!metadata["mesh_enabled"].as_bool().unwrap());
assert_eq!(metadata["limiting_node"].as_str().unwrap(), "comm2");
}
#[tokio::test]
async fn test_communication_range_with_mesh() {
let rule = CommunicationRangeConstraintRule::new(2, true);
let mut long_range = Capability::new(
"comm1".to_string(),
"Long Range Radio".to_string(),
CapabilityType::Communication,
0.9,
);
long_range.metadata_json =
serde_json::to_string(&json!({"range": 1000.0})).unwrap_or_default();
let mut short_range = Capability::new(
"comm2".to_string(),
"Short Range Radio".to_string(),
CapabilityType::Communication,
0.85,
);
short_range.metadata_json =
serde_json::to_string(&json!({"range": 200.0})).unwrap_or_default();
let caps = vec![long_range, short_range];
let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
let result = rule.compose(&caps, &context).await.unwrap();
assert!(result.has_compositions());
let composed = &result.composed_capabilities[0];
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
assert_eq!(metadata["effective_range"].as_f64().unwrap(), 1000.0);
assert!(metadata["mesh_enabled"].as_bool().unwrap());
assert_eq!(metadata["limiting_node"].as_str().unwrap(), "comm1");
}
#[tokio::test]
async fn test_mission_duration_constraint() {
let rule = MissionDurationConstraintRule::default();
let mut long_endurance = Capability::new(
"platform1".to_string(),
"Fixed-Wing UAV".to_string(),
CapabilityType::Mobility,
0.95,
);
long_endurance.metadata_json =
serde_json::to_string(&json!({"endurance_minutes": 120.0})).unwrap_or_default();
let mut short_endurance = Capability::new(
"platform2".to_string(),
"Quadcopter".to_string(),
CapabilityType::Mobility,
0.8,
);
short_endurance.metadata_json =
serde_json::to_string(&json!({"endurance_minutes": 25.0})).unwrap_or_default();
let caps = vec![long_endurance, short_endurance];
let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
assert!(rule.applies_to(&caps));
let result = rule.compose(&caps, &context).await.unwrap();
assert!(result.has_compositions());
let composed = &result.composed_capabilities[0];
assert_eq!(composed.name, "Team Mission Duration");
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
assert_eq!(metadata["mission_duration_minutes"].as_f64().unwrap(), 25.0);
assert_eq!(metadata["limiting_platform"].as_str().unwrap(), "platform2");
assert_eq!(composed.confidence, 0.8);
}
#[tokio::test]
async fn test_constraint_rules_dont_apply_insufficient_platforms() {
let speed_rule = TeamSpeedConstraintRule::default();
let comm_rule = CommunicationRangeConstraintRule::default();
let duration_rule = MissionDurationConstraintRule::default();
let mut single_platform = Capability::new(
"platform1".to_string(),
"Solo Platform".to_string(),
CapabilityType::Mobility,
0.9,
);
single_platform.metadata_json =
serde_json::to_string(&json!({"max_speed": 10.0, "endurance_minutes": 60.0}))
.unwrap_or_default();
let caps = vec![single_platform];
assert!(!speed_rule.applies_to(&caps));
assert!(!comm_rule.applies_to(&caps));
assert!(!duration_rule.applies_to(&caps));
}
#[tokio::test]
async fn test_team_speed_with_three_platforms() {
let rule = TeamSpeedConstraintRule::default();
let platforms: Vec<Capability> = vec![
("fast", 25.0, 0.95),
("medium", 15.0, 0.9),
("slow", 8.0, 0.85),
]
.into_iter()
.map(|(name, speed, confidence)| {
let mut cap = Capability::new(
format!("platform_{}", name),
name.to_string(),
CapabilityType::Mobility,
confidence,
);
cap.metadata_json =
serde_json::to_string(&json!({"max_speed": speed})).unwrap_or_default();
cap
})
.collect();
let context = CompositionContext::new(vec![
"node1".to_string(),
"node2".to_string(),
"node3".to_string(),
]);
let result = rule.compose(&platforms, &context).await.unwrap();
assert!(result.has_compositions());
let composed = &result.composed_capabilities[0];
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
assert_eq!(metadata["team_speed"].as_f64().unwrap(), 8.0);
assert_eq!(metadata["platform_count"].as_u64().unwrap(), 3);
}
#[tokio::test]
async fn test_constraint_metadata_accuracy() {
let rule = TeamSpeedConstraintRule::default();
let mut platform1 = Capability::new(
"p1".to_string(),
"Platform 1".to_string(),
CapabilityType::Mobility,
0.9,
);
platform1.metadata_json =
serde_json::to_string(&json!({"max_speed": 12.5})).unwrap_or_default();
let mut platform2 = Capability::new(
"p2".to_string(),
"Platform 2".to_string(),
CapabilityType::Mobility,
0.85,
);
platform2.metadata_json =
serde_json::to_string(&json!({"max_speed": 18.3})).unwrap_or_default();
let caps = vec![platform1, platform2];
let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
let result = rule.compose(&caps, &context).await.unwrap();
let composed = &result.composed_capabilities[0];
let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
let individual_speeds = metadata["individual_speeds"].as_array().unwrap();
assert_eq!(individual_speeds.len(), 2);
assert!(individual_speeds.contains(&json!(12.5)));
assert!(individual_speeds.contains(&json!(18.3)));
}
}