Skip to main content

datasynth_ocpm/models/
object_relationship.rs

1//! Object relationship model for OCPM.
2//!
3//! Object relationships capture the many-to-many connections between
4//! business objects, such as "Order contains OrderLines" or
5//! "Invoice references PurchaseOrder".
6
7use chrono::{DateTime, Utc};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use uuid::Uuid;
12
13use super::ObjectAttributeValue;
14
15/// Many-to-many relationship between object instances.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ObjectRelationship {
18    /// Unique relationship ID
19    pub relationship_id: Uuid,
20    /// Relationship type (from ObjectRelationshipType)
21    pub relationship_type: String,
22    /// Source object ID
23    pub source_object_id: Uuid,
24    /// Source object type
25    pub source_type_id: String,
26    /// Target object ID
27    pub target_object_id: Uuid,
28    /// Target object type
29    pub target_type_id: String,
30    /// When the relationship was established
31    pub established_at: DateTime<Utc>,
32    /// Optional quantity for the relationship (e.g., items ordered)
33    pub quantity: Option<Decimal>,
34    /// Additional attributes
35    pub attributes: HashMap<String, ObjectAttributeValue>,
36}
37
38impl ObjectRelationship {
39    /// Create a new object relationship.
40    pub fn new(
41        relationship_type: &str,
42        source_object_id: Uuid,
43        source_type_id: &str,
44        target_object_id: Uuid,
45        target_type_id: &str,
46    ) -> Self {
47        Self {
48            relationship_id: Uuid::new_v4(),
49            relationship_type: relationship_type.into(),
50            source_object_id,
51            source_type_id: source_type_id.into(),
52            target_object_id,
53            target_type_id: target_type_id.into(),
54            established_at: Utc::now(),
55            quantity: None,
56            attributes: HashMap::new(),
57        }
58    }
59
60    /// Set the established timestamp.
61    pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
62        self.established_at = timestamp;
63        self
64    }
65
66    /// Set the quantity.
67    pub fn with_quantity(mut self, quantity: Decimal) -> Self {
68        self.quantity = Some(quantity);
69        self
70    }
71
72    /// Add an attribute.
73    pub fn with_attribute(mut self, key: &str, value: ObjectAttributeValue) -> Self {
74        self.attributes.insert(key.into(), value);
75        self
76    }
77}
78
79/// Index for fast relationship lookups.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct RelationshipIndex {
82    /// All relationships
83    relationships: Vec<ObjectRelationship>,
84    /// Index: source_object_id -> relationship indices
85    by_source: HashMap<Uuid, Vec<usize>>,
86    /// Index: target_object_id -> relationship indices
87    by_target: HashMap<Uuid, Vec<usize>>,
88    /// Index: relationship_type -> relationship indices
89    by_type: HashMap<String, Vec<usize>>,
90}
91
92impl RelationshipIndex {
93    /// Create a new relationship index.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Add a relationship to the index.
99    pub fn add(&mut self, relationship: ObjectRelationship) {
100        let idx = self.relationships.len();
101
102        self.by_source
103            .entry(relationship.source_object_id)
104            .or_default()
105            .push(idx);
106
107        self.by_target
108            .entry(relationship.target_object_id)
109            .or_default()
110            .push(idx);
111
112        self.by_type
113            .entry(relationship.relationship_type.clone())
114            .or_default()
115            .push(idx);
116
117        self.relationships.push(relationship);
118    }
119
120    /// Get all relationships from a source object.
121    pub fn get_outgoing(&self, source_id: Uuid) -> Vec<&ObjectRelationship> {
122        self.by_source
123            .get(&source_id)
124            .map(|indices| {
125                indices
126                    .iter()
127                    .filter_map(|&i| self.relationships.get(i))
128                    .collect()
129            })
130            .unwrap_or_default()
131    }
132
133    /// Get all relationships to a target object.
134    pub fn get_incoming(&self, target_id: Uuid) -> Vec<&ObjectRelationship> {
135        self.by_target
136            .get(&target_id)
137            .map(|indices| {
138                indices
139                    .iter()
140                    .filter_map(|&i| self.relationships.get(i))
141                    .collect()
142            })
143            .unwrap_or_default()
144    }
145
146    /// Get all relationships of a specific type.
147    pub fn get_by_type(&self, relationship_type: &str) -> Vec<&ObjectRelationship> {
148        self.by_type
149            .get(relationship_type)
150            .map(|indices| {
151                indices
152                    .iter()
153                    .filter_map(|&i| self.relationships.get(i))
154                    .collect()
155            })
156            .unwrap_or_default()
157    }
158
159    /// Get all relationships.
160    pub fn all(&self) -> &[ObjectRelationship] {
161        &self.relationships
162    }
163
164    /// Get the total number of relationships.
165    pub fn len(&self) -> usize {
166        self.relationships.len()
167    }
168
169    /// Check if the index is empty.
170    pub fn is_empty(&self) -> bool {
171        self.relationships.is_empty()
172    }
173
174    /// Iterate over all relationships.
175    pub fn iter(&self) -> impl Iterator<Item = &ObjectRelationship> {
176        self.relationships.iter()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_relationship_creation() {
186        let source_id = Uuid::new_v4();
187        let target_id = Uuid::new_v4();
188
189        let rel = ObjectRelationship::new(
190            "contains",
191            source_id,
192            "purchase_order",
193            target_id,
194            "order_line",
195        )
196        .with_quantity(Decimal::from(10));
197
198        assert_eq!(rel.relationship_type, "contains");
199        assert_eq!(rel.source_object_id, source_id);
200        assert_eq!(rel.target_object_id, target_id);
201        assert_eq!(rel.quantity, Some(Decimal::from(10)));
202    }
203
204    #[test]
205    fn test_relationship_index() {
206        let mut index = RelationshipIndex::new();
207
208        let po_id = Uuid::new_v4();
209        let line1_id = Uuid::new_v4();
210        let line2_id = Uuid::new_v4();
211
212        index.add(ObjectRelationship::new(
213            "contains",
214            po_id,
215            "purchase_order",
216            line1_id,
217            "order_line",
218        ));
219        index.add(ObjectRelationship::new(
220            "contains",
221            po_id,
222            "purchase_order",
223            line2_id,
224            "order_line",
225        ));
226
227        assert_eq!(index.len(), 2);
228        assert_eq!(index.get_outgoing(po_id).len(), 2);
229        assert_eq!(index.get_incoming(line1_id).len(), 1);
230        assert_eq!(index.get_by_type("contains").len(), 2);
231    }
232}