ruvector_graph/
hyperedge.rs

1//! N-ary relationship support (hyperedges)
2//!
3//! Extends the basic edge model to support relationships connecting multiple nodes
4
5use crate::types::{NodeId, Properties, PropertyValue};
6use bincode::{Decode, Encode};
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use uuid::Uuid;
10
11/// Unique identifier for a hyperedge
12pub type HyperedgeId = String;
13
14/// Hyperedge connecting multiple nodes (N-ary relationship)
15#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
16pub struct Hyperedge {
17    /// Unique identifier
18    pub id: HyperedgeId,
19    /// Node IDs connected by this hyperedge
20    pub nodes: Vec<NodeId>,
21    /// Hyperedge type/label (e.g., "MEETING", "COLLABORATION")
22    pub edge_type: String,
23    /// Natural language description of the relationship
24    pub description: Option<String>,
25    /// Property key-value pairs
26    pub properties: Properties,
27    /// Confidence/weight (0.0-1.0)
28    pub confidence: f32,
29}
30
31impl Hyperedge {
32    /// Create a new hyperedge with generated UUID
33    pub fn new<S: Into<String>>(nodes: Vec<NodeId>, edge_type: S) -> Self {
34        Self {
35            id: Uuid::new_v4().to_string(),
36            nodes,
37            edge_type: edge_type.into(),
38            description: None,
39            properties: Properties::new(),
40            confidence: 1.0,
41        }
42    }
43
44    /// Create a new hyperedge with specific ID
45    pub fn with_id<S: Into<String>>(id: HyperedgeId, nodes: Vec<NodeId>, edge_type: S) -> Self {
46        Self {
47            id,
48            nodes,
49            edge_type: edge_type.into(),
50            description: None,
51            properties: Properties::new(),
52            confidence: 1.0,
53        }
54    }
55
56    /// Get the order of the hyperedge (number of nodes)
57    pub fn order(&self) -> usize {
58        self.nodes.len()
59    }
60
61    /// Check if hyperedge contains a specific node
62    pub fn contains_node(&self, node_id: &NodeId) -> bool {
63        self.nodes.contains(node_id)
64    }
65
66    /// Check if hyperedge contains all specified nodes
67    pub fn contains_all_nodes(&self, node_ids: &[NodeId]) -> bool {
68        node_ids.iter().all(|id| self.contains_node(id))
69    }
70
71    /// Check if hyperedge contains any of the specified nodes
72    pub fn contains_any_node(&self, node_ids: &[NodeId]) -> bool {
73        node_ids.iter().any(|id| self.contains_node(id))
74    }
75
76    /// Get unique nodes (removes duplicates)
77    pub fn unique_nodes(&self) -> HashSet<&NodeId> {
78        self.nodes.iter().collect()
79    }
80
81    /// Set the description
82    pub fn set_description<S: Into<String>>(&mut self, description: S) -> &mut Self {
83        self.description = Some(description.into());
84        self
85    }
86
87    /// Set the confidence
88    pub fn set_confidence(&mut self, confidence: f32) -> &mut Self {
89        self.confidence = confidence.clamp(0.0, 1.0);
90        self
91    }
92
93    /// Set a property
94    pub fn set_property<K, V>(&mut self, key: K, value: V) -> &mut Self
95    where
96        K: Into<String>,
97        V: Into<PropertyValue>,
98    {
99        self.properties.insert(key.into(), value.into());
100        self
101    }
102
103    /// Get a property
104    pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
105        self.properties.get(key)
106    }
107
108    /// Remove a property
109    pub fn remove_property(&mut self, key: &str) -> Option<PropertyValue> {
110        self.properties.remove(key)
111    }
112
113    /// Check if hyperedge has a property
114    pub fn has_property(&self, key: &str) -> bool {
115        self.properties.contains_key(key)
116    }
117
118    /// Get all property keys
119    pub fn property_keys(&self) -> Vec<&String> {
120        self.properties.keys().collect()
121    }
122
123    /// Clear all properties
124    pub fn clear_properties(&mut self) {
125        self.properties.clear();
126    }
127
128    /// Get the number of properties
129    pub fn property_count(&self) -> usize {
130        self.properties.len()
131    }
132}
133
134/// Builder for creating hyperedges with fluent API
135pub struct HyperedgeBuilder {
136    hyperedge: Hyperedge,
137}
138
139impl HyperedgeBuilder {
140    /// Create a new builder
141    pub fn new<S: Into<String>>(nodes: Vec<NodeId>, edge_type: S) -> Self {
142        Self {
143            hyperedge: Hyperedge::new(nodes, edge_type),
144        }
145    }
146
147    /// Create builder with specific ID
148    pub fn with_id<S: Into<String>>(id: HyperedgeId, nodes: Vec<NodeId>, edge_type: S) -> Self {
149        Self {
150            hyperedge: Hyperedge::with_id(id, nodes, edge_type),
151        }
152    }
153
154    /// Set description
155    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
156        self.hyperedge.set_description(description);
157        self
158    }
159
160    /// Set confidence
161    pub fn confidence(mut self, confidence: f32) -> Self {
162        self.hyperedge.set_confidence(confidence);
163        self
164    }
165
166    /// Set a property
167    pub fn property<K, V>(mut self, key: K, value: V) -> Self
168    where
169        K: Into<String>,
170        V: Into<PropertyValue>,
171    {
172        self.hyperedge.set_property(key, value);
173        self
174    }
175
176    /// Build the hyperedge
177    pub fn build(self) -> Hyperedge {
178        self.hyperedge
179    }
180}
181
182/// Hyperedge role assignment for directed N-ary relationships
183#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
184pub struct HyperedgeWithRoles {
185    /// Base hyperedge
186    pub hyperedge: Hyperedge,
187    /// Role assignments: node_id -> role
188    pub roles: std::collections::HashMap<NodeId, String>,
189}
190
191impl HyperedgeWithRoles {
192    /// Create a new hyperedge with roles
193    pub fn new(hyperedge: Hyperedge) -> Self {
194        Self {
195            hyperedge,
196            roles: std::collections::HashMap::new(),
197        }
198    }
199
200    /// Assign a role to a node
201    pub fn assign_role<S: Into<String>>(&mut self, node_id: NodeId, role: S) -> &mut Self {
202        self.roles.insert(node_id, role.into());
203        self
204    }
205
206    /// Get the role of a node
207    pub fn get_role(&self, node_id: &NodeId) -> Option<&String> {
208        self.roles.get(node_id)
209    }
210
211    /// Get all nodes with a specific role
212    pub fn nodes_with_role(&self, role: &str) -> Vec<&NodeId> {
213        self.roles
214            .iter()
215            .filter(|(_, r)| r.as_str() == role)
216            .map(|(id, _)| id)
217            .collect()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_hyperedge_creation() {
227        let nodes = vec![
228            "node1".to_string(),
229            "node2".to_string(),
230            "node3".to_string(),
231        ];
232        let hedge = Hyperedge::new(nodes, "MEETING");
233
234        assert!(!hedge.id.is_empty());
235        assert_eq!(hedge.order(), 3);
236        assert_eq!(hedge.edge_type, "MEETING");
237        assert_eq!(hedge.confidence, 1.0);
238    }
239
240    #[test]
241    fn test_hyperedge_contains() {
242        let nodes = vec![
243            "node1".to_string(),
244            "node2".to_string(),
245            "node3".to_string(),
246        ];
247        let hedge = Hyperedge::new(nodes, "MEETING");
248
249        assert!(hedge.contains_node(&"node1".to_string()));
250        assert!(hedge.contains_node(&"node2".to_string()));
251        assert!(!hedge.contains_node(&"node4".to_string()));
252
253        assert!(hedge.contains_all_nodes(&["node1".to_string(), "node2".to_string()]));
254        assert!(!hedge.contains_all_nodes(&["node1".to_string(), "node4".to_string()]));
255
256        assert!(hedge.contains_any_node(&["node1".to_string(), "node4".to_string()]));
257        assert!(!hedge.contains_any_node(&["node4".to_string(), "node5".to_string()]));
258    }
259
260    #[test]
261    fn test_hyperedge_builder() {
262        let nodes = vec!["node1".to_string(), "node2".to_string()];
263        let hedge = HyperedgeBuilder::new(nodes, "COLLABORATION")
264            .description("Team collaboration on project X")
265            .confidence(0.95)
266            .property("project", "X")
267            .property("duration", 30i64)
268            .build();
269
270        assert_eq!(hedge.edge_type, "COLLABORATION");
271        assert_eq!(hedge.confidence, 0.95);
272        assert!(hedge.description.is_some());
273        assert_eq!(
274            hedge.get_property("project"),
275            Some(&PropertyValue::String("X".to_string()))
276        );
277    }
278
279    #[test]
280    fn test_hyperedge_with_roles() {
281        let nodes = vec![
282            "alice".to_string(),
283            "bob".to_string(),
284            "charlie".to_string(),
285        ];
286        let hedge = Hyperedge::new(nodes, "MEETING");
287
288        let mut hedge_with_roles = HyperedgeWithRoles::new(hedge);
289        hedge_with_roles.assign_role("alice".to_string(), "organizer");
290        hedge_with_roles.assign_role("bob".to_string(), "participant");
291        hedge_with_roles.assign_role("charlie".to_string(), "participant");
292
293        assert_eq!(
294            hedge_with_roles.get_role(&"alice".to_string()),
295            Some(&"organizer".to_string())
296        );
297
298        let participants = hedge_with_roles.nodes_with_role("participant");
299        assert_eq!(participants.len(), 2);
300    }
301
302    #[test]
303    fn test_unique_nodes() {
304        let nodes = vec![
305            "node1".to_string(),
306            "node2".to_string(),
307            "node1".to_string(), // duplicate
308        ];
309        let hedge = Hyperedge::new(nodes, "TEST");
310
311        let unique = hedge.unique_nodes();
312        assert_eq!(unique.len(), 2);
313    }
314}