1use std::collections::HashMap;
23use std::io::Write;
24
25use crate::query::resolver::{classify_inline_literal, write_iri_term, write_object_term};
26use crate::NQuin;
27
28pub fn serialize_to_ntriples<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
30 crate::resolver::format_ntriples_to(quins, writer)
31 .map_err(|e| format!("Failed to write N-Triples: {e}"))
32}
33
34fn group_by<'a>(quins: &'a [NQuin], key: impl Fn(&NQuin) -> u64) -> Vec<(u64, Vec<&'a NQuin>)> {
37 let mut order: Vec<u64> = Vec::new();
38 let mut map: HashMap<u64, Vec<&NQuin>> = HashMap::new();
39 for quin in quins {
40 let k = key(quin);
41 let bucket = map.entry(k).or_default();
42 if bucket.is_empty() {
43 order.push(k);
44 }
45 bucket.push(quin);
46 }
47 order
48 .into_iter()
49 .map(|k| {
50 let v = map.remove(&k).unwrap_or_default();
51 (k, v)
52 })
53 .collect()
54}
55
56fn write_subject_block<W: Write>(
60 writer: &mut W,
61 subject: u64,
62 rows: &[&NQuin],
63 indent: &str,
64) -> std::io::Result<()> {
65 write!(writer, "{indent}")?;
66 write_iri_term(subject, writer)?;
67 for (i, quin) in rows.iter().enumerate() {
68 if i == 0 {
69 write!(writer, " ")?;
70 } else {
71 write!(writer, " ;\n{indent} ")?;
72 }
73 write_iri_term(quin.predicate, writer)?;
74 write!(writer, " ")?;
75 write_object_term(quin.object, writer)?;
76 }
77 writeln!(writer, " .")
78}
79
80pub fn serialize_to_turtle<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
82 for (subject, rows) in group_by(quins, |q| q.subject) {
83 write_subject_block(writer, subject, &rows, "")
84 .map_err(|e| format!("Failed to write Turtle: {e}"))?;
85 }
86 Ok(())
87}
88
89pub fn serialize_to_nquads<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
91 crate::resolver::format_nquads_to(quins, writer)
92 .map_err(|e| format!("Failed to write N-Quads: {e}"))
93}
94
95pub fn serialize_to_trig<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
97 for (context, ctx_quins) in group_by(quins, |q| q.context) {
98 write!(writer, "").map_err(|e| format!("Failed to write TriG: {e}"))?;
99 write_iri_term(context, writer).map_err(|e| format!("Failed to write TriG graph: {e}"))?;
100 writeln!(writer, " {{").map_err(|e| format!("Failed to write TriG graph: {e}"))?;
101
102 let owned: Vec<NQuin> = ctx_quins.iter().map(|q| **q).collect();
104 for (subject, rows) in group_by(&owned, |q| q.subject) {
105 write_subject_block(writer, subject, &rows, " ")
106 .map_err(|e| format!("Failed to write TriG statement: {e}"))?;
107 }
108
109 writeln!(writer, "}}").map_err(|e| format!("Failed to write TriG graph end: {e}"))?;
110 }
111 Ok(())
112}
113
114pub fn serialize_to_n3<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
119 for (subject, rows) in group_by(quins, |q| q.subject) {
120 write_subject_block(writer, subject, &rows, "")
121 .map_err(|e| format!("Failed to write N3: {e}"))?;
122 }
123 Ok(())
124}
125
126enum JsonLdTerm {
128 Iri(String),
129 Literal { value: String, datatype: String },
130}
131
132fn jsonld_term(hash: u64) -> JsonLdTerm {
135 if let Some(bytes) = crate::resolver::resolve_hash(hash) {
136 return JsonLdTerm::Iri(String::from_utf8_lossy(bytes).into_owned());
137 }
138 if (hash & crate::resolver::MSB_FLAG) != 0 {
139 return JsonLdTerm::Iri(format!(
140 "did:q42:ptr/{:016x}",
141 hash & !crate::resolver::MSB_FLAG
142 ));
143 }
144 if let Some(lit) = classify_inline_literal(hash) {
145 return JsonLdTerm::Literal {
146 value: lit.to_string(),
147 datatype: lit.datatype_iri().to_string(),
148 };
149 }
150 JsonLdTerm::Iri(format!("quin:hash/{hash:016x}"))
151}
152
153fn json_escape(s: &str) -> String {
155 let mut out = String::with_capacity(s.len() + 2);
156 for c in s.chars() {
157 match c {
158 '"' => out.push_str("\\\""),
159 '\\' => out.push_str("\\\\"),
160 '\n' => out.push_str("\\n"),
161 '\r' => out.push_str("\\r"),
162 '\t' => out.push_str("\\t"),
163 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
164 c => out.push(c),
165 }
166 }
167 out
168}
169
170pub fn serialize_to_jsonld<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
175 let err = |e: std::io::Error| format!("Failed to write JSON-LD: {e}");
176 writeln!(writer, "[").map_err(err)?;
177
178 let subjects = group_by(quins, |q| q.subject);
179 for (si, (subject, rows)) in subjects.iter().enumerate() {
180 if si > 0 {
181 writeln!(writer, ",").map_err(err)?;
182 }
183 let subj_iri = match jsonld_term(*subject) {
184 JsonLdTerm::Iri(s) => s,
185 JsonLdTerm::Literal { value, .. } => value,
189 };
190 writeln!(writer, " {{").map_err(err)?;
191 write!(writer, " \"@id\": \"{}\"", json_escape(&subj_iri)).map_err(err)?;
192
193 let owned: Vec<NQuin> = rows.iter().map(|q| **q).collect();
195 let by_pred = group_by(&owned, |q| q.predicate);
196 for (predicate, pred_rows) in &by_pred {
197 let pred_iri = match jsonld_term(*predicate) {
198 JsonLdTerm::Iri(s) => s,
199 JsonLdTerm::Literal { value, .. } => value,
200 };
201 writeln!(writer, ",").map_err(err)?;
202 writeln!(writer, " \"{}\": [", json_escape(&pred_iri)).map_err(err)?;
203 for (oi, quin) in pred_rows.iter().enumerate() {
204 if oi > 0 {
205 writeln!(writer, ",").map_err(err)?;
206 }
207 match jsonld_term(quin.object) {
208 JsonLdTerm::Iri(iri) => {
209 write!(writer, " {{ \"@id\": \"{}\" }}", json_escape(&iri))
210 .map_err(err)?;
211 }
212 JsonLdTerm::Literal { value, datatype } => {
213 write!(
214 writer,
215 " {{ \"@value\": \"{}\", \"@type\": \"{}\" }}",
216 json_escape(&value),
217 json_escape(&datatype)
218 )
219 .map_err(err)?;
220 }
221 }
222 }
223 write!(writer, "\n ]").map_err(err)?;
224 }
225 write!(writer, "\n }}").map_err(err)?;
226 }
227
228 writeln!(writer, "\n]").map_err(err)?;
229 Ok(())
230}
231
232pub fn serialize_to_cborld<W: Write>(writer: &mut W, quins: &[NQuin]) -> Result<(), String> {
249 use ciborium::value::Value;
250
251 let term_string = |h: u64| -> String {
252 match jsonld_term(h) {
253 JsonLdTerm::Iri(s) => s,
254 JsonLdTerm::Literal { value, .. } => value,
255 }
256 };
257
258 let mut arr: Vec<Value> = Vec::with_capacity(quins.len());
259 for q in quins {
260 arr.push(Value::Map(vec![
261 (
262 Value::Text("@id".to_string()),
263 Value::Text(term_string(q.subject)),
264 ),
265 (
266 Value::Text(term_string(q.predicate)),
267 Value::Text(term_string(q.object)),
268 ),
269 ]));
270 }
271
272 ciborium::ser::into_writer(&Value::Array(arr), writer)
273 .map_err(|e| format!("Failed to write CBOR-LD: {e}"))
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::query::resolver::{INLINE_TAG_INTEGER, MSB_FLAG};
280
281 fn s(quins: &[NQuin], f: impl Fn(&mut Vec<u8>, &[NQuin]) -> Result<(), String>) -> String {
282 let mut buf = Vec::new();
283 f(&mut buf, quins).unwrap();
284 String::from_utf8(buf).unwrap()
285 }
286
287 fn quin(subject: u64, predicate: u64, object: u64) -> NQuin {
289 NQuin {
290 subject,
291 predicate,
292 object,
293 context: 0,
294 metadata: 0,
295 parity: 0,
296 }
297 }
298
299 #[test]
300 fn turtle_is_valid_and_groups_by_subject() {
301 let s1 = MSB_FLAG | 0x11;
303 let quins = [
304 quin(s1, MSB_FLAG | 0x22, MSB_FLAG | 0x33),
305 quin(s1, MSB_FLAG | 0x44, MSB_FLAG | 0x55),
306 ];
307 let out = s(&quins, serialize_to_turtle);
308 assert_eq!(out.matches(" .\n").count(), 1, "one '.' per subject: {out}");
310 assert!(out.contains(" ;\n"), "predicate list uses ';': {out}");
312 assert!(
314 !out.lines().next().unwrap().trim_end().ends_with("> ."),
315 "subject must not be terminated alone: {out}"
316 );
317 }
318
319 #[test]
320 fn turtle_object_integer_is_typed_literal_not_iri() {
321 let quins = [quin(
322 MSB_FLAG | 0x11,
323 MSB_FLAG | 0x22,
324 INLINE_TAG_INTEGER | 42,
325 )];
326 let out = s(&quins, serialize_to_turtle);
327 assert!(
328 out.contains(r#""42"^^<"#),
329 "integer object as typed literal: {out}"
330 );
331 assert!(out.contains("XMLSchema#integer"), "{out}");
332 }
333
334 #[test]
335 fn jsonld_literal_object_uses_value_and_type() {
336 let quins = [quin(
337 MSB_FLAG | 0x11,
338 MSB_FLAG | 0x22,
339 INLINE_TAG_INTEGER | 7,
340 )];
341 let out = s(&quins, serialize_to_jsonld);
342 assert!(out.contains(r#""@value": "7""#), "{out}");
343 assert!(out.contains(r#""@type""#), "{out}");
344 assert!(out.contains("XMLSchema#integer"), "{out}");
345 }
346
347 #[test]
348 fn jsonld_iri_object_uses_id() {
349 let quins = [quin(MSB_FLAG | 0x11, MSB_FLAG | 0x22, MSB_FLAG | 0x33)];
350 let out = s(&quins, serialize_to_jsonld);
351 assert!(out.contains(r#""@id""#), "{out}");
352 assert!(out.contains("did:q42:ptr/"), "{out}");
354 }
355
356 #[test]
357 fn trig_wraps_statements_in_graph_braces() {
358 let quins = [quin(MSB_FLAG | 0x11, MSB_FLAG | 0x22, MSB_FLAG | 0x33)];
359 let out = s(&quins, serialize_to_trig);
360 assert!(out.contains(" {\n"), "graph opens with '{{': {out}");
361 assert!(
362 out.trim_end().ends_with('}'),
363 "graph closes with '}}': {out}"
364 );
365 }
366
367 #[test]
368 fn cborld_emits_decodable_cbor_array_of_maps() {
369 let quins = [
371 quin(MSB_FLAG | 0x11, MSB_FLAG | 0x22, MSB_FLAG | 0x33),
372 quin(MSB_FLAG | 0x44, MSB_FLAG | 0x55, INLINE_TAG_INTEGER | 9),
373 ];
374 let mut buf = Vec::new();
375 serialize_to_cborld(&mut buf, &quins).unwrap();
376 assert_eq!(buf[0], 0x82, "expected CBOR array(2), got {:#x}", buf[0]);
378 let val: ciborium::value::Value = ciborium::de::from_reader(&buf[..]).unwrap();
380 match val {
381 ciborium::value::Value::Array(a) => {
382 assert_eq!(a.len(), 2, "one map per triple");
383 match &a[0] {
384 ciborium::value::Value::Map(m) => assert_eq!(m.len(), 2, "@id + one predicate"),
385 other => panic!("expected map, got {other:?}"),
386 }
387 }
388 other => panic!("expected array, got {other:?}"),
389 }
390 }
391}