1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Default, Serialize, PartialEq)]
35pub struct EdgeMetadata {
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub relationship_type: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub weight: Option<f64>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub weights: Option<HashMap<String, f64>>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub properties: Option<HashMap<String, serde_json::Value>>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub edge_text: Option<String>,
56}
57
58impl EdgeMetadata {
59 pub fn new(
62 relationship_type: Option<String>,
63 weight: Option<f64>,
64 edge_text: Option<String>,
65 ) -> Self {
66 let effective_edge_text = edge_text.or_else(|| relationship_type.clone());
67 Self {
68 relationship_type,
69 weight,
70 weights: None,
71 properties: None,
72 edge_text: effective_edge_text,
73 }
74 }
75
76 pub fn with_all(
78 relationship_type: Option<String>,
79 weight: Option<f64>,
80 weights: Option<HashMap<String, f64>>,
81 properties: Option<HashMap<String, serde_json::Value>>,
82 edge_text: Option<String>,
83 ) -> Self {
84 let effective_edge_text = edge_text.or_else(|| relationship_type.clone());
85 Self {
86 relationship_type,
87 weight,
88 weights,
89 properties,
90 edge_text: effective_edge_text,
91 }
92 }
93
94 pub fn ensure_edge_text(&mut self) {
99 if self.edge_text.is_none() {
100 self.edge_text.clone_from(&self.relationship_type);
101 }
102 }
103}
104
105impl<'de> Deserialize<'de> for EdgeMetadata {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: serde::Deserializer<'de>,
113 {
114 #[derive(Deserialize)]
116 struct EdgeMetadataRaw {
117 relationship_type: Option<String>,
118 weight: Option<f64>,
119 weights: Option<HashMap<String, f64>>,
120 properties: Option<HashMap<String, serde_json::Value>>,
121 edge_text: Option<String>,
122 }
123
124 let raw = EdgeMetadataRaw::deserialize(deserializer)?;
125
126 let effective_edge_text = raw.edge_text.or_else(|| raw.relationship_type.clone());
128
129 Ok(EdgeMetadata {
130 relationship_type: raw.relationship_type,
131 weight: raw.weight,
132 weights: raw.weights,
133 properties: raw.properties,
134 edge_text: effective_edge_text,
135 })
136 }
137}
138
139#[cfg(test)]
140#[allow(
141 clippy::unwrap_used,
142 clippy::expect_used,
143 reason = "test code — panics are acceptable failures"
144)]
145mod tests {
146 use super::*;
147 use serde_json::json;
148
149 #[test]
150 fn test_new_auto_populates_edge_text() {
151 let edge = EdgeMetadata::new(Some("contains".into()), None, None);
152 assert_eq!(edge.relationship_type.as_deref(), Some("contains"));
153 assert_eq!(edge.edge_text.as_deref(), Some("contains"));
154 }
155
156 #[test]
157 fn test_new_explicit_edge_text_preserved() {
158 let edge = EdgeMetadata::new(
159 Some("contains".into()),
160 Some(0.5),
161 Some("custom text".into()),
162 );
163 assert_eq!(edge.relationship_type.as_deref(), Some("contains"));
164 assert_eq!(edge.weight, Some(0.5));
165 assert_eq!(edge.edge_text.as_deref(), Some("custom text"));
166 }
167
168 #[test]
169 fn test_new_no_relationship_type_no_edge_text() {
170 let edge = EdgeMetadata::new(None, Some(1.0), None);
171 assert_eq!(edge.relationship_type, None);
172 assert_eq!(edge.edge_text, None);
173 assert_eq!(edge.weight, Some(1.0));
174 }
175
176 #[test]
177 fn test_ensure_edge_text_populates_when_none() {
178 let mut edge = EdgeMetadata {
179 relationship_type: Some("works_at".into()),
180 edge_text: None,
181 ..Default::default()
182 };
183 edge.ensure_edge_text();
184 assert_eq!(edge.edge_text.as_deref(), Some("works_at"));
185 }
186
187 #[test]
188 fn test_ensure_edge_text_preserves_existing() {
189 let mut edge = EdgeMetadata {
190 relationship_type: Some("works_at".into()),
191 edge_text: Some("already set".into()),
192 ..Default::default()
193 };
194 edge.ensure_edge_text();
195 assert_eq!(edge.edge_text.as_deref(), Some("already set"));
196 }
197
198 #[test]
199 fn test_ensure_edge_text_both_none() {
200 let mut edge = EdgeMetadata::default();
201 edge.ensure_edge_text();
202 assert_eq!(edge.edge_text, None);
203 }
204
205 #[test]
206 fn test_default_all_none() {
207 let edge = EdgeMetadata::default();
208 assert_eq!(edge.relationship_type, None);
209 assert_eq!(edge.weight, None);
210 assert_eq!(edge.weights, None);
211 assert_eq!(edge.properties, None);
212 assert_eq!(edge.edge_text, None);
213 }
214
215 #[test]
216 fn test_with_all_fields() {
217 let mut weights = HashMap::new();
218 weights.insert("strength".into(), 0.8);
219 weights.insert("confidence".into(), 0.9);
220
221 let mut properties = HashMap::new();
222 properties.insert("source".into(), json!("manual"));
223
224 let edge = EdgeMetadata::with_all(
225 Some("contains".into()),
226 Some(0.5),
227 Some(weights.clone()),
228 Some(properties.clone()),
229 Some("custom text".into()),
230 );
231
232 assert_eq!(edge.relationship_type.as_deref(), Some("contains"));
233 assert_eq!(edge.weight, Some(0.5));
234 assert_eq!(edge.weights.as_ref().unwrap().get("strength"), Some(&0.8));
235 assert_eq!(edge.weights.as_ref().unwrap().get("confidence"), Some(&0.9));
236 assert_eq!(
237 edge.properties.as_ref().unwrap().get("source"),
238 Some(&json!("manual"))
239 );
240 assert_eq!(edge.edge_text.as_deref(), Some("custom text"));
241 }
242
243 #[test]
244 fn test_with_all_auto_populates_edge_text() {
245 let edge = EdgeMetadata::with_all(
246 Some("located_in".into()),
247 None,
248 None,
249 None,
250 None, );
252 assert_eq!(edge.edge_text.as_deref(), Some("located_in"));
253 }
254
255 #[test]
256 fn test_serialization_roundtrip() {
257 let edge = EdgeMetadata::new(Some("works_at".into()), Some(0.75), None);
258 let json = serde_json::to_string(&edge).unwrap();
259 let deserialized: EdgeMetadata = serde_json::from_str(&json).unwrap();
260
261 assert_eq!(edge, deserialized);
262 }
263
264 #[test]
265 fn test_serialization_skips_none_fields() {
266 let edge = EdgeMetadata::new(Some("works_at".into()), None, None);
267 let json_value: serde_json::Value = serde_json::to_value(&edge).unwrap();
268 let obj = json_value.as_object().unwrap();
269
270 assert!(obj.contains_key("relationship_type"));
271 assert!(obj.contains_key("edge_text"));
272 assert!(!obj.contains_key("weight"));
273 assert!(!obj.contains_key("weights"));
274 assert!(!obj.contains_key("properties"));
275 }
276
277 #[test]
278 fn test_deserialize_auto_populates_edge_text() {
279 let json = r#"{"relationship_type": "contains"}"#;
281 let edge: EdgeMetadata = serde_json::from_str(json).unwrap();
282
283 assert_eq!(edge.relationship_type.as_deref(), Some("contains"));
284 assert_eq!(edge.edge_text.as_deref(), Some("contains"));
285 }
286
287 #[test]
288 fn test_deserialize_explicit_edge_text_preserved() {
289 let json = r#"{"relationship_type": "contains", "edge_text": "custom"}"#;
290 let edge: EdgeMetadata = serde_json::from_str(json).unwrap();
291
292 assert_eq!(edge.relationship_type.as_deref(), Some("contains"));
293 assert_eq!(edge.edge_text.as_deref(), Some("custom"));
294 }
295
296 #[test]
297 fn test_deserialize_empty_json() {
298 let json = r#"{}"#;
299 let edge: EdgeMetadata = serde_json::from_str(json).unwrap();
300
301 assert_eq!(edge, EdgeMetadata::default());
302 }
303
304 #[test]
305 fn test_deserialize_with_weights() {
306 let json = r#"{
307 "relationship_type": "contains",
308 "weight": 0.5,
309 "weights": {"strength": 0.8, "confidence": 0.9}
310 }"#;
311 let edge: EdgeMetadata = serde_json::from_str(json).unwrap();
312
313 assert_eq!(edge.weight, Some(0.5));
314 let weights = edge.weights.as_ref().unwrap();
315 assert_eq!(weights.get("strength"), Some(&0.8));
316 assert_eq!(weights.get("confidence"), Some(&0.9));
317 assert_eq!(edge.edge_text.as_deref(), Some("contains"));
319 }
320
321 #[test]
322 fn test_deserialize_with_properties() {
323 let json = r#"{
324 "relationship_type": "works_at",
325 "properties": {"since": "2020", "role": "engineer", "active": true}
326 }"#;
327 let edge: EdgeMetadata = serde_json::from_str(json).unwrap();
328
329 let props = edge.properties.as_ref().unwrap();
330 assert_eq!(props.get("since"), Some(&json!("2020")));
331 assert_eq!(props.get("role"), Some(&json!("engineer")));
332 assert_eq!(props.get("active"), Some(&json!(true)));
333 }
334
335 #[test]
336 fn test_clone() {
337 let edge = EdgeMetadata::new(Some("contains".into()), Some(0.5), None);
338 let cloned = edge.clone();
339 assert_eq!(edge, cloned);
340 }
341
342 #[test]
343 fn test_debug_format() {
344 let edge = EdgeMetadata::new(Some("contains".into()), None, None);
345 let debug = format!("{edge:?}");
346 assert!(debug.contains("EdgeMetadata"));
347 assert!(debug.contains("contains"));
348 }
349}