1use std::borrow::Cow;
2
3use ahash::AHashMap;
4
5pub type RecordId = u64;
6pub type FieldName = String;
7
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
10pub enum FieldValue {
11 Text(String),
12 Int(i64),
13 UInt(u64),
14 Float(f64),
15 Bool(bool),
16 Bytes(Vec<u8>),
17 Null,
18}
19
20impl From<String> for FieldValue {
21 fn from(s: String) -> Self { FieldValue::Text(s) }
22}
23impl From<&str> for FieldValue {
24 fn from(s: &str) -> Self { FieldValue::Text(s.to_owned()) }
25}
26impl From<i64> for FieldValue {
27 fn from(i: i64) -> Self { FieldValue::Int(i) }
28}
29impl From<i32> for FieldValue {
30 fn from(i: i32) -> Self { FieldValue::Int(i as i64) }
31}
32impl From<u64> for FieldValue {
33 fn from(u: u64) -> Self { FieldValue::UInt(u) }
34}
35impl From<Vec<u8>> for FieldValue {
36 fn from(b: Vec<u8>) -> Self { FieldValue::Bytes(b) }
37}
38impl From<u32> for FieldValue {
39 fn from(u: u32) -> Self { FieldValue::UInt(u as u64) }
40}
41impl From<f64> for FieldValue {
42 fn from(f: f64) -> Self { FieldValue::Float(f) }
43}
44impl From<f32> for FieldValue {
45 fn from(f: f32) -> Self { FieldValue::Float(f as f64) }
46}
47impl From<bool> for FieldValue {
48 fn from(b: bool) -> Self { FieldValue::Bool(b) }
49}
50impl<T: Into<FieldValue>> From<Option<T>> for FieldValue {
51 fn from(opt: Option<T>) -> Self {
52 match opt {
53 Some(v) => v.into(),
54 None => FieldValue::Null,
55 }
56 }
57}
58
59#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
61pub struct Record {
62 pub id: RecordId,
63 pub fields: AHashMap<FieldName, FieldValue>,
64 pub source: Option<String>,
65}
66
67impl Record {
68 pub fn new(id: RecordId) -> Self {
69 Self { id, fields: AHashMap::new(), source: None }
70 }
71
72 pub fn with_source(mut self, source: impl Into<String>) -> Self {
73 self.source = Some(source.into());
74 self
75 }
76
77 pub fn insert(mut self, name: impl Into<String>, value: impl Into<FieldValue>) -> Self {
78 self.fields.insert(name.into(), value.into());
79 self
80 }
81
82 pub fn get(&self, name: &str) -> Option<&FieldValue> {
83 self.fields.get(name)
84 }
85
86 pub fn text(&self, name: &str) -> Option<&str> {
87 match self.fields.get(name) {
88 Some(FieldValue::Text(s)) => Some(s.as_str()),
89 _ => None,
90 }
91 }
92
93 pub fn field_as_str(&self, name: &str) -> Option<Cow<'_, str>> {
95 match self.fields.get(name)? {
96 FieldValue::Text(s) => Some(Cow::Borrowed(s.as_str())),
97 FieldValue::Int(i) => Some(Cow::Owned(i.to_string())),
98 FieldValue::UInt(u) => Some(Cow::Owned(u.to_string())),
99 FieldValue::Float(f) => Some(Cow::Owned(f.to_string())),
100 FieldValue::Bool(b) => Some(Cow::Owned(b.to_string())),
101 FieldValue::Bytes(_) => None,
102 FieldValue::Null => None,
103 }
104 }
105
106 pub fn field_as<T: FromFieldValue>(&self, name: &str) -> Option<T> {
115 self.fields.get(name).and_then(T::from_field_value)
116 }
117}
118
119pub trait FromFieldValue: Sized {
121 fn from_field_value(v: &FieldValue) -> Option<Self>;
122}
123
124impl FromFieldValue for f64 {
125 fn from_field_value(v: &FieldValue) -> Option<Self> {
126 match v {
127 FieldValue::Float(f) => Some(*f),
128 FieldValue::Int(i) => Some(*i as f64),
129 FieldValue::UInt(u) => Some(*u as f64),
130 FieldValue::Text(s) => s.parse::<f64>().ok(),
132 _ => None,
133 }
134 }
135}
136
137impl FromFieldValue for f32 {
138 fn from_field_value(v: &FieldValue) -> Option<Self> {
139 match v {
140 FieldValue::Float(f) => Some(*f as f32),
141 FieldValue::Int(i) => Some(*i as f32),
142 FieldValue::UInt(u) => Some(*u as f32),
143 FieldValue::Text(s) => s.parse::<f32>().ok(),
144 _ => None,
145 }
146 }
147}
148
149impl FromFieldValue for i64 {
150 fn from_field_value(v: &FieldValue) -> Option<Self> {
151 match v {
152 FieldValue::Int(i) => Some(*i),
153 FieldValue::UInt(u) => i64::try_from(*u).ok(),
154 FieldValue::Text(s) => s.parse::<i64>().ok(),
155 _ => None,
156 }
157 }
158}
159
160impl FromFieldValue for i32 {
161 fn from_field_value(v: &FieldValue) -> Option<Self> {
162 match v {
163 FieldValue::Int(i) => i32::try_from(*i).ok(),
164 FieldValue::UInt(u) => i32::try_from(*u).ok(),
165 FieldValue::Text(s) => s.parse::<i32>().ok(),
166 _ => None,
167 }
168 }
169}
170
171impl FromFieldValue for u64 {
172 fn from_field_value(v: &FieldValue) -> Option<Self> {
173 match v {
174 FieldValue::UInt(u) => Some(*u),
175 FieldValue::Int(i) => u64::try_from(*i).ok(),
176 FieldValue::Text(s) => s.parse::<u64>().ok(),
177 _ => None,
178 }
179 }
180}
181
182impl FromFieldValue for u32 {
183 fn from_field_value(v: &FieldValue) -> Option<Self> {
184 match v {
185 FieldValue::UInt(u) => u32::try_from(*u).ok(),
186 FieldValue::Int(i) => u32::try_from(*i).ok(),
187 FieldValue::Text(s) => s.parse::<u32>().ok(),
188 _ => None,
189 }
190 }
191}
192
193impl FromFieldValue for bool {
194 fn from_field_value(v: &FieldValue) -> Option<Self> {
195 match v {
196 FieldValue::Bool(b) => Some(*b),
197 _ => None,
198 }
199 }
200}
201
202impl FromFieldValue for String {
203 fn from_field_value(v: &FieldValue) -> Option<Self> {
204 match v {
205 FieldValue::Text(s) => Some(s.clone()),
206 FieldValue::Int(i) => Some(i.to_string()),
207 FieldValue::UInt(u) => Some(u.to_string()),
208 FieldValue::Float(f) => Some(f.to_string()),
209 FieldValue::Bool(b) => Some(b.to_string()),
210 FieldValue::Bytes(_) | FieldValue::Null => None,
211 }
212 }
213}
214
215impl FromFieldValue for Vec<u8> {
216 fn from_field_value(v: &FieldValue) -> Option<Self> {
217 match v {
218 FieldValue::Bytes(b) => Some(b.clone()),
219 FieldValue::Text(s) => Some(s.as_bytes().to_vec()),
220 _ => None,
221 }
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn field_value_equality() {
231 assert_eq!(FieldValue::Text("hello".into()), FieldValue::Text("hello".into()));
232 assert_ne!(FieldValue::Int(1), FieldValue::Int(2));
233 assert_eq!(FieldValue::Null, FieldValue::Null);
234 }
235
236 #[test]
237 fn record_builder_chain() {
238 let r = Record::new(42)
239 .with_source("kvk")
240 .insert("name", "Alice")
241 .insert("age", 30i64);
242
243 assert_eq!(r.id, 42);
244 assert_eq!(r.source.as_deref(), Some("kvk"));
245 assert_eq!(r.text("name"), Some("Alice"));
246 assert_eq!(r.get("age"), Some(&FieldValue::Int(30)));
247 assert_eq!(r.get("missing"), None);
248 }
249
250 #[test]
251 fn field_as_str_coerces_scalars() {
252 let r = Record::new(1)
253 .insert("phone", 5551234567i64)
254 .insert("lat", 52.345f64)
255 .insert("active", true)
256 .insert("name", "Alice")
257 .insert("empty", FieldValue::Null);
258
259 assert_eq!(r.field_as_str("phone").as_deref(), Some("5551234567"));
260 assert_eq!(r.field_as_str("lat").as_deref(), Some("52.345"));
261 assert_eq!(r.field_as_str("active").as_deref(), Some("true"));
262 assert_eq!(r.field_as_str("name").as_deref(), Some("Alice"));
263 assert_eq!(r.field_as_str("empty"), None);
264 assert_eq!(r.field_as_str("missing"), None);
265 }
266
267 #[test]
268 fn from_impls_roundtrip() {
269 assert_eq!(FieldValue::from("hello"), FieldValue::Text("hello".into()));
270 assert_eq!(FieldValue::from(42i64), FieldValue::Int(42));
271 assert_eq!(FieldValue::from(3.14f64), FieldValue::Float(3.14));
272 assert_eq!(FieldValue::from(true), FieldValue::Bool(true));
273 assert_eq!(FieldValue::from(Some("hi")), FieldValue::Text("hi".into()));
274 assert_eq!(FieldValue::from(None::<&str>), FieldValue::Null);
275 assert_eq!(FieldValue::from(u64::MAX), FieldValue::UInt(u64::MAX));
277 assert_eq!(FieldValue::from(vec![1u8, 2, 3]), FieldValue::Bytes(vec![1, 2, 3]));
279 }
280
281 #[test]
282 fn field_as_str_new_variants() {
283 let r = Record::new(1)
284 .insert("count", 42u64)
285 .insert("data", FieldValue::Bytes(vec![0xff]));
286 assert_eq!(r.field_as_str("count").as_deref(), Some("42"));
287 assert_eq!(r.field_as_str("data"), None);
288 }
289
290 #[test]
291 fn field_as_typed() {
292 let r = Record::new(1)
293 .insert("lat", 52.37f64)
294 .insert("count", 10u64)
295 .insert("age", 30i64)
296 .insert("active", true)
297 .insert("name", "Alice")
298 .insert("blob", FieldValue::Bytes(vec![1, 2, 3]));
299
300 assert_eq!(r.field_as::<f64>("lat"), Some(52.37));
301 assert_eq!(r.field_as::<f32>("lat"), Some(52.37f32));
302 assert_eq!(r.field_as::<u64>("count"), Some(10u64));
303 assert_eq!(r.field_as::<i64>("count"), Some(10i64));
304 assert_eq!(r.field_as::<i64>("age"), Some(30i64));
305 assert_eq!(r.field_as::<bool>("active"), Some(true));
306 assert_eq!(r.field_as::<String>("name"), Some("Alice".to_string()));
307 assert_eq!(r.field_as::<Vec<u8>>("blob"), Some(vec![1u8, 2, 3]));
308 assert_eq!(r.field_as::<f64>("missing"), None);
309 }
310
311 #[test]
312 fn field_as_cross_variant_coercions() {
313 let r = Record::new(1)
314 .insert("int_val", 100i64)
315 .insert("uint_val", 200u64);
316
317 assert_eq!(r.field_as::<f64>("int_val"), Some(100.0));
319 assert_eq!(r.field_as::<f64>("uint_val"), Some(200.0));
321 assert_eq!(r.field_as::<i64>("uint_val"), Some(200i64));
323 assert_eq!(r.field_as::<u64>("int_val"), Some(100u64));
325
326 let r2 = Record::new(2).insert("neg", -1i64);
328 assert_eq!(r2.field_as::<u64>("neg"), None);
329 }
330}