Skip to main content

peat_protocol/composition/
engine.rs

1//! Composition engine and rule registry
2//!
3//! This module provides the composition engine that applies multiple
4//! composition rules to sets of capabilities.
5
6use crate::composition::rules::{CompositionContext, CompositionResult, CompositionRule};
7use crate::models::capability::Capability;
8use crate::Result;
9use std::sync::Arc;
10use tracing::{debug, instrument};
11
12/// Composition engine that manages and applies composition rules
13pub struct CompositionEngine {
14    /// Registered composition rules
15    rules: Vec<Arc<dyn CompositionRule>>,
16}
17
18impl CompositionEngine {
19    /// Create a new composition engine
20    pub fn new() -> Self {
21        Self { rules: Vec::new() }
22    }
23
24    /// Register a composition rule
25    ///
26    /// Rules are applied in registration order during composition.
27    pub fn register_rule(&mut self, rule: Arc<dyn CompositionRule>) {
28        debug!("Registering composition rule: {}", rule.name());
29        self.rules.push(rule);
30    }
31
32    /// Get the number of registered rules
33    pub fn rule_count(&self) -> usize {
34        self.rules.len()
35    }
36
37    /// Compose capabilities using all applicable rules
38    ///
39    /// Applies each registered rule to the input capabilities and
40    /// aggregates all detected compositions.
41    #[instrument(skip(self, capabilities, context))]
42    pub async fn compose(
43        &self,
44        capabilities: &[Capability],
45        context: &CompositionContext,
46    ) -> Result<Vec<CompositionResult>> {
47        debug!(
48            "Composing {} capabilities with {} rules",
49            capabilities.len(),
50            self.rules.len()
51        );
52
53        let mut all_results = Vec::new();
54
55        for rule in &self.rules {
56            if rule.applies_to(capabilities) {
57                debug!("Applying rule: {}", rule.name());
58                let result = rule.compose(capabilities, context).await?;
59
60                if result.has_compositions() {
61                    debug!(
62                        "Rule {} produced {} compositions",
63                        rule.name(),
64                        result.composed_capabilities.len()
65                    );
66                    all_results.push(result);
67                }
68            } else {
69                debug!("Rule {} does not apply", rule.name());
70            }
71        }
72
73        debug!("Total compositions: {}", all_results.len());
74        Ok(all_results)
75    }
76
77    /// Compose capabilities and flatten into a single list
78    ///
79    /// This is a convenience method that applies all rules and returns
80    /// all composed capabilities in a flat list.
81    pub async fn compose_all(
82        &self,
83        capabilities: &[Capability],
84        context: &CompositionContext,
85    ) -> Result<Vec<Capability>> {
86        let results = self.compose(capabilities, context).await?;
87
88        let composed_capabilities: Vec<Capability> = results
89            .into_iter()
90            .flat_map(|r| r.composed_capabilities)
91            .collect();
92
93        Ok(composed_capabilities)
94    }
95}
96
97impl Default for CompositionEngine {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::composition::rules::CompositionRule;
107    use crate::models::capability::{CapabilityExt, CapabilityType};
108    use async_trait::async_trait;
109
110    // Mock rule for testing
111    struct MockRule {
112        name: String,
113        should_apply: bool,
114        result_count: usize,
115    }
116
117    impl MockRule {
118        fn new(name: &str, should_apply: bool, result_count: usize) -> Self {
119            Self {
120                name: name.to_string(),
121                should_apply,
122                result_count,
123            }
124        }
125    }
126
127    #[async_trait]
128    impl CompositionRule for MockRule {
129        fn name(&self) -> &str {
130            &self.name
131        }
132
133        fn description(&self) -> &str {
134            "Mock rule for testing"
135        }
136
137        fn applies_to(&self, _capabilities: &[Capability]) -> bool {
138            self.should_apply
139        }
140
141        async fn compose(
142            &self,
143            _capabilities: &[Capability],
144            _context: &CompositionContext,
145        ) -> Result<CompositionResult> {
146            let composed: Vec<Capability> = (0..self.result_count)
147                .map(|i| {
148                    Capability::new(
149                        format!("composed_{}", i),
150                        format!("Composed {}", i),
151                        CapabilityType::Emergent,
152                        0.8,
153                    )
154                })
155                .collect();
156
157            Ok(CompositionResult::new(composed, 0.8))
158        }
159    }
160
161    #[test]
162    fn test_engine_creation() {
163        let engine = CompositionEngine::new();
164        assert_eq!(engine.rule_count(), 0);
165    }
166
167    #[test]
168    fn test_register_rule() {
169        let mut engine = CompositionEngine::new();
170        let rule = Arc::new(MockRule::new("test_rule", true, 1));
171
172        engine.register_rule(rule);
173        assert_eq!(engine.rule_count(), 1);
174    }
175
176    #[test]
177    fn test_register_multiple_rules() {
178        let mut engine = CompositionEngine::new();
179
180        engine.register_rule(Arc::new(MockRule::new("rule1", true, 1)));
181        engine.register_rule(Arc::new(MockRule::new("rule2", true, 1)));
182        engine.register_rule(Arc::new(MockRule::new("rule3", true, 1)));
183
184        assert_eq!(engine.rule_count(), 3);
185    }
186
187    #[tokio::test]
188    async fn test_compose_with_applicable_rule() {
189        let mut engine = CompositionEngine::new();
190        engine.register_rule(Arc::new(MockRule::new("applicable", true, 2)));
191
192        let capabilities = vec![Capability::new(
193            "sensor".to_string(),
194            "Sensor".to_string(),
195            CapabilityType::Sensor,
196            0.9,
197        )];
198
199        let context = CompositionContext::new(vec!["node1".to_string()]);
200        let results = engine.compose(&capabilities, &context).await.unwrap();
201
202        assert_eq!(results.len(), 1);
203        assert_eq!(results[0].composed_capabilities.len(), 2);
204    }
205
206    #[tokio::test]
207    async fn test_compose_with_non_applicable_rule() {
208        let mut engine = CompositionEngine::new();
209        engine.register_rule(Arc::new(MockRule::new("not_applicable", false, 2)));
210
211        let capabilities = vec![Capability::new(
212            "sensor".to_string(),
213            "Sensor".to_string(),
214            CapabilityType::Sensor,
215            0.9,
216        )];
217
218        let context = CompositionContext::new(vec!["node1".to_string()]);
219        let results = engine.compose(&capabilities, &context).await.unwrap();
220
221        // Rule doesn't apply, so no results
222        assert_eq!(results.len(), 0);
223    }
224
225    #[tokio::test]
226    async fn test_compose_with_multiple_rules() {
227        let mut engine = CompositionEngine::new();
228        engine.register_rule(Arc::new(MockRule::new("rule1", true, 1)));
229        engine.register_rule(Arc::new(MockRule::new("rule2", true, 2)));
230        engine.register_rule(Arc::new(MockRule::new("rule3", false, 1))); // Won't apply
231
232        let capabilities = vec![Capability::new(
233            "sensor".to_string(),
234            "Sensor".to_string(),
235            CapabilityType::Sensor,
236            0.9,
237        )];
238
239        let context = CompositionContext::new(vec!["node1".to_string()]);
240        let results = engine.compose(&capabilities, &context).await.unwrap();
241
242        // Only 2 rules apply
243        assert_eq!(results.len(), 2);
244        assert_eq!(results[0].composed_capabilities.len(), 1);
245        assert_eq!(results[1].composed_capabilities.len(), 2);
246    }
247
248    #[tokio::test]
249    async fn test_compose_all() {
250        let mut engine = CompositionEngine::new();
251        engine.register_rule(Arc::new(MockRule::new("rule1", true, 2)));
252        engine.register_rule(Arc::new(MockRule::new("rule2", true, 3)));
253
254        let capabilities = vec![Capability::new(
255            "sensor".to_string(),
256            "Sensor".to_string(),
257            CapabilityType::Sensor,
258            0.9,
259        )];
260
261        let context = CompositionContext::new(vec!["node1".to_string()]);
262        let composed = engine.compose_all(&capabilities, &context).await.unwrap();
263
264        // Should flatten all results: 2 + 3 = 5
265        assert_eq!(composed.len(), 5);
266    }
267
268    #[tokio::test]
269    async fn test_compose_empty_capabilities() {
270        let mut engine = CompositionEngine::new();
271        engine.register_rule(Arc::new(MockRule::new("rule1", true, 1)));
272
273        let capabilities = vec![];
274        let context = CompositionContext::new(vec!["node1".to_string()]);
275        let results = engine.compose(&capabilities, &context).await.unwrap();
276
277        // Rule still applies even with empty input
278        assert_eq!(results.len(), 1);
279    }
280}