Skip to main content

peat_protocol/models/
capability.rs

1//! Capability data structures
2//!
3//! This module re-exports the protobuf-generated Capability types from cap-schema
4//! and provides extension traits for ergonomic usage.
5
6// Re-export protobuf types from cap-schema
7pub use peat_schema::cap::capability::v1::{Capability, CapabilityType};
8
9/// Extension trait for Capability with ergonomic constructors and accessors
10pub trait CapabilityExt {
11    /// Create a new capability
12    ///
13    /// # Arguments
14    /// * `id` - Unique capability identifier
15    /// * `name` - Human-readable name
16    /// * `capability_type` - Type of capability (sensor, compute, etc.)
17    /// * `confidence` - Confidence score (0.0 - 1.0), will be clamped
18    fn new(id: String, name: String, capability_type: CapabilityType, confidence: f32) -> Self;
19
20    /// Get the capability type as the enum (not i32)
21    ///
22    /// Returns the CapabilityType enum value, converting from the protobuf i32 field.
23    /// Returns Unspecified if the field contains an invalid value.
24    fn get_capability_type(&self) -> CapabilityType;
25
26    /// Set the capability type from the enum
27    ///
28    /// # Arguments
29    /// * `capability_type` - The CapabilityType enum value to set
30    fn set_capability_type(&mut self, capability_type: CapabilityType);
31
32    /// Check if this capability is valid (confidence > threshold)
33    ///
34    /// # Arguments
35    /// * `threshold` - Minimum confidence threshold (0.0 - 1.0)
36    fn is_valid(&self, threshold: f32) -> bool;
37}
38
39impl CapabilityExt for Capability {
40    fn new(id: String, name: String, capability_type: CapabilityType, confidence: f32) -> Self {
41        Self {
42            id,
43            name,
44            capability_type: capability_type as i32,
45            confidence: confidence.clamp(0.0, 1.0),
46            metadata_json: String::new(),
47            registered_at: None,
48        }
49    }
50
51    fn get_capability_type(&self) -> CapabilityType {
52        CapabilityType::try_from(self.capability_type).unwrap_or(CapabilityType::Unspecified)
53    }
54
55    fn set_capability_type(&mut self, capability_type: CapabilityType) {
56        self.capability_type = capability_type as i32;
57    }
58
59    fn is_valid(&self, threshold: f32) -> bool {
60        self.confidence >= threshold
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_capability_new() {
70        let cap = Capability::new(
71            "sensor-1".to_string(),
72            "Camera".to_string(),
73            CapabilityType::Sensor,
74            0.85,
75        );
76
77        assert_eq!(cap.id, "sensor-1");
78        assert_eq!(cap.name, "Camera");
79        assert_eq!(cap.get_capability_type(), CapabilityType::Sensor);
80        assert_eq!(cap.confidence, 0.85);
81    }
82
83    #[test]
84    fn test_capability_confidence_clamping() {
85        // Test upper bound
86        let cap_high = Capability::new(
87            "test".to_string(),
88            "Test".to_string(),
89            CapabilityType::Compute,
90            1.5,
91        );
92        assert_eq!(cap_high.confidence, 1.0);
93
94        // Test lower bound
95        let cap_low = Capability::new(
96            "test".to_string(),
97            "Test".to_string(),
98            CapabilityType::Compute,
99            -0.5,
100        );
101        assert_eq!(cap_low.confidence, 0.0);
102
103        // Test normal value
104        let cap_normal = Capability::new(
105            "test".to_string(),
106            "Test".to_string(),
107            CapabilityType::Compute,
108            0.75,
109        );
110        assert_eq!(cap_normal.confidence, 0.75);
111    }
112
113    #[test]
114    fn test_get_capability_type() {
115        let cap = Capability::new(
116            "test".to_string(),
117            "Test".to_string(),
118            CapabilityType::Communication,
119            0.8,
120        );
121
122        assert_eq!(cap.get_capability_type(), CapabilityType::Communication);
123        assert_eq!(cap.capability_type, CapabilityType::Communication as i32);
124    }
125
126    #[test]
127    fn test_set_capability_type() {
128        let mut cap = Capability::new(
129            "test".to_string(),
130            "Test".to_string(),
131            CapabilityType::Sensor,
132            0.8,
133        );
134
135        assert_eq!(cap.get_capability_type(), CapabilityType::Sensor);
136
137        cap.set_capability_type(CapabilityType::Payload);
138        assert_eq!(cap.get_capability_type(), CapabilityType::Payload);
139        assert_eq!(cap.capability_type, CapabilityType::Payload as i32);
140    }
141
142    #[test]
143    fn test_capability_type_roundtrip() {
144        // Test all capability types
145        let types = vec![
146            CapabilityType::Unspecified,
147            CapabilityType::Sensor,
148            CapabilityType::Compute,
149            CapabilityType::Communication,
150            CapabilityType::Mobility,
151            CapabilityType::Payload,
152            CapabilityType::Emergent,
153        ];
154
155        for cap_type in types {
156            let cap = Capability::new("test".to_string(), "Test".to_string(), cap_type, 0.8);
157            assert_eq!(cap.get_capability_type(), cap_type);
158        }
159    }
160
161    #[test]
162    fn test_is_valid() {
163        let cap = Capability::new(
164            "test".to_string(),
165            "Test".to_string(),
166            CapabilityType::Sensor,
167            0.8,
168        );
169
170        assert!(cap.is_valid(0.7));
171        assert!(cap.is_valid(0.8));
172        assert!(!cap.is_valid(0.9));
173    }
174
175    #[test]
176    fn test_is_valid_edge_cases() {
177        let cap_zero = Capability::new(
178            "test".to_string(),
179            "Test".to_string(),
180            CapabilityType::Sensor,
181            0.0,
182        );
183        assert!(cap_zero.is_valid(0.0));
184        assert!(!cap_zero.is_valid(0.1));
185
186        let cap_one = Capability::new(
187            "test".to_string(),
188            "Test".to_string(),
189            CapabilityType::Sensor,
190            1.0,
191        );
192        assert!(cap_one.is_valid(1.0));
193        assert!(cap_one.is_valid(0.9));
194    }
195
196    #[test]
197    fn test_invalid_capability_type_defaults_to_unspecified() {
198        let mut cap = Capability::new(
199            "test".to_string(),
200            "Test".to_string(),
201            CapabilityType::Sensor,
202            0.8,
203        );
204
205        // Manually set to an invalid value
206        cap.capability_type = 999;
207
208        // Should return Unspecified for invalid values
209        assert_eq!(cap.get_capability_type(), CapabilityType::Unspecified);
210    }
211
212    #[test]
213    fn test_metadata_json_field() {
214        let cap = Capability::new(
215            "test".to_string(),
216            "Test".to_string(),
217            CapabilityType::Sensor,
218            0.8,
219        );
220
221        // New capabilities start with empty metadata
222        assert_eq!(cap.metadata_json, "");
223
224        // Can be set to JSON string
225        let mut cap_with_metadata = cap.clone();
226        cap_with_metadata.metadata_json = r#"{"key": "value"}"#.to_string();
227        assert_eq!(cap_with_metadata.metadata_json, r#"{"key": "value"}"#);
228    }
229
230    #[test]
231    fn test_registered_at_field() {
232        let cap = Capability::new(
233            "test".to_string(),
234            "Test".to_string(),
235            CapabilityType::Sensor,
236            0.8,
237        );
238
239        // New capabilities start with no timestamp
240        assert!(cap.registered_at.is_none());
241    }
242
243    #[test]
244    fn test_capability_with_empty_strings() {
245        let cap = Capability::new(String::new(), String::new(), CapabilityType::Sensor, 0.5);
246
247        assert_eq!(cap.id, "");
248        assert_eq!(cap.name, "");
249        assert_eq!(cap.confidence, 0.5);
250    }
251
252    #[test]
253    fn test_capability_confidence_boundary_values() {
254        // Test exact boundary values
255        let cap_zero = Capability::new(
256            "test".to_string(),
257            "Test".to_string(),
258            CapabilityType::Sensor,
259            0.0,
260        );
261        assert_eq!(cap_zero.confidence, 0.0);
262        assert!(cap_zero.is_valid(0.0));
263        assert!(!cap_zero.is_valid(0.000001));
264
265        let cap_one = Capability::new(
266            "test".to_string(),
267            "Test".to_string(),
268            CapabilityType::Sensor,
269            1.0,
270        );
271        assert_eq!(cap_one.confidence, 1.0);
272        assert!(cap_one.is_valid(1.0));
273        assert!(cap_one.is_valid(0.999999));
274    }
275
276    #[test]
277    fn test_capability_type_set_then_get() {
278        let mut cap = Capability::new(
279            "test".to_string(),
280            "Test".to_string(),
281            CapabilityType::Unspecified,
282            0.5,
283        );
284
285        // Test setting each capability type
286        for cap_type in [
287            CapabilityType::Sensor,
288            CapabilityType::Compute,
289            CapabilityType::Communication,
290            CapabilityType::Mobility,
291            CapabilityType::Payload,
292            CapabilityType::Emergent,
293        ] {
294            cap.set_capability_type(cap_type);
295            assert_eq!(cap.get_capability_type(), cap_type);
296            assert_eq!(cap.capability_type, cap_type as i32);
297        }
298    }
299
300    #[test]
301    fn test_capability_clone() {
302        let cap1 = Capability::new(
303            "sensor-1".to_string(),
304            "Camera".to_string(),
305            CapabilityType::Sensor,
306            0.85,
307        );
308
309        let cap2 = cap1.clone();
310        assert_eq!(cap1.id, cap2.id);
311        assert_eq!(cap1.name, cap2.name);
312        assert_eq!(cap1.capability_type, cap2.capability_type);
313        assert_eq!(cap1.confidence, cap2.confidence);
314    }
315
316    #[test]
317    fn test_capability_metadata_json_manipulation() {
318        let mut cap = Capability::new(
319            "test".to_string(),
320            "Test".to_string(),
321            CapabilityType::Sensor,
322            0.8,
323        );
324
325        // Set complex JSON
326        cap.metadata_json =
327            r#"{"manufacturer": "ACME", "model": "X1000", "version": "2.1"}"#.to_string();
328        assert!(cap.metadata_json.contains("ACME"));
329
330        // Parse to ensure it's valid JSON
331        let parsed: serde_json::Value = serde_json::from_str(&cap.metadata_json).unwrap();
332        assert_eq!(parsed["manufacturer"], "ACME");
333        assert_eq!(parsed["model"], "X1000");
334    }
335
336    #[test]
337    fn test_is_valid_threshold_variations() {
338        let cap = Capability::new(
339            "test".to_string(),
340            "Test".to_string(),
341            CapabilityType::Sensor,
342            0.75,
343        );
344
345        // Test various thresholds
346        assert!(cap.is_valid(0.0));
347        assert!(cap.is_valid(0.5));
348        assert!(cap.is_valid(0.74));
349        assert!(cap.is_valid(0.75)); // Equal to confidence
350        assert!(!cap.is_valid(0.76));
351        assert!(!cap.is_valid(0.9));
352        assert!(!cap.is_valid(1.0));
353    }
354
355    #[test]
356    fn test_capability_protobuf_field_access() {
357        let cap = Capability::new(
358            "test-id".to_string(),
359            "Test Name".to_string(),
360            CapabilityType::Compute,
361            0.9,
362        );
363
364        // Direct protobuf field access
365        assert_eq!(cap.id, "test-id");
366        assert_eq!(cap.name, "Test Name");
367        assert_eq!(cap.capability_type, CapabilityType::Compute as i32);
368        assert_eq!(cap.confidence, 0.9);
369        assert_eq!(cap.metadata_json, "");
370        assert!(cap.registered_at.is_none());
371    }
372}