Skip to main content

alizarin_core/graph/
nodes.rs

1//! Node, Nodegroup, and Edge types for the graph structure.
2
3use super::translatable::StaticTranslatableString;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// A node in the graph representing a data field or structural element
8#[derive(Serialize, Deserialize, Clone, Debug)]
9pub struct StaticNode {
10    pub nodeid: String,
11    pub name: String,
12    #[serde(default)]
13    pub alias: Option<String>,
14    pub datatype: String,
15    #[serde(default)]
16    pub nodegroup_id: Option<String>,
17    pub graph_id: String,
18    #[serde(default)]
19    pub is_collector: bool,
20    #[serde(default)]
21    pub isrequired: bool,
22    #[serde(default)]
23    pub exportable: bool,
24    #[serde(default)]
25    pub sortorder: Option<i32>,
26    #[serde(default)]
27    pub config: HashMap<String, serde_json::Value>,
28    #[serde(default)]
29    pub parentproperty: Option<String>,
30    /// Ontology class URIs for this node. Accepts a single string or an array
31    /// of strings on the wire; a single-element list is serialised as a plain
32    /// string for round-trip compatibility with upstream Arches.
33    #[serde(default, with = "super::serde_helpers::optional_string_or_vec")]
34    pub ontologyclass: Option<Vec<String>>,
35    #[serde(default)]
36    pub description: Option<StaticTranslatableString>,
37    #[serde(default)]
38    pub fieldname: Option<String>,
39    #[serde(default)]
40    pub hascustomalias: bool,
41    #[serde(default)]
42    pub issearchable: bool,
43    #[serde(default)]
44    pub istopnode: bool,
45    #[serde(default)]
46    pub sourcebranchpublication_id: Option<String>,
47    // Arches-HER 2.0+ fields
48    /// Source identifier for import/export tracking
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub source_identifier_id: Option<String>,
51    /// Whether node is immutable
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub is_immutable: Option<bool>,
54}
55
56impl StaticNode {
57    /// Check if this node is the root node (no nodegroup_id)
58    pub fn is_root(&self) -> bool {
59        self.nodegroup_id.is_none()
60            || self
61                .nodegroup_id
62                .as_ref()
63                .map(|s| s.is_empty())
64                .unwrap_or(true)
65    }
66
67    /// Get the display name
68    pub fn display_name(&self) -> &str {
69        &self.name
70    }
71
72    /// Get the alias or empty string
73    pub fn display_alias(&self) -> &str {
74        self.alias.as_deref().unwrap_or("")
75    }
76
77    /// Serialize to JSON value
78    pub fn to_json(&self) -> serde_json::Value {
79        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
80    }
81}
82
83/// A nodegroup defining cardinality and grouping of nodes
84#[derive(Serialize, Deserialize, Clone, Debug)]
85pub struct StaticNodegroup {
86    pub nodegroupid: String,
87    #[serde(default)]
88    pub cardinality: Option<String>,
89    #[serde(default)]
90    pub parentnodegroup_id: Option<String>,
91    #[serde(default)]
92    pub legacygroupid: Option<String>,
93    // Arches-HER 2.0+ fields
94    /// Reference to the grouping/semantic node for this nodegroup
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub grouping_node_id: Option<String>,
97}
98
99impl StaticNodegroup {
100    /// Serialize to JSON value
101    pub fn to_json(&self) -> serde_json::Value {
102        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
103    }
104}
105
106/// An edge connecting two nodes (domain -> range)
107#[derive(Serialize, Deserialize, Clone, Debug)]
108pub struct StaticEdge {
109    pub domainnode_id: String,
110    pub rangenode_id: String,
111    #[serde(default)]
112    pub edgeid: String,
113    #[serde(default)]
114    pub graph_id: String,
115    #[serde(default)]
116    pub name: Option<String>,
117    #[serde(default)]
118    pub ontologyproperty: Option<String>,
119    #[serde(default)]
120    pub description: Option<String>,
121    // Arches-HER 2.0+ fields
122    /// Source identifier for import/export tracking
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub source_identifier_id: Option<String>,
125}
126
127impl StaticEdge {
128    /// Serialize to JSON value
129    pub fn to_json(&self) -> serde_json::Value {
130        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_node_is_root() {
140        let node = StaticNode {
141            nodeid: "123".to_string(),
142            name: "Root".to_string(),
143            alias: None,
144            datatype: "semantic".to_string(),
145            nodegroup_id: None,
146            graph_id: "graph1".to_string(),
147            is_collector: false,
148            isrequired: false,
149            exportable: true,
150            sortorder: None,
151            config: HashMap::new(),
152            parentproperty: None,
153            ontologyclass: None,
154            description: None,
155            fieldname: None,
156            hascustomalias: false,
157            issearchable: false,
158            istopnode: true,
159            sourcebranchpublication_id: None,
160            source_identifier_id: None,
161            is_immutable: None,
162        };
163        assert!(node.is_root());
164    }
165}