datasynth_core/models/
graph_properties.rs1use chrono::NaiveDate;
8use rust_decimal::Decimal;
9use std::collections::HashMap;
10
11#[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 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!("{f:.6}"),
33 Self::Decimal(d) => d.to_string(),
34 Self::Bool(b) => b.to_string(),
35 Self::Date(d) => format!("{d}T00:00:00Z"),
36 Self::StringList(v) => v.join(";"),
37 }
38 }
39
40 pub fn as_str(&self) -> Option<&str> {
42 match self {
43 Self::String(s) => Some(s),
44 _ => None,
45 }
46 }
47
48 pub fn as_bool(&self) -> Option<bool> {
50 match self {
51 Self::Bool(b) => Some(*b),
52 _ => None,
53 }
54 }
55
56 pub fn as_decimal(&self) -> Option<Decimal> {
58 match self {
59 Self::Decimal(d) => Some(*d),
60 _ => None,
61 }
62 }
63
64 pub fn as_int(&self) -> Option<i64> {
66 match self {
67 Self::Int(i) => Some(*i),
68 _ => None,
69 }
70 }
71
72 pub fn as_float(&self) -> Option<f64> {
74 match self {
75 Self::Float(f) => Some(*f),
76 _ => None,
77 }
78 }
79
80 pub fn as_date(&self) -> Option<NaiveDate> {
82 match self {
83 Self::Date(d) => Some(*d),
84 _ => None,
85 }
86 }
87}
88
89pub fn camel_to_snake(s: &str) -> String {
93 let mut result = String::with_capacity(s.len() + 4);
94 let chars: Vec<char> = s.chars().collect();
95 for (i, &c) in chars.iter().enumerate() {
96 if c.is_uppercase() {
97 if i > 0 {
101 let prev_lower = chars[i - 1].is_lowercase();
102 let next_lower = chars.get(i + 1).is_some_and(|nc| nc.is_lowercase());
103 if prev_lower || (next_lower && chars[i - 1].is_uppercase()) {
104 result.push('_');
105 }
106 }
107 result.push(c.to_lowercase().next().unwrap_or(c));
108 } else {
109 result.push(c);
110 }
111 }
112 result
113}
114
115pub trait ToNodeProperties {
120 fn node_type_name(&self) -> &'static str;
122
123 fn node_type_code(&self) -> u16;
125
126 fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue>;
128}
129
130#[cfg(test)]
131#[allow(clippy::unwrap_used, clippy::approx_constant)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_graph_property_value_to_string() {
137 assert_eq!(GraphPropertyValue::Bool(true).to_string_value(), "true");
138 assert_eq!(GraphPropertyValue::Bool(false).to_string_value(), "false");
139 assert_eq!(GraphPropertyValue::Int(42).to_string_value(), "42");
140 assert_eq!(GraphPropertyValue::Int(-7).to_string_value(), "-7");
141 assert_eq!(
142 GraphPropertyValue::String("hello".into()).to_string_value(),
143 "hello"
144 );
145 assert_eq!(
146 GraphPropertyValue::Float(3.14).to_string_value(),
147 "3.140000"
148 );
149 assert_eq!(
150 GraphPropertyValue::Decimal(Decimal::new(1234, 2)).to_string_value(),
151 "12.34"
152 );
153 assert_eq!(
154 GraphPropertyValue::Date(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap())
155 .to_string_value(),
156 "2024-01-15T00:00:00Z"
157 );
158 assert_eq!(
159 GraphPropertyValue::StringList(vec!["a".into(), "b".into(), "c".into()])
160 .to_string_value(),
161 "a;b;c"
162 );
163 }
164
165 #[test]
166 fn test_accessor_methods() {
167 assert_eq!(
168 GraphPropertyValue::String("test".into()).as_str(),
169 Some("test")
170 );
171 assert_eq!(GraphPropertyValue::Int(42).as_str(), None);
172 assert_eq!(GraphPropertyValue::Bool(true).as_bool(), Some(true));
173 assert_eq!(GraphPropertyValue::String("x".into()).as_bool(), None);
174 assert_eq!(
175 GraphPropertyValue::Decimal(Decimal::new(100, 0)).as_decimal(),
176 Some(Decimal::new(100, 0))
177 );
178 assert_eq!(GraphPropertyValue::Bool(true).as_decimal(), None);
179 assert_eq!(GraphPropertyValue::Int(99).as_int(), Some(99));
180 assert_eq!(GraphPropertyValue::Float(1.5).as_float(), Some(1.5));
181 let d = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
182 assert_eq!(GraphPropertyValue::Date(d).as_date(), Some(d));
183 }
184
185 #[test]
186 fn test_empty_string_list() {
187 assert_eq!(GraphPropertyValue::StringList(vec![]).to_string_value(), "");
188 }
189
190 #[test]
191 fn test_date_rfc3339_format() {
192 let d = NaiveDate::from_ymd_opt(2024, 12, 1).unwrap();
193 assert_eq!(
194 GraphPropertyValue::Date(d).to_string_value(),
195 "2024-12-01T00:00:00Z"
196 );
197 }
198
199 #[test]
200 fn test_camel_to_snake_basic() {
201 assert_eq!(super::camel_to_snake("CosoComponent"), "coso_component");
202 assert_eq!(super::camel_to_snake("InternalControl"), "internal_control");
203 assert_eq!(super::camel_to_snake("Account"), "account");
204 assert_eq!(super::camel_to_snake("JournalEntry"), "journal_entry");
205 assert_eq!(super::camel_to_snake("PurchaseOrder"), "purchase_order");
206 assert_eq!(super::camel_to_snake("SoxAssertion"), "sox_assertion");
207 }
208
209 #[test]
210 fn test_camel_to_snake_consecutive_uppercase() {
211 assert_eq!(super::camel_to_snake("P2PPool"), "p2p_pool");
212 assert_eq!(super::camel_to_snake("O2CPool"), "o2c_pool");
213 assert_eq!(super::camel_to_snake("BankTransaction"), "bank_transaction");
214 }
215
216 #[test]
217 fn test_camel_to_snake_already_snake() {
218 assert_eq!(super::camel_to_snake("already_snake"), "already_snake");
219 assert_eq!(super::camel_to_snake("vendor"), "vendor");
220 }
221
222 #[test]
223 fn test_camel_to_snake_single_word() {
224 assert_eq!(super::camel_to_snake("Vendor"), "vendor");
225 assert_eq!(super::camel_to_snake("Employee"), "employee");
226 }
227}