Skip to main content

qualia_core_db/sparql_library/parsers/
json_parser.rs

1//! JSON Parser for QualiaDB
2//!
3//! Zero-allocation JSON parser that streams data directly into NQuin format.
4//! Supports common data types with proper inline type tagging.
5
6use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc};
7use serde_json::{Deserializer, Value};
8use std::io::Read;
9
10use crate::mini_parser::hash_token;
11use crate::NQuin;
12
13/// Data type for JSON field mapping
14#[derive(Debug, Clone, Copy)]
15pub enum JsonDatatype {
16    Integer,
17    Float,
18    DateTime,
19    StringRef,
20}
21
22/// Field mapping configuration
23#[derive(Debug, Clone)]
24pub struct JsonFieldMapping {
25    pub source_key: String,
26    pub predicate_hash: u64,
27    pub datatype: JsonDatatype,
28}
29
30/// JSON parsing configuration
31#[derive(Debug, Clone)]
32pub struct JsonMappingProfile {
33    pub base_class_hash: u64,
34    pub fields: Vec<JsonFieldMapping>,
35}
36
37/// Parse JSON data from a reader and stream Quins
38pub fn parse_json_to_quins<R: Read>(
39    reader: R,
40    profile: &JsonMappingProfile,
41    mut on_quin: impl FnMut(NQuin),
42) -> Result<(), String> {
43    let stream = Deserializer::from_reader(reader).into_iter::<Value>();
44
45    let mut emit_object = |obj: &serde_json::Map<String, Value>| {
46        let subject_hash: u64 = rand::random(); // Ephemeral Subject ID for this entity
47
48        for field in &profile.fields {
49            if let Some(val) = obj.get(&field.source_key) {
50                match field.datatype {
51                    JsonDatatype::Integer => {
52                        let parsed_int: u64 = val.as_u64().unwrap_or(0);
53                        let quin = NQuin {
54                            subject: subject_hash,
55                            predicate: field.predicate_hash,
56                            object: parsed_int | (0b001 << 60), // INLINE_TAG_INTEGER
57                            context: 0,
58                            metadata: 0,
59                            parity: NQuin::calculate_parity(
60                                subject_hash,
61                                field.predicate_hash,
62                                parsed_int | (0b001 << 60),
63                                0,
64                                0,
65                            ),
66                        };
67                        on_quin(quin);
68                    }
69                    JsonDatatype::Float => {
70                        let parsed_float: f32 = val.as_f64().unwrap_or(0.0) as f32;
71                        let float_bits: u32 = parsed_float.to_bits();
72                        let inline_tag: u64 = 0b010 << 60;
73                        let packed_object = inline_tag | (float_bits as u64);
74
75                        let quin = NQuin {
76                            subject: subject_hash,
77                            predicate: field.predicate_hash,
78                            object: packed_object,
79                            context: 0,
80                            metadata: 0,
81                            parity: NQuin::calculate_parity(
82                                subject_hash,
83                                field.predicate_hash,
84                                packed_object,
85                                0,
86                                0,
87                            ),
88                        };
89                        on_quin(quin);
90                    }
91                    JsonDatatype::StringRef => {
92                        if let Some(s) = val.as_str() {
93                            let hash = hash_token(s);
94                            let quin = NQuin {
95                                subject: subject_hash,
96                                predicate: field.predicate_hash,
97                                object: hash,
98                                context: 0,
99                                metadata: 0,
100                                parity: NQuin::calculate_parity(
101                                    subject_hash,
102                                    field.predicate_hash,
103                                    hash,
104                                    0,
105                                    0,
106                                ),
107                            };
108                            on_quin(quin);
109                        }
110                    }
111                    JsonDatatype::DateTime => {
112                        let s = match val.as_str() {
113                            Some(s) => s,
114                            None => continue,
115                        };
116                        let millis: u64 = parse_datetime_millis(s).unwrap_or(0);
117                        let quin = NQuin {
118                            subject: subject_hash,
119                            predicate: field.predicate_hash,
120                            object: (0b011u64 << 60) | millis,
121                            context: 0,
122                            metadata: 0,
123                            parity: NQuin::calculate_parity(
124                                subject_hash,
125                                field.predicate_hash,
126                                (0b011u64 << 60) | millis,
127                                0,
128                                0,
129                            ),
130                        };
131                        on_quin(quin);
132                    }
133                }
134            }
135        }
136    };
137
138    for value in stream {
139        match value {
140            Ok(Value::Object(obj)) => emit_object(&obj),
141            Ok(Value::Array(entries)) => {
142                for entry in entries {
143                    if let Value::Object(obj) = entry {
144                        emit_object(&obj);
145                    }
146                }
147            }
148            _ => {}
149        }
150    }
151
152    Ok(())
153}
154
155/// Try parsing common datetime string formats into Unix milliseconds.
156fn parse_datetime_millis(s: &str) -> Option<u64> {
157    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
158        return Some(dt.timestamp_millis() as u64);
159    }
160    for fmt in &[
161        "%Y-%m-%dT%H:%M:%S",
162        "%Y-%m-%d %H:%M:%S",
163        "%Y-%m-%dT%H:%M:%SZ",
164    ] {
165        if let Ok(nd) = NaiveDateTime::parse_from_str(s, fmt) {
166            return Some(Utc.from_utc_datetime(&nd).timestamp_millis() as u64);
167        }
168    }
169    if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
170        let nd = d.and_hms_opt(0, 0, 0)?;
171        return Some(Utc.from_utc_datetime(&nd).timestamp_millis() as u64);
172    }
173    None
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{parse_json_to_quins, JsonDatatype, JsonFieldMapping, JsonMappingProfile};
179
180    #[test]
181    fn parses_each_object_in_a_top_level_array() {
182        let profile = JsonMappingProfile {
183            base_class_hash: 0,
184            fields: vec![JsonFieldMapping {
185                source_key: "name".to_owned(),
186                predicate_hash: 42,
187                datatype: JsonDatatype::StringRef,
188            }],
189        };
190        let mut quins = Vec::new();
191
192        parse_json_to_quins(
193            br#"[{"name":"Alice"},{"name":"Bob"}]"#.as_slice(),
194            &profile,
195            |quin| quins.push(quin),
196        )
197        .expect("top-level arrays containing objects are valid JSON mappings");
198
199        assert_eq!(quins.len(), 2);
200        assert!(quins.iter().all(|quin| quin.predicate == 42));
201    }
202}