data_modelling_core/models/
domain_config.rs

1//! Domain configuration model
2//!
3//! Defines the DomainConfig entity for the data modelling application.
4//! This represents the domain.yaml configuration file that stores domain metadata
5//! and asset references.
6//!
7//! Note: This is separate from the `Domain` struct in domain.rs which represents
8//! the visual domain schema with systems, nodes, and connections.
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use uuid::Uuid;
14
15/// Owner information for a domain
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
17pub struct DomainOwner {
18    /// Owner name
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub name: Option<String>,
21    /// Owner email
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub email: Option<String>,
24    /// Team name
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub team: Option<String>,
27    /// Role (e.g., "Data Owner", "Data Steward")
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub role: Option<String>,
30}
31
32/// Position on canvas for view rendering
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct ViewPosition {
35    /// X coordinate on canvas
36    pub x: f64,
37    /// Y coordinate on canvas
38    pub y: f64,
39}
40
41/// DomainConfig - Configuration file for a domain (domain.yaml)
42///
43/// This represents the domain.yaml file that stores:
44/// - Domain metadata (id, name, description, timestamps)
45/// - Owner information
46/// - References to assets (tables, products, assets, processes, decisions)
47/// - View positions for canvas rendering
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub struct DomainConfig {
50    /// Unique identifier for the domain
51    pub id: Uuid,
52    /// Parent workspace identifier
53    pub workspace_id: Uuid,
54    /// Domain name (unique within workspace, max 255 chars)
55    pub name: String,
56    /// Optional description of the domain's purpose
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub description: Option<String>,
59    /// Creation timestamp
60    pub created_at: DateTime<Utc>,
61    /// Last modification timestamp
62    pub last_modified_at: DateTime<Utc>,
63    /// Owner information
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub owner: Option<DomainOwner>,
66    /// Array of system IDs that belong to this domain
67    #[serde(default, skip_serializing_if = "Vec::is_empty")]
68    pub systems: Vec<Uuid>,
69    /// Array of ODCS table IDs that belong to this domain
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub tables: Vec<Uuid>,
72    /// Array of ODPS product IDs that belong to this domain
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub products: Vec<Uuid>,
75    /// Array of CADS compute asset IDs that belong to this domain
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    pub assets: Vec<Uuid>,
78    /// Array of BPMN process IDs that belong to this domain
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub processes: Vec<Uuid>,
81    /// Array of DMN decision IDs that belong to this domain
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub decisions: Vec<Uuid>,
84    /// View positions for different view modes
85    /// Key: view mode name (e.g., "systems", "process", "operational", "analytical", "products")
86    /// Value: Map of entity ID to position
87    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
88    pub view_positions: HashMap<String, HashMap<String, ViewPosition>>,
89    /// Path to domain folder (for offline mode)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub folder_path: Option<String>,
92    /// Path to workspace root folder (for offline mode)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub workspace_path: Option<String>,
95}
96
97impl DomainConfig {
98    /// Create a new DomainConfig
99    pub fn new(name: String, workspace_id: Uuid) -> Self {
100        let now = Utc::now();
101        Self {
102            id: Uuid::new_v4(),
103            workspace_id,
104            name,
105            description: None,
106            created_at: now,
107            last_modified_at: now,
108            owner: None,
109            systems: Vec::new(),
110            tables: Vec::new(),
111            products: Vec::new(),
112            assets: Vec::new(),
113            processes: Vec::new(),
114            decisions: Vec::new(),
115            view_positions: HashMap::new(),
116            folder_path: None,
117            workspace_path: None,
118        }
119    }
120
121    /// Create a DomainConfig with a specific ID
122    pub fn with_id(id: Uuid, name: String, workspace_id: Uuid) -> Self {
123        let now = Utc::now();
124        Self {
125            id,
126            workspace_id,
127            name,
128            description: None,
129            created_at: now,
130            last_modified_at: now,
131            owner: None,
132            systems: Vec::new(),
133            tables: Vec::new(),
134            products: Vec::new(),
135            assets: Vec::new(),
136            processes: Vec::new(),
137            decisions: Vec::new(),
138            view_positions: HashMap::new(),
139            folder_path: None,
140            workspace_path: None,
141        }
142    }
143
144    /// Set description
145    pub fn with_description(mut self, description: String) -> Self {
146        self.description = Some(description);
147        self
148    }
149
150    /// Set owner
151    pub fn with_owner(mut self, owner: DomainOwner) -> Self {
152        self.owner = Some(owner);
153        self
154    }
155
156    /// Add a table ID
157    pub fn add_table(&mut self, table_id: Uuid) {
158        if !self.tables.contains(&table_id) {
159            self.tables.push(table_id);
160            self.last_modified_at = Utc::now();
161        }
162    }
163
164    /// Remove a table ID
165    pub fn remove_table(&mut self, table_id: Uuid) -> bool {
166        let initial_len = self.tables.len();
167        self.tables.retain(|&id| id != table_id);
168        if self.tables.len() != initial_len {
169            self.last_modified_at = Utc::now();
170            true
171        } else {
172            false
173        }
174    }
175
176    /// Add a product ID
177    pub fn add_product(&mut self, product_id: Uuid) {
178        if !self.products.contains(&product_id) {
179            self.products.push(product_id);
180            self.last_modified_at = Utc::now();
181        }
182    }
183
184    /// Add an asset ID
185    pub fn add_asset(&mut self, asset_id: Uuid) {
186        if !self.assets.contains(&asset_id) {
187            self.assets.push(asset_id);
188            self.last_modified_at = Utc::now();
189        }
190    }
191
192    /// Add a process ID
193    pub fn add_process(&mut self, process_id: Uuid) {
194        if !self.processes.contains(&process_id) {
195            self.processes.push(process_id);
196            self.last_modified_at = Utc::now();
197        }
198    }
199
200    /// Add a decision ID
201    pub fn add_decision(&mut self, decision_id: Uuid) {
202        if !self.decisions.contains(&decision_id) {
203            self.decisions.push(decision_id);
204            self.last_modified_at = Utc::now();
205        }
206    }
207
208    /// Add a system ID
209    pub fn add_system(&mut self, system_id: Uuid) {
210        if !self.systems.contains(&system_id) {
211            self.systems.push(system_id);
212            self.last_modified_at = Utc::now();
213        }
214    }
215
216    /// Set view position for an entity in a view mode
217    pub fn set_view_position(&mut self, view_mode: &str, entity_id: &str, x: f64, y: f64) {
218        let positions = self
219            .view_positions
220            .entry(view_mode.to_string())
221            .or_default();
222        positions.insert(entity_id.to_string(), ViewPosition { x, y });
223        self.last_modified_at = Utc::now();
224    }
225
226    /// Get view position for an entity in a view mode
227    pub fn get_view_position(&self, view_mode: &str, entity_id: &str) -> Option<&ViewPosition> {
228        self.view_positions
229            .get(view_mode)
230            .and_then(|positions| positions.get(entity_id))
231    }
232
233    /// Import from YAML
234    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
235        serde_yaml::from_str(yaml_content)
236    }
237
238    /// Export to YAML
239    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
240        serde_yaml::to_string(self)
241    }
242
243    /// Import from JSON
244    pub fn from_json(json_content: &str) -> Result<Self, serde_json::Error> {
245        serde_json::from_str(json_content)
246    }
247
248    /// Export to JSON
249    pub fn to_json(&self) -> Result<String, serde_json::Error> {
250        serde_json::to_string(self)
251    }
252
253    /// Export to pretty JSON
254    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
255        serde_json::to_string_pretty(self)
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_domain_config_new() {
265        let workspace_id = Uuid::new_v4();
266        let config = DomainConfig::new("Customer Management".to_string(), workspace_id);
267
268        assert_eq!(config.name, "Customer Management");
269        assert_eq!(config.workspace_id, workspace_id);
270        assert!(config.tables.is_empty());
271        assert!(config.products.is_empty());
272    }
273
274    #[test]
275    fn test_domain_config_add_table() {
276        let mut config = DomainConfig::new("Test".to_string(), Uuid::new_v4());
277        let table_id = Uuid::new_v4();
278
279        config.add_table(table_id);
280        assert_eq!(config.tables.len(), 1);
281        assert_eq!(config.tables[0], table_id);
282
283        // Adding same ID should not duplicate
284        config.add_table(table_id);
285        assert_eq!(config.tables.len(), 1);
286    }
287
288    #[test]
289    fn test_domain_config_view_positions() {
290        let mut config = DomainConfig::new("Test".to_string(), Uuid::new_v4());
291        let entity_id = Uuid::new_v4().to_string();
292
293        config.set_view_position("systems", &entity_id, 100.0, 200.0);
294
295        let pos = config.get_view_position("systems", &entity_id);
296        assert!(pos.is_some());
297        let pos = pos.unwrap();
298        assert_eq!(pos.x, 100.0);
299        assert_eq!(pos.y, 200.0);
300    }
301
302    #[test]
303    fn test_domain_config_yaml_roundtrip() {
304        let workspace_id = Uuid::new_v4();
305        let mut config = DomainConfig::new("Finance".to_string(), workspace_id);
306        config.description = Some("Financial data domain".to_string());
307        config.owner = Some(DomainOwner {
308            name: Some("Jane Doe".to_string()),
309            email: Some("jane@example.com".to_string()),
310            team: Some("Data Team".to_string()),
311            role: Some("Data Owner".to_string()),
312        });
313        config.add_table(Uuid::new_v4());
314        config.add_product(Uuid::new_v4());
315
316        let yaml = config.to_yaml().unwrap();
317        let parsed = DomainConfig::from_yaml(&yaml).unwrap();
318
319        assert_eq!(config.id, parsed.id);
320        assert_eq!(config.name, parsed.name);
321        assert_eq!(config.description, parsed.description);
322        assert_eq!(config.tables.len(), parsed.tables.len());
323    }
324
325    #[test]
326    fn test_domain_config_json_roundtrip() {
327        let config = DomainConfig::new("Test".to_string(), Uuid::new_v4());
328
329        let json = config.to_json().unwrap();
330        let parsed = DomainConfig::from_json(&json).unwrap();
331
332        assert_eq!(config.id, parsed.id);
333        assert_eq!(config.name, parsed.name);
334    }
335}