Skip to main content

datasynth_core/models/
graph_properties.rs

1//! Graph property mapping trait and types for node export.
2//!
3//! Provides a `ToNodeProperties` trait that each model struct implements
4//! to map typed Rust fields to camelCase graph properties expected by
5//! downstream consumers (e.g. AssureTwin).
6
7use chrono::NaiveDate;
8use rust_decimal::Decimal;
9use std::collections::HashMap;
10
11/// Property value for graph node export.
12///
13/// Mirrors `datasynth-graph` `NodeProperty` but lives in `datasynth-core`
14/// to avoid circular dependencies.
15#[derive(Debug, Clone, PartialEq)]
16pub enum GraphPropertyValue {
17    String(String),
18    Int(i64),
19    Float(f64),
20    Decimal(Decimal),
21    Bool(bool),
22    Date(NaiveDate),
23    StringList(Vec<String>),
24}
25
26impl GraphPropertyValue {
27    /// Convert any variant to a string representation.
28    pub fn to_string_value(&self) -> String {
29        match self {
30            Self::String(s) => s.clone(),
31            Self::Int(i) => i.to_string(),
32            Self::Float(f) => format!("{:.6}", f),
33            Self::Decimal(d) => d.to_string(),
34            Self::Bool(b) => b.to_string(),
35            Self::Date(d) => d.to_string(),
36            Self::StringList(v) => v.join(";"),
37        }
38    }
39
40    /// Try to extract a string reference.
41    pub fn as_str(&self) -> Option<&str> {
42        match self {
43            Self::String(s) => Some(s),
44            _ => None,
45        }
46    }
47
48    /// Try to extract a bool value.
49    pub fn as_bool(&self) -> Option<bool> {
50        match self {
51            Self::Bool(b) => Some(*b),
52            _ => None,
53        }
54    }
55
56    /// Try to extract a Decimal value.
57    pub fn as_decimal(&self) -> Option<Decimal> {
58        match self {
59            Self::Decimal(d) => Some(*d),
60            _ => None,
61        }
62    }
63
64    /// Try to extract an i64 value.
65    pub fn as_int(&self) -> Option<i64> {
66        match self {
67            Self::Int(i) => Some(*i),
68            _ => None,
69        }
70    }
71
72    /// Try to extract an f64 value.
73    pub fn as_float(&self) -> Option<f64> {
74        match self {
75            Self::Float(f) => Some(*f),
76            _ => None,
77        }
78    }
79
80    /// Try to extract a date value.
81    pub fn as_date(&self) -> Option<NaiveDate> {
82        match self {
83            Self::Date(d) => Some(*d),
84            _ => None,
85        }
86    }
87}
88
89/// Trait for converting typed model structs to graph node property maps.
90///
91/// Implementations map struct fields to camelCase property keys matching
92/// downstream consumer (AssureTwin) DTO expectations.
93pub trait ToNodeProperties {
94    /// Entity type name (snake_case), e.g. `"uncertain_tax_position"`.
95    fn node_type_name(&self) -> &'static str;
96
97    /// Numeric entity type code for registry, e.g. `416`.
98    fn node_type_code(&self) -> u16;
99
100    /// Convert all fields to a property map with camelCase keys.
101    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue>;
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_graph_property_value_to_string() {
110        assert_eq!(GraphPropertyValue::Bool(true).to_string_value(), "true");
111        assert_eq!(GraphPropertyValue::Bool(false).to_string_value(), "false");
112        assert_eq!(GraphPropertyValue::Int(42).to_string_value(), "42");
113        assert_eq!(GraphPropertyValue::Int(-7).to_string_value(), "-7");
114        assert_eq!(
115            GraphPropertyValue::String("hello".into()).to_string_value(),
116            "hello"
117        );
118        assert_eq!(
119            GraphPropertyValue::Float(3.14).to_string_value(),
120            "3.140000"
121        );
122        assert_eq!(
123            GraphPropertyValue::Decimal(Decimal::new(1234, 2)).to_string_value(),
124            "12.34"
125        );
126        assert_eq!(
127            GraphPropertyValue::Date(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
128                .to_string_value(),
129            "2024-01-15"
130        );
131        assert_eq!(
132            GraphPropertyValue::StringList(vec!["a".into(), "b".into(), "c".into()])
133                .to_string_value(),
134            "a;b;c"
135        );
136    }
137
138    #[test]
139    fn test_accessor_methods() {
140        assert_eq!(
141            GraphPropertyValue::String("test".into()).as_str(),
142            Some("test")
143        );
144        assert_eq!(GraphPropertyValue::Int(42).as_str(), None);
145        assert_eq!(GraphPropertyValue::Bool(true).as_bool(), Some(true));
146        assert_eq!(GraphPropertyValue::String("x".into()).as_bool(), None);
147        assert_eq!(
148            GraphPropertyValue::Decimal(Decimal::new(100, 0)).as_decimal(),
149            Some(Decimal::new(100, 0))
150        );
151        assert_eq!(GraphPropertyValue::Bool(true).as_decimal(), None);
152        assert_eq!(GraphPropertyValue::Int(99).as_int(), Some(99));
153        assert_eq!(GraphPropertyValue::Float(1.5).as_float(), Some(1.5));
154        let d = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
155        assert_eq!(GraphPropertyValue::Date(d).as_date(), Some(d));
156    }
157
158    #[test]
159    fn test_empty_string_list() {
160        assert_eq!(GraphPropertyValue::StringList(vec![]).to_string_value(), "");
161    }
162}