Skip to main content

hyle_source_qmap/
lib.rs

1use hyle::{ModelResult, Row, Source, Value};
2use indexmap::IndexMap;
3use qmap::Qmap;
4use std::ffi::CStr;
5use libc::c_char;
6use serde::{Serialize, Deserialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum FieldType {
10    String = 0,
11    Int = 1,
12    Bool = 2,
13    NullableString = 3,
14    Reference = 4,
15    MultiReference = 5,
16}
17
18pub struct FieldDef {
19    pub name: String,
20    pub field_type: FieldType,
21    pub target_dataset: Option<String>,
22    pub inverse_name: Option<String>,
23}
24
25pub struct DatasetDef {
26    pub id: String,
27    pub fields: Vec<FieldDef>,
28    pub source_hd: u32,
29}
30
31#[derive(Serialize, Deserialize, Debug)]
32pub struct RelationItem {
33    pub id: String,
34    #[serde(flatten)]
35    pub inverse: IndexMap<String, Vec<String>>,
36}
37
38#[derive(Serialize, Deserialize, Debug)]
39pub struct Relation {
40    pub target: String,
41    pub inverse: Option<String>,
42    pub items: Vec<RelationItem>,
43}
44
45/// Builds a Hyle Source directly from qmap, assuming the qmap value is a JSON object string.
46/// This matches the current behavior of common_dataset.c while we transition.
47pub fn build_source_from_json_qmap(qmap: &Qmap, def: &DatasetDef) -> Source {
48    let mut rows = Vec::new();
49    let mut cursor = qmap.iter(std::ptr::null(), 0);
50
51    while let Some((_key_ptr, val_ptr)) = cursor.next() {
52        let c_str = unsafe { CStr::from_ptr(val_ptr as *const c_char) };
53        if let Ok(json_str) = c_str.to_str() {
54            if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(json_str) {
55                let mut row: Row = IndexMap::new();
56                for (k, v) in map {
57                    row.insert(k, json_to_hyle_value(v));
58                }
59                rows.push(row);
60            }
61        }
62    }
63
64    let mut source = Source::new();
65    source.insert(def.id.clone(), ModelResult::many(rows));
66    
67    // Build relations
68    let mut relations = IndexMap::new();
69    for field in &def.fields {
70        if let Some(target) = &field.target_dataset {
71            if let Some(rel) = build_relation(qmap, def, field, target) {
72                relations.insert(field.name.clone(), rel);
73            }
74        }
75    }
76    
77    if !relations.is_empty() {
78        if let Ok(rel_val) = serde_json::to_value(relations) {
79            source.insert("relations".to_string(), ModelResult::one(json_to_hyle_row(rel_val)));
80        }
81    }
82
83    source
84}
85
86fn build_relation(qmap: &Qmap, _def: &DatasetDef, field: &FieldDef, target: &str) -> Option<Relation> {
87    // We need to build the inverse mapping
88    // This replicates dataset_build_relations_json from C
89    
90    // 1. Collect all references from all rows
91    let mut inverse_map: IndexMap<String, Vec<String>> = IndexMap::new();
92    let mut cursor = qmap.iter(std::ptr::null(), 0);
93    while let Some((key_ptr, val_ptr)) = cursor.next() {
94        let id = unsafe { CStr::from_ptr(key_ptr as *const c_char) }.to_string_lossy().into_owned();
95        let row_json = unsafe { CStr::from_ptr(val_ptr as *const c_char) }.to_string_lossy();
96        
97        if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&row_json) {
98            if let Some(val) = map.get(&field.name) {
99                match val {
100                    serde_json::Value::String(s) => {
101                        if !s.is_empty() {
102                            inverse_map.entry(s.clone()).or_default().push(id);
103                        }
104                    }
105                    serde_json::Value::Array(arr) => {
106                        for v in arr {
107                            if let serde_json::Value::String(s) = v {
108                                if !s.is_empty() {
109                                    inverse_map.entry(s.clone()).or_default().push(id.clone());
110                                }
111                            }
112                        }
113                    }
114                    _ => {}
115                }
116            }
117        }
118    }
119    
120    if inverse_map.is_empty() {
121        return None;
122    }
123    
124    let mut items = Vec::new();
125    for (target_id, source_ids) in inverse_map {
126        let mut inverse = IndexMap::new();
127        if let Some(inv_name) = &field.inverse_name {
128            inverse.insert(inv_name.clone(), source_ids);
129        }
130        items.push(RelationItem {
131            id: target_id,
132            inverse,
133        });
134    }
135    
136    Some(Relation {
137        target: target.to_string(),
138        inverse: field.inverse_name.clone(),
139        items,
140    })
141}
142
143fn json_to_hyle_value(v: serde_json::Value) -> Value {
144    match v {
145        serde_json::Value::String(s) => Value::String(s),
146        serde_json::Value::Number(n) => Value::String(n.to_string()),
147        serde_json::Value::Bool(b) => Value::String(b.to_string()),
148        serde_json::Value::Array(arr) => {
149            Value::Array(arr.into_iter().map(json_to_hyle_value).collect())
150        }
151        serde_json::Value::Null => Value::String(String::new()),
152        serde_json::Value::Object(obj) => Value::String(serde_json::to_string(&obj).unwrap_or_default()),
153    }
154}
155
156fn json_to_hyle_row(v: serde_json::Value) -> Row {
157    if let serde_json::Value::Object(map) = v {
158        let mut row = IndexMap::new();
159        for (k, val) in map {
160            row.insert(k, json_to_hyle_value(val));
161        }
162        row
163    } else {
164        IndexMap::new()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_build_source_from_json_qmap() {
174        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
175        q.put_str("song1", "{\"id\":\"song1\", \"title\":\"Song One\", \"type\":\"Rock\"}");
176        q.put_str("song2", "{\"id\":\"song2\", \"title\":\"Song Two\", \"type\":[\"Jazz\",\"Blues\"]}");
177
178        let def = DatasetDef {
179            id: "song.items".to_string(),
180            fields: vec![
181                FieldDef {
182                    name: "id".to_string(),
183                    field_type: FieldType::String,
184                    target_dataset: None,
185                    inverse_name: None,
186                },
187                FieldDef {
188                    name: "title".to_string(),
189                    field_type: FieldType::String,
190                    target_dataset: None,
191                    inverse_name: None,
192                },
193                FieldDef {
194                    name: "type".to_string(),
195                    field_type: FieldType::MultiReference,
196                    target_dataset: Some("song.types".to_string()),
197                    inverse_name: Some("songs".to_string()),
198                },
199            ],
200            source_hd: q.handle(),
201        };
202
203        let source = build_source_from_json_qmap(&q, &def);
204        assert!(source.contains_key("song.items"));
205        assert!(source.contains_key("relations"));
206
207        let result = source.get("song.items").unwrap();
208        let rows = result.rows();
209        assert_eq!(rows.len(), 2);
210
211        let song2 = rows.iter().find(|r| r.get("id") == Some(&Value::String("song2".to_string()))).unwrap();
212        assert_eq!(song2.get("type"), Some(&Value::Array(vec![Value::String("Jazz".to_string()), Value::String("Blues".to_string())])));
213
214        let rel_result = source.get("relations").unwrap();
215        let rel_row = &rel_result.rows()[0];
216        let type_rel_str = match rel_row.get("type").unwrap() {
217            Value::String(s) => s,
218            _ => panic!("Expected string JSON for relation"),
219        };
220        let type_rel: Relation = serde_json::from_str(type_rel_str).unwrap();
221        assert_eq!(type_rel.target, "song.types");
222        assert_eq!(type_rel.items.len(), 3); // Rock, Jazz, Blues
223    }
224
225    #[test]
226    fn test_single_reference() {
227        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
228        q.put_str("song1", "{\"id\":\"song1\", \"author\":\"author1\"}");
229        q.put_str("song2", "{\"id\":\"song2\", \"author\":\"author1\"}");
230
231        let def = DatasetDef {
232            id: "song.items".to_string(),
233            fields: vec![
234                FieldDef {
235                    name: "id".to_string(),
236                    field_type: FieldType::String,
237                    target_dataset: None,
238                    inverse_name: None,
239                },
240                FieldDef {
241                    name: "author".to_string(),
242                    field_type: FieldType::Reference,
243                    target_dataset: Some("author.items".to_string()),
244                    inverse_name: Some("songs".to_string()),
245                },
246            ],
247            source_hd: q.handle(),
248        };
249
250        let source = build_source_from_json_qmap(&q, &def);
251        let rel_result = source.get("relations").unwrap();
252        let rel_row = &rel_result.rows()[0];
253        let author_rel_str = match rel_row.get("author").unwrap() {
254            Value::String(s) => s,
255            _ => panic!("Expected string JSON for relation"),
256        };
257        let author_rel: Relation = serde_json::from_str(author_rel_str).unwrap();
258        assert_eq!(author_rel.target, "author.items");
259        assert_eq!(author_rel.items.len(), 1); // Only author1
260        assert_eq!(author_rel.items[0].id, "author1");
261        assert_eq!(author_rel.items[0].inverse.get("songs").unwrap(), &vec!["song1".to_string(), "song2".to_string()]);
262    }
263
264    #[test]
265    fn test_primitive_types() {
266        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
267        q.put_str("item1", "{\"id\":\"item1\", \"count\":42, \"active\":true, \"price\":3.14}");
268
269        let def = DatasetDef {
270            id: "items".to_string(),
271            fields: vec![
272                FieldDef { name: "id".to_string(), field_type: FieldType::String, target_dataset: None, inverse_name: None },
273                FieldDef { name: "count".to_string(), field_type: FieldType::Int, target_dataset: None, inverse_name: None },
274                FieldDef { name: "active".to_string(), field_type: FieldType::Bool, target_dataset: None, inverse_name: None },
275            ],
276            source_hd: q.handle(),
277        };
278
279        let source = build_source_from_json_qmap(&q, &def);
280        let result = source.get("items").unwrap();
281        let row = &result.rows()[0];
282        assert_eq!(row.get("count"), Some(&Value::String("42".to_string())));
283        assert_eq!(row.get("active"), Some(&Value::String("true".to_string())));
284        assert_eq!(row.get("price"), Some(&Value::String("3.14".to_string())));
285    }
286
287    #[test]
288    fn test_null_and_empty_values() {
289        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
290        q.put_str("item1", "{\"id\":\"item1\", \"ref\":null, \"multi_ref\":[], \"str\":\"\"}");
291        q.put_str("item2", "{\"id\":\"item2\", \"multi_ref\":[\"\"]}");
292
293        let def = DatasetDef {
294            id: "items".to_string(),
295            fields: vec![
296                FieldDef { name: "ref".to_string(), field_type: FieldType::Reference, target_dataset: Some("target".to_string()), inverse_name: None },
297                FieldDef { name: "multi_ref".to_string(), field_type: FieldType::MultiReference, target_dataset: Some("target".to_string()), inverse_name: None },
298            ],
299            source_hd: q.handle(),
300        };
301
302        let source = build_source_from_json_qmap(&q, &def);
303        let result = source.get("items").unwrap();
304        let rows = result.rows();
305        let item1 = rows.iter().find(|r| r.get("id") == Some(&Value::String("item1".to_string()))).unwrap();
306        assert_eq!(item1.get("ref"), Some(&Value::String("".to_string())));
307        assert_eq!(item1.get("str"), Some(&Value::String("".to_string())));
308        
309        assert!(!source.contains_key("relations"));
310    }
311
312    #[test]
313    fn test_multiple_relations() {
314        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
315        q.put_str("item1", "{\"id\":\"item1\", \"ref1\":\"targetA1\", \"ref2\":\"targetB1\"}");
316
317        let def = DatasetDef {
318            id: "items".to_string(),
319            fields: vec![
320                FieldDef { name: "ref1".to_string(), field_type: FieldType::Reference, target_dataset: Some("targetA".to_string()), inverse_name: None },
321                FieldDef { name: "ref2".to_string(), field_type: FieldType::Reference, target_dataset: Some("targetB".to_string()), inverse_name: None },
322            ],
323            source_hd: q.handle(),
324        };
325
326        let source = build_source_from_json_qmap(&q, &def);
327        let rel_result = source.get("relations").unwrap();
328        let rel_row = &rel_result.rows()[0];
329        
330        assert!(rel_row.contains_key("ref1"));
331        assert!(rel_row.contains_key("ref2"));
332    }
333
334    #[test]
335    fn test_fault_tolerance() {
336        let q = Qmap::open(None, None, 2, 2, 0xFF, 0).unwrap();
337        q.put_str("item1", "{\"id\":\"item1\"}");
338        q.put_str("item2", "INVALID JSON");
339        q.put_str("item3", "{\"id\":\"item3\"}");
340
341        let def = DatasetDef {
342            id: "items".to_string(),
343            fields: vec![],
344            source_hd: q.handle(),
345        };
346
347        let source = build_source_from_json_qmap(&q, &def);
348        let result = source.get("items").unwrap();
349        assert_eq!(result.rows().len(), 2);
350        assert_eq!(result.rows()[0].get("id"), Some(&Value::String("item1".to_string())));
351        assert_eq!(result.rows()[1].get("id"), Some(&Value::String("item3".to_string())));
352    }
353}