Skip to main content

datasynth_ocpm/models/
object_instance.rs

1//! Object instance model for OCPM.
2//!
3//! Object instances are specific occurrences of object types that
4//! participate in process executions.
5
6use chrono::{DateTime, NaiveDate, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// A business object instance in OCPM.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ObjectInstance {
15    /// Unique object identifier
16    pub object_id: Uuid,
17    /// Object type reference
18    pub object_type_id: String,
19    /// External ID (e.g., "PO-1000-0000000001")
20    pub external_id: String,
21    /// Current lifecycle state
22    pub current_state: String,
23    /// Creation timestamp
24    pub created_at: DateTime<Utc>,
25    /// Completion timestamp (if terminal state reached)
26    pub completed_at: Option<DateTime<Utc>>,
27    /// Company code
28    pub company_code: String,
29    /// Object attributes
30    pub attributes: HashMap<String, ObjectAttributeValue>,
31    /// Is this object marked as anomalous
32    pub is_anomaly: bool,
33    /// Anomaly type if applicable
34    pub anomaly_type: Option<String>,
35}
36
37impl ObjectInstance {
38    /// Create a new object instance.
39    pub fn new(object_type_id: &str, external_id: &str, company_code: &str) -> Self {
40        Self {
41            object_id: Uuid::new_v4(),
42            object_type_id: object_type_id.into(),
43            external_id: external_id.into(),
44            current_state: "created".into(),
45            created_at: Utc::now(),
46            completed_at: None,
47            company_code: company_code.into(),
48            attributes: HashMap::new(),
49            is_anomaly: false,
50            anomaly_type: None,
51        }
52    }
53
54    /// Create with a specific UUID (for deterministic generation).
55    pub fn with_id(mut self, id: Uuid) -> Self {
56        self.object_id = id;
57        self
58    }
59
60    /// Set the initial state.
61    pub fn with_state(mut self, state: &str) -> Self {
62        self.current_state = state.into();
63        self
64    }
65
66    /// Set creation timestamp.
67    pub fn with_created_at(mut self, created_at: DateTime<Utc>) -> Self {
68        self.created_at = created_at;
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    /// Mark as completed.
79    pub fn complete(&mut self, terminal_state: &str) {
80        self.current_state = terminal_state.into();
81        self.completed_at = Some(Utc::now());
82    }
83
84    /// Transition to a new state.
85    pub fn transition(&mut self, new_state: &str) {
86        self.current_state = new_state.into();
87    }
88
89    /// Mark as anomalous.
90    pub fn mark_anomaly(&mut self, anomaly_type: &str) {
91        self.is_anomaly = true;
92        self.anomaly_type = Some(anomaly_type.into());
93    }
94
95    /// Check if the object is in a terminal state.
96    pub fn is_completed(&self) -> bool {
97        self.completed_at.is_some()
98    }
99
100    /// Get a string attribute value.
101    pub fn get_string(&self, key: &str) -> Option<&str> {
102        match self.attributes.get(key) {
103            Some(ObjectAttributeValue::String(s)) => Some(s.as_str()),
104            _ => None,
105        }
106    }
107
108    /// Get a decimal attribute value.
109    pub fn get_decimal(&self, key: &str) -> Option<Decimal> {
110        match self.attributes.get(key) {
111            Some(ObjectAttributeValue::Decimal(d)) => Some(*d),
112            _ => None,
113        }
114    }
115}
116
117/// Attribute values for object instances.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(untagged)]
120pub enum ObjectAttributeValue {
121    /// String value
122    String(String),
123    /// Integer value
124    Integer(i64),
125    /// Decimal value (for monetary amounts)
126    Decimal(Decimal),
127    /// Date value
128    Date(NaiveDate),
129    /// DateTime value
130    DateTime(DateTime<Utc>),
131    /// Boolean value
132    Boolean(bool),
133    /// Reference to another object
134    Reference(Uuid),
135    /// Null/missing value
136    Null,
137}
138
139impl From<String> for ObjectAttributeValue {
140    fn from(s: String) -> Self {
141        Self::String(s)
142    }
143}
144
145impl From<&str> for ObjectAttributeValue {
146    fn from(s: &str) -> Self {
147        Self::String(s.into())
148    }
149}
150
151impl From<i64> for ObjectAttributeValue {
152    fn from(i: i64) -> Self {
153        Self::Integer(i)
154    }
155}
156
157impl From<Decimal> for ObjectAttributeValue {
158    fn from(d: Decimal) -> Self {
159        Self::Decimal(d)
160    }
161}
162
163impl From<NaiveDate> for ObjectAttributeValue {
164    fn from(d: NaiveDate) -> Self {
165        Self::Date(d)
166    }
167}
168
169impl From<DateTime<Utc>> for ObjectAttributeValue {
170    fn from(dt: DateTime<Utc>) -> Self {
171        Self::DateTime(dt)
172    }
173}
174
175impl From<bool> for ObjectAttributeValue {
176    fn from(b: bool) -> Self {
177        Self::Boolean(b)
178    }
179}
180
181impl From<Uuid> for ObjectAttributeValue {
182    fn from(id: Uuid) -> Self {
183        Self::Reference(id)
184    }
185}
186
187/// Graph of objects with relationships indexed for fast lookup.
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct ObjectGraph {
190    /// All objects by ID
191    pub objects: HashMap<Uuid, ObjectInstance>,
192    /// Objects indexed by type
193    objects_by_type: HashMap<String, Vec<Uuid>>,
194    /// Objects indexed by external ID
195    objects_by_external_id: HashMap<String, Uuid>,
196}
197
198impl ObjectGraph {
199    /// Create a new empty object graph.
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    /// Add an object to the graph.
205    pub fn add_object(&mut self, object: ObjectInstance) {
206        let object_id = object.object_id;
207        let type_id = object.object_type_id.clone();
208        let external_id = object.external_id.clone();
209
210        self.objects.insert(object_id, object);
211
212        self.objects_by_type
213            .entry(type_id)
214            .or_default()
215            .push(object_id);
216
217        self.objects_by_external_id.insert(external_id, object_id);
218    }
219
220    /// Get an object by ID.
221    pub fn get(&self, object_id: Uuid) -> Option<&ObjectInstance> {
222        self.objects.get(&object_id)
223    }
224
225    /// Get a mutable reference to an object.
226    pub fn get_mut(&mut self, object_id: Uuid) -> Option<&mut ObjectInstance> {
227        self.objects.get_mut(&object_id)
228    }
229
230    /// Get an object by external ID.
231    pub fn get_by_external_id(&self, external_id: &str) -> Option<&ObjectInstance> {
232        self.objects_by_external_id
233            .get(external_id)
234            .and_then(|id| self.objects.get(id))
235    }
236
237    /// Get all objects of a specific type.
238    pub fn get_by_type(&self, type_id: &str) -> Vec<&ObjectInstance> {
239        self.objects_by_type
240            .get(type_id)
241            .map(|ids| ids.iter().filter_map(|id| self.objects.get(id)).collect())
242            .unwrap_or_default()
243    }
244
245    /// Get the total number of objects.
246    pub fn len(&self) -> usize {
247        self.objects.len()
248    }
249
250    /// Check if the graph is empty.
251    pub fn is_empty(&self) -> bool {
252        self.objects.is_empty()
253    }
254
255    /// Iterate over all objects.
256    pub fn iter(&self) -> impl Iterator<Item = &ObjectInstance> {
257        self.objects.values()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_object_instance_creation() {
267        let obj = ObjectInstance::new("purchase_order", "PO-001", "1000");
268        assert_eq!(obj.object_type_id, "purchase_order");
269        assert_eq!(obj.external_id, "PO-001");
270        assert_eq!(obj.company_code, "1000");
271        assert!(!obj.is_anomaly);
272    }
273
274    #[test]
275    fn test_object_graph() {
276        let mut graph = ObjectGraph::new();
277
278        let po1 = ObjectInstance::new("purchase_order", "PO-001", "1000");
279        let po2 = ObjectInstance::new("purchase_order", "PO-002", "1000");
280        let gr1 = ObjectInstance::new("goods_receipt", "GR-001", "1000");
281
282        graph.add_object(po1);
283        graph.add_object(po2);
284        graph.add_object(gr1);
285
286        assert_eq!(graph.len(), 3);
287        assert_eq!(graph.get_by_type("purchase_order").len(), 2);
288        assert_eq!(graph.get_by_type("goods_receipt").len(), 1);
289        assert!(graph.get_by_external_id("PO-001").is_some());
290    }
291}