use crate::models::capability::Capability;
use crate::Result;
use async_trait::async_trait;
use crate::models::{AuthorityLevel, NodeConfig};
#[derive(Debug, Clone)]
pub struct CompositionContext {
pub node_ids: Vec<String>,
pub cell_id: Option<String>,
pub timestamp: std::time::SystemTime,
pub node_configs: Vec<NodeConfig>,
}
impl CompositionContext {
pub fn new(node_ids: Vec<String>) -> Self {
Self {
node_ids,
cell_id: None,
timestamp: std::time::SystemTime::now(),
node_configs: Vec::new(),
}
}
pub fn with_cell_id(mut self, cell_id: String) -> Self {
self.cell_id = Some(cell_id);
self
}
pub fn with_node_configs(mut self, configs: Vec<NodeConfig>) -> Self {
self.node_configs = configs;
self
}
pub fn max_authority(&self) -> Option<AuthorityLevel> {
use crate::models::HumanMachinePairExt;
self.node_configs
.iter()
.filter_map(|config| config.operator_binding.as_ref())
.filter_map(|binding| binding.max_authority())
.max()
}
pub fn has_commander(&self) -> bool {
self.max_authority() == Some(AuthorityLevel::Commander)
}
pub fn authorization_bonus(&self) -> i32 {
use crate::models::AuthorityLevelExt;
match self.max_authority() {
Some(auth) => (auth.to_score() * 5.0).round() as i32,
None => 0,
}
}
}
impl Default for CompositionContext {
fn default() -> Self {
Self {
node_ids: Vec::new(),
cell_id: None,
timestamp: std::time::SystemTime::now(),
node_configs: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct CompositionResult {
pub composed_capabilities: Vec<Capability>,
pub confidence: f32,
pub contributing_capabilities: Vec<String>, }
impl CompositionResult {
pub fn new(composed_capabilities: Vec<Capability>, confidence: f32) -> Self {
Self {
composed_capabilities,
confidence,
contributing_capabilities: Vec::new(),
}
}
pub fn with_contributors(mut self, capability_ids: Vec<String>) -> Self {
self.contributing_capabilities = capability_ids;
self
}
pub fn has_compositions(&self) -> bool {
!self.composed_capabilities.is_empty()
}
}
#[async_trait]
pub trait CompositionRule: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn applies_to(&self, capabilities: &[Capability]) -> bool;
async fn compose(
&self,
capabilities: &[Capability],
context: &CompositionContext,
) -> Result<CompositionResult>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::capability::{CapabilityExt, CapabilityType};
#[test]
fn test_composition_context_creation() {
let node_ids = vec!["node1".to_string(), "node2".to_string()];
let ctx = CompositionContext::new(node_ids.clone());
assert_eq!(ctx.node_ids, node_ids);
assert_eq!(ctx.cell_id, None);
}
#[test]
fn test_composition_context_with_cell() {
let ctx = CompositionContext::new(vec!["node1".to_string()])
.with_cell_id("cell_alpha".to_string());
assert_eq!(ctx.cell_id, Some("cell_alpha".to_string()));
}
#[test]
fn test_composition_result_creation() {
let capability = Capability::new(
"test".to_string(),
"Test Capability".to_string(),
CapabilityType::Emergent,
0.9,
);
let result = CompositionResult::new(vec![capability], 0.8);
assert_eq!(result.composed_capabilities.len(), 1);
assert_eq!(result.confidence, 0.8);
assert!(result.has_compositions());
}
#[test]
fn test_composition_result_with_contributors() {
let capability = Capability::new(
"emergent".to_string(),
"Emergent".to_string(),
CapabilityType::Emergent,
0.9,
);
let result = CompositionResult::new(vec![capability], 0.8)
.with_contributors(vec!["cap1".to_string(), "cap2".to_string()]);
assert_eq!(result.contributing_capabilities.len(), 2);
assert!(result
.contributing_capabilities
.contains(&"cap1".to_string()));
}
#[test]
fn test_empty_composition_result() {
let result = CompositionResult::new(vec![], 0.0);
assert!(!result.has_compositions());
assert_eq!(result.composed_capabilities.len(), 0);
}
#[test]
fn test_composition_context_with_node_configs() {
use crate::models::{NodeConfig, NodeConfigExt};
let config = NodeConfig::new("UAV".to_string());
let ctx =
CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
assert_eq!(ctx.node_configs.len(), 1);
}
#[test]
fn test_composition_context_max_authority_none() {
let ctx = CompositionContext::new(vec!["node1".to_string()]);
assert!(ctx.max_authority().is_none());
assert!(!ctx.has_commander());
assert_eq!(ctx.authorization_bonus(), 0);
}
#[test]
fn test_composition_context_max_authority_with_commander() {
use crate::models::{
HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
OperatorExt, OperatorRank,
};
let operator = Operator::new(
"op1".to_string(),
"CPT Smith".to_string(),
OperatorRank::O3,
AuthorityLevel::Commander,
"11A".to_string(),
);
let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
let config = NodeConfig::with_operator("Command Post".to_string(), binding);
let ctx =
CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Commander));
assert!(ctx.has_commander());
assert_eq!(ctx.authorization_bonus(), 4); }
#[test]
fn test_composition_context_authorization_bonus_levels() {
use crate::models::{
HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
OperatorExt, OperatorRank,
};
let operator = Operator::new(
"op1".to_string(),
"SGT Jones".to_string(),
OperatorRank::E5,
AuthorityLevel::Supervisor,
"11B".to_string(),
);
let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
let config = NodeConfig::with_operator("Control Station".to_string(), binding);
let ctx =
CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Supervisor));
assert!(!ctx.has_commander());
let bonus = ctx.authorization_bonus();
assert!((2..=3).contains(&bonus));
}
#[test]
fn test_composition_context_default() {
let ctx = CompositionContext::default();
assert!(ctx.node_ids.is_empty());
assert!(ctx.cell_id.is_none());
assert!(ctx.node_configs.is_empty());
}
}