khive_vcs_adapters/
record.rs1use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EntityRecord {
14 pub id: Uuid,
15 pub kind: String,
16 pub name: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub description: Option<String>,
19 #[serde(default)]
20 pub properties: serde_json::Value,
21 #[serde(default)]
22 pub tags: Vec<String>,
23}
24
25#[derive(Deserialize)]
27struct EdgeRecordRaw {
28 edge_id: Uuid,
29 source: String,
30 target: String,
31 relation: String,
32 #[serde(default = "default_weight")]
33 weight: f64,
34 #[serde(default)]
35 properties: serde_json::Value,
36}
37
38impl TryFrom<EdgeRecordRaw> for EdgeRecord {
39 type Error = String;
40
41 fn try_from(raw: EdgeRecordRaw) -> Result<Self, Self::Error> {
42 if !raw.weight.is_finite() {
43 return Err(format!(
44 "EdgeRecord: weight must be finite, got {}",
45 raw.weight
46 ));
47 }
48 if !(0.0..=1.0).contains(&raw.weight) {
49 return Err(format!(
50 "EdgeRecord: weight must be in [0.0, 1.0], got {}",
51 raw.weight
52 ));
53 }
54 Ok(Self {
55 edge_id: raw.edge_id,
56 source: raw.source,
57 target: raw.target,
58 relation: raw.relation,
59 weight: raw.weight,
60 properties: raw.properties,
61 })
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(try_from = "EdgeRecordRaw")]
68pub struct EdgeRecord {
69 pub edge_id: Uuid,
70 pub source: String,
71 pub target: String,
72 pub relation: String,
73 #[serde(default = "default_weight")]
74 pub weight: f64,
75 #[serde(default)]
76 pub properties: serde_json::Value,
77}
78
79fn default_weight() -> f64 {
80 0.7
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use uuid::Uuid;
87
88 fn raw_with_weight(w: f64) -> EdgeRecordRaw {
89 EdgeRecordRaw {
90 edge_id: Uuid::nil(),
91 source: "aa".into(),
92 target: "bb".into(),
93 relation: "extends".into(),
94 weight: w,
95 properties: serde_json::Value::Null,
96 }
97 }
98
99 #[test]
100 fn edge_record_try_from_rejects_nan_weight() {
101 assert!(
102 EdgeRecord::try_from(raw_with_weight(f64::NAN)).is_err(),
103 "NaN weight must be rejected"
104 );
105 }
106
107 #[test]
108 fn edge_record_try_from_rejects_inf_weight() {
109 assert!(
110 EdgeRecord::try_from(raw_with_weight(f64::INFINITY)).is_err(),
111 "Inf weight must be rejected"
112 );
113 }
114
115 #[test]
116 fn edge_record_try_from_rejects_weight_below_range() {
117 assert!(
118 EdgeRecord::try_from(raw_with_weight(-0.1)).is_err(),
119 "weight -0.1 must be rejected (below [0.0, 1.0])"
120 );
121 }
122
123 #[test]
124 fn edge_record_try_from_rejects_weight_above_range() {
125 assert!(
126 EdgeRecord::try_from(raw_with_weight(1.1)).is_err(),
127 "weight 1.1 must be rejected (above [0.0, 1.0])"
128 );
129 }
130
131 #[test]
132 fn edge_record_serde_rejects_weight_below_range() {
133 let json = r#"{"edge_id":"00000000-0000-0000-0000-000000000000","source":"a","target":"b","relation":"extends","weight":-0.1}"#;
134 let result: Result<EdgeRecord, _> = serde_json::from_str(json);
135 assert!(
136 result.is_err(),
137 "serde must reject weight < 0.0 at the JSON boundary"
138 );
139 }
140
141 #[test]
142 fn edge_record_serde_rejects_weight_above_range() {
143 let json = r#"{"edge_id":"00000000-0000-0000-0000-000000000000","source":"a","target":"b","relation":"extends","weight":2.0}"#;
144 let result: Result<EdgeRecord, _> = serde_json::from_str(json);
145 assert!(
146 result.is_err(),
147 "serde must reject weight > 1.0 at the JSON boundary"
148 );
149 }
150
151 #[test]
152 fn edge_record_try_from_accepts_finite_weight() {
153 assert!(
154 EdgeRecord::try_from(raw_with_weight(0.7)).is_ok(),
155 "finite weight 0.7 must be accepted"
156 );
157 }
158
159 #[test]
160 fn edge_record_try_from_accepts_boundary_weights() {
161 assert!(
162 EdgeRecord::try_from(raw_with_weight(0.0)).is_ok(),
163 "weight 0.0 must be accepted (lower bound)"
164 );
165 assert!(
166 EdgeRecord::try_from(raw_with_weight(1.0)).is_ok(),
167 "weight 1.0 must be accepted (upper bound)"
168 );
169 }
170
171 #[test]
172 fn edge_record_serde_roundtrip_valid() {
173 let id = Uuid::new_v4();
174 let record = EdgeRecord {
175 edge_id: id,
176 source: "aa".into(),
177 target: "bb".into(),
178 relation: "extends".into(),
179 weight: 0.7,
180 properties: serde_json::Value::Null,
181 };
182 let json = serde_json::to_string(&record).unwrap();
183 let restored: EdgeRecord = serde_json::from_str(&json).unwrap();
184 assert_eq!(restored.edge_id, id);
185 assert!((restored.weight - 0.7).abs() < 1e-12);
186 }
187}