1use std::collections::{BTreeMap, HashMap};
9use std::fmt::Write as FmtWrite;
10
11const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
14const RDF_FIRST: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#first";
15const RDF_REST: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest";
16const RDF_NIL: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil";
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub enum RdfTerm {
23 Iri(String),
25 Literal {
27 value: String,
29 datatype: Option<String>,
31 language: Option<String>,
33 },
34 Blank(String),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct RdfTriple {
41 pub subject: RdfTerm,
43 pub predicate: RdfTerm,
45 pub object: RdfTerm,
47}
48
49impl RdfTriple {
50 pub fn new(subject: RdfTerm, predicate: RdfTerm, object: RdfTerm) -> Self {
52 Self {
53 subject,
54 predicate,
55 object,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct CompactConfig {
63 pub indent_size: usize,
65 pub max_line_width: usize,
67 pub use_a_shorthand: bool,
69 pub inline_blank_nodes: bool,
72 pub use_collection_syntax: bool,
74 pub sort_subjects: bool,
76}
77
78impl Default for CompactConfig {
79 fn default() -> Self {
80 Self {
81 indent_size: 2,
82 max_line_width: 80,
83 use_a_shorthand: true,
84 inline_blank_nodes: true,
85 use_collection_syntax: true,
86 sort_subjects: true,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Default)]
93pub struct SerializerStats {
94 pub triples_count: usize,
96 pub subjects_count: usize,
98 pub prefix_count: usize,
100 pub a_shorthand_count: usize,
102 pub inlined_blanks: usize,
104}
105
106pub struct CompactSerializer {
110 prefixes: BTreeMap<String, String>,
112 config: CompactConfig,
114}
115
116impl CompactSerializer {
117 pub fn new() -> Self {
119 Self {
120 prefixes: BTreeMap::new(),
121 config: CompactConfig::default(),
122 }
123 }
124
125 pub fn with_config(config: CompactConfig) -> Self {
127 Self {
128 prefixes: BTreeMap::new(),
129 config,
130 }
131 }
132
133 pub fn add_prefix(&mut self, prefix: impl Into<String>, namespace: impl Into<String>) {
135 self.prefixes.insert(prefix.into(), namespace.into());
136 }
137
138 pub fn serialize(&self, triples: &[RdfTriple]) -> (String, SerializerStats) {
140 let mut stats = SerializerStats {
141 triples_count: triples.len(),
142 ..Default::default()
143 };
144
145 let mut output = String::new();
146
147 for (prefix, ns) in &self.prefixes {
149 let _ = writeln!(output, "@prefix {prefix}: <{ns}> .");
150 stats.prefix_count += 1;
151 }
152 if !self.prefixes.is_empty() {
153 output.push('\n');
154 }
155
156 let grouped = self.group_by_subject(triples);
158 stats.subjects_count = grouped.len();
159
160 let blank_usage = self.blank_node_object_count(triples);
162
163 let inlineable: std::collections::HashSet<String> = if self.config.inline_blank_nodes {
166 blank_usage
167 .iter()
168 .filter(|&(id, &count)| {
169 count == 1 && grouped.contains_key(&RdfTerm::Blank(id.clone()))
170 })
171 .map(|(id, _)| id.clone())
172 .collect()
173 } else {
174 std::collections::HashSet::new()
175 };
176
177 let mut subjects: Vec<&RdfTerm> = grouped.keys().collect();
179 if self.config.sort_subjects {
180 subjects.sort();
181 }
182
183 let indent = " ".repeat(self.config.indent_size);
184
185 for (idx, subject) in subjects.iter().enumerate() {
186 if let RdfTerm::Blank(id) = subject {
188 if inlineable.contains(id.as_str()) {
189 continue;
190 }
191 }
192
193 let pred_obj_list = grouped.get(subject).map(|v| v.as_slice()).unwrap_or(&[]);
194
195 let subj_str = self.format_term(subject);
196 let _ = write!(output, "{subj_str}");
197
198 let pred_groups = self.group_by_predicate(pred_obj_list);
200 let preds: Vec<&RdfTerm> = {
201 let mut v: Vec<&RdfTerm> = pred_groups.keys().collect();
202 v.sort();
203 v
204 };
205
206 for (pi, pred) in preds.iter().enumerate() {
207 let pred_str = self.format_predicate(pred, &mut stats);
208
209 if pi == 0 {
210 let _ = write!(output, " {pred_str}");
211 } else {
212 let _ = write!(output, " ;\n{indent}{pred_str}");
213 }
214
215 let objects = pred_groups.get(pred).map(|v| v.as_slice()).unwrap_or(&[]);
216 for (oi, obj) in objects.iter().enumerate() {
217 let obj_str =
218 self.format_object(obj, &grouped, &inlineable, &indent, &mut stats);
219 if oi == 0 {
220 let _ = write!(output, " {obj_str}");
221 } else {
222 let _ = write!(output, " ,\n{indent}{indent}{obj_str}");
223 }
224 }
225 }
226
227 let _ = writeln!(output, " .");
228 if idx + 1 < subjects.len() {
229 output.push('\n');
230 }
231 }
232
233 (output, stats)
234 }
235
236 fn group_by_subject<'a>(
239 &self,
240 triples: &'a [RdfTriple],
241 ) -> BTreeMap<RdfTerm, Vec<(&'a RdfTerm, &'a RdfTerm)>> {
242 let mut map: BTreeMap<RdfTerm, Vec<(&'a RdfTerm, &'a RdfTerm)>> = BTreeMap::new();
243 for t in triples {
244 map.entry(t.subject.clone())
245 .or_default()
246 .push((&t.predicate, &t.object));
247 }
248 map
249 }
250
251 fn group_by_predicate<'a>(
252 &self,
253 pred_obj_list: &[(&'a RdfTerm, &'a RdfTerm)],
254 ) -> BTreeMap<RdfTerm, Vec<&'a RdfTerm>> {
255 let mut map: BTreeMap<RdfTerm, Vec<&'a RdfTerm>> = BTreeMap::new();
256 for &(pred, obj) in pred_obj_list {
257 map.entry(pred.clone()).or_default().push(obj);
258 }
259 map
260 }
261
262 fn blank_node_object_count(&self, triples: &[RdfTriple]) -> HashMap<String, usize> {
265 let mut counts: HashMap<String, usize> = HashMap::new();
266 for t in triples {
267 if let RdfTerm::Blank(id) = &t.object {
268 *counts.entry(id.clone()).or_insert(0) += 1;
269 }
270 }
271 counts
272 }
273
274 fn format_term(&self, term: &RdfTerm) -> String {
277 match term {
278 RdfTerm::Iri(iri) => self.compress_iri(iri),
279 RdfTerm::Literal {
280 value,
281 datatype,
282 language,
283 } => {
284 let mut s = format!("\"{}\"", Self::escape_turtle(value));
285 if let Some(lang) = language {
286 let _ = write!(s, "@{lang}");
287 } else if let Some(dt) = datatype {
288 let compressed = self.compress_iri(dt);
289 let _ = write!(s, "^^{compressed}");
290 }
291 s
292 }
293 RdfTerm::Blank(id) => format!("_:{id}"),
294 }
295 }
296
297 fn format_predicate(&self, pred: &RdfTerm, stats: &mut SerializerStats) -> String {
298 if let RdfTerm::Iri(iri) = pred {
299 if self.config.use_a_shorthand && iri == RDF_TYPE {
300 stats.a_shorthand_count += 1;
301 return "a".to_string();
302 }
303 }
304 self.format_term(pred)
305 }
306
307 fn format_object(
308 &self,
309 obj: &RdfTerm,
310 grouped: &BTreeMap<RdfTerm, Vec<(&RdfTerm, &RdfTerm)>>,
311 inlineable: &std::collections::HashSet<String>,
312 indent: &str,
313 stats: &mut SerializerStats,
314 ) -> String {
315 if let RdfTerm::Blank(id) = obj {
316 if self.config.inline_blank_nodes && inlineable.contains(id.as_str()) {
317 if let Some(pred_obj_list) = grouped.get(&RdfTerm::Blank(id.clone())) {
318 stats.inlined_blanks += 1;
319 return self.format_inline_blank(pred_obj_list, indent, stats);
320 }
321 }
322 }
323 self.format_term(obj)
324 }
325
326 fn format_inline_blank(
327 &self,
328 pred_obj_list: &[(&RdfTerm, &RdfTerm)],
329 indent: &str,
330 stats: &mut SerializerStats,
331 ) -> String {
332 if pred_obj_list.is_empty() {
333 return "[]".to_string();
334 }
335 let inner_indent = format!("{indent} ");
336 let mut s = String::from("[\n");
337 for (i, &(pred, obj)) in pred_obj_list.iter().enumerate() {
338 let pred_str = self.format_predicate(pred, stats);
339 let obj_str = self.format_term(obj);
340 let sep = if i + 1 < pred_obj_list.len() {
341 " ;"
342 } else {
343 ""
344 };
345 let _ = writeln!(s, "{inner_indent}{pred_str} {obj_str}{sep}");
346 }
347 let _ = write!(s, "{indent}]");
348 s
349 }
350
351 fn compress_iri(&self, iri: &str) -> String {
354 let mut best: Option<(&str, &str)> = None;
356 for (prefix, ns) in &self.prefixes {
357 if iri.starts_with(ns.as_str())
358 && (best.is_none() || ns.len() > best.map(|(_, n)| n.len()).unwrap_or(0))
359 {
360 best = Some((prefix.as_str(), ns.as_str()));
361 }
362 }
363 if let Some((prefix, ns)) = best {
364 let local = &iri[ns.len()..];
365 format!("{prefix}:{local}")
366 } else {
367 format!("<{iri}>")
368 }
369 }
370
371 fn escape_turtle(s: &str) -> String {
374 let mut out = String::with_capacity(s.len());
375 for c in s.chars() {
376 match c {
377 '\\' => out.push_str("\\\\"),
378 '"' => out.push_str("\\\""),
379 '\n' => out.push_str("\\n"),
380 '\r' => out.push_str("\\r"),
381 '\t' => out.push_str("\\t"),
382 _ => out.push(c),
383 }
384 }
385 out
386 }
387
388 pub fn detect_list(&self, head: &str, triples: &[RdfTriple]) -> Option<Vec<RdfTerm>> {
395 let mut by_subject: HashMap<String, Vec<(&RdfTerm, &RdfTerm)>> = HashMap::new();
396 for t in triples {
397 if let RdfTerm::Blank(id) = &t.subject {
398 by_subject
399 .entry(id.clone())
400 .or_default()
401 .push((&t.predicate, &t.object));
402 }
403 }
404
405 let mut items = Vec::new();
406 let mut current = head.to_string();
407
408 for _ in 0..10_000 {
409 let po = by_subject.get(¤t)?;
410
411 let first = po.iter().find_map(|(p, o)| {
412 if let RdfTerm::Iri(iri) = p {
413 if iri == RDF_FIRST {
414 return Some((*o).clone());
415 }
416 }
417 None
418 })?;
419
420 let rest = po.iter().find_map(|(p, o)| {
421 if let RdfTerm::Iri(iri) = p {
422 if iri == RDF_REST {
423 return Some((*o).clone());
424 }
425 }
426 None
427 })?;
428
429 items.push(first);
430
431 match &rest {
432 RdfTerm::Iri(iri) if iri == RDF_NIL => return Some(items),
433 RdfTerm::Blank(next_id) => {
434 current = next_id.clone();
435 }
436 _ => return None,
437 }
438 }
439
440 None
441 }
442
443 pub fn format_collection(&self, items: &[RdfTerm]) -> String {
445 let parts: Vec<String> = items.iter().map(|t| self.format_term(t)).collect();
446 format!("( {} )", parts.join(" "))
447 }
448
449 pub fn suggest_prefixes(
456 triples: &[RdfTriple],
457 max_prefixes: usize,
458 ) -> BTreeMap<String, String> {
459 let mut ns_count: HashMap<String, usize> = HashMap::new();
460
461 for t in triples {
462 for term in [&t.subject, &t.predicate, &t.object] {
463 if let RdfTerm::Iri(iri) = term {
464 if let Some(ns) = Self::extract_namespace(iri) {
465 *ns_count.entry(ns).or_insert(0) += 1;
466 }
467 }
468 }
469 }
470
471 let mut sorted: Vec<(String, usize)> = ns_count.into_iter().collect();
472 sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
473
474 let mut result = BTreeMap::new();
475 for (i, (ns, _)) in sorted.into_iter().take(max_prefixes).enumerate() {
476 let prefix = Self::derive_prefix_name(&ns, i);
477 result.insert(prefix, ns);
478 }
479 result
480 }
481
482 fn extract_namespace(iri: &str) -> Option<String> {
483 if let Some(pos) = iri.rfind('#') {
484 Some(iri[..=pos].to_string())
485 } else {
486 iri.rfind('/').map(|pos| iri[..=pos].to_string())
487 }
488 }
489
490 fn derive_prefix_name(ns: &str, idx: usize) -> String {
491 let stripped = ns.trim_end_matches('#').trim_end_matches('/');
493 if let Some(pos) = stripped.rfind('/') {
494 let candidate = &stripped[pos + 1..];
495 if !candidate.is_empty() && candidate.len() <= 10 {
496 return candidate.to_lowercase();
497 }
498 }
499 format!("ns{idx}")
500 }
501}
502
503impl Default for CompactSerializer {
504 fn default() -> Self {
505 Self::new()
506 }
507}
508
509#[cfg(test)]
514mod tests {
515 use super::*;
516
517 fn iri(s: &str) -> RdfTerm {
520 RdfTerm::Iri(s.to_string())
521 }
522
523 fn lit(s: &str) -> RdfTerm {
524 RdfTerm::Literal {
525 value: s.to_string(),
526 datatype: None,
527 language: None,
528 }
529 }
530
531 fn lit_lang(s: &str, lang: &str) -> RdfTerm {
532 RdfTerm::Literal {
533 value: s.to_string(),
534 datatype: None,
535 language: Some(lang.to_string()),
536 }
537 }
538
539 fn lit_typed(s: &str, dt: &str) -> RdfTerm {
540 RdfTerm::Literal {
541 value: s.to_string(),
542 datatype: Some(dt.to_string()),
543 language: None,
544 }
545 }
546
547 fn blank(s: &str) -> RdfTerm {
548 RdfTerm::Blank(s.to_string())
549 }
550
551 fn triple(s: RdfTerm, p: RdfTerm, o: RdfTerm) -> RdfTriple {
552 RdfTriple::new(s, p, o)
553 }
554
555 #[test]
558 fn test_subject_grouping() {
559 let triples = vec![
560 triple(iri("http://ex.org/s"), iri("http://ex.org/p1"), lit("a")),
561 triple(iri("http://ex.org/s"), iri("http://ex.org/p2"), lit("b")),
562 ];
563 let ser = CompactSerializer::new();
564 let (output, stats) = ser.serialize(&triples);
565 assert_eq!(stats.subjects_count, 1);
566 assert_eq!(output.matches(" .").count(), 1);
568 }
569
570 #[test]
571 fn test_multiple_subjects() {
572 let triples = vec![
573 triple(iri("http://ex.org/s1"), iri("http://ex.org/p"), lit("a")),
574 triple(iri("http://ex.org/s2"), iri("http://ex.org/p"), lit("b")),
575 ];
576 let ser = CompactSerializer::new();
577 let (output, stats) = ser.serialize(&triples);
578 assert_eq!(stats.subjects_count, 2);
579 assert_eq!(output.matches(" .").count(), 2);
580 }
581
582 #[test]
585 fn test_predicate_grouping_semicolons() {
586 let triples = vec![
587 triple(iri("http://ex.org/s"), iri("http://ex.org/p1"), lit("a")),
588 triple(iri("http://ex.org/s"), iri("http://ex.org/p2"), lit("b")),
589 ];
590 let ser = CompactSerializer::new();
591 let (output, _) = ser.serialize(&triples);
592 assert!(output.contains(";"));
593 }
594
595 #[test]
598 fn test_object_list_commas() {
599 let triples = vec![
600 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), lit("a")),
601 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), lit("b")),
602 ];
603 let ser = CompactSerializer::new();
604 let (output, _) = ser.serialize(&triples);
605 assert!(output.contains(","));
606 }
607
608 #[test]
611 fn test_prefix_compression() {
612 let mut ser = CompactSerializer::new();
613 ser.add_prefix("ex", "http://example.org/");
614 let triples = vec![triple(
615 iri("http://example.org/s"),
616 iri("http://example.org/p"),
617 lit("v"),
618 )];
619 let (output, _) = ser.serialize(&triples);
620 assert!(output.contains("@prefix ex: <http://example.org/> ."));
621 assert!(output.contains("ex:s"));
622 assert!(output.contains("ex:p"));
623 }
624
625 #[test]
626 fn test_no_prefix_full_iri() {
627 let ser = CompactSerializer::new();
628 let triples = vec![triple(
629 iri("http://example.org/s"),
630 iri("http://example.org/p"),
631 lit("v"),
632 )];
633 let (output, _) = ser.serialize(&triples);
634 assert!(output.contains("<http://example.org/s>"));
635 }
636
637 #[test]
638 fn test_suggest_prefixes() {
639 let triples = vec![
640 triple(
641 iri("http://example.org/s1"),
642 iri("http://example.org/p"),
643 lit("a"),
644 ),
645 triple(
646 iri("http://example.org/s2"),
647 iri("http://other.org/q"),
648 lit("b"),
649 ),
650 ];
651 let suggested = CompactSerializer::suggest_prefixes(&triples, 5);
652 assert!(!suggested.is_empty());
653 }
654
655 #[test]
658 fn test_a_shorthand() {
659 let ser = CompactSerializer::new();
660 let triples = vec![triple(
661 iri("http://example.org/s"),
662 iri(RDF_TYPE),
663 iri("http://example.org/MyClass"),
664 )];
665 let (output, stats) = ser.serialize(&triples);
666 assert!(output.contains(" a "));
667 assert_eq!(stats.a_shorthand_count, 1);
668 }
669
670 #[test]
671 fn test_no_a_shorthand_when_disabled() {
672 let config = CompactConfig {
673 use_a_shorthand: false,
674 ..Default::default()
675 };
676 let ser = CompactSerializer::with_config(config);
677 let triples = vec![triple(
678 iri("http://example.org/s"),
679 iri(RDF_TYPE),
680 iri("http://example.org/MyClass"),
681 )];
682 let (output, stats) = ser.serialize(&triples);
683 assert!(!output.contains(" a "));
684 assert_eq!(stats.a_shorthand_count, 0);
685 }
686
687 #[test]
690 fn test_blank_node_inline() {
691 let triples = vec![
692 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), blank("b0")),
693 triple(blank("b0"), iri("http://ex.org/name"), lit("Alice")),
694 ];
695 let ser = CompactSerializer::new();
696 let (output, stats) = ser.serialize(&triples);
697 assert!(output.contains("["), "Should contain inline blank node");
698 assert!(stats.inlined_blanks >= 1);
699 }
700
701 #[test]
702 fn test_blank_node_not_inlined_multiple_refs() {
703 let triples = vec![
704 triple(iri("http://ex.org/s1"), iri("http://ex.org/p"), blank("b0")),
705 triple(iri("http://ex.org/s2"), iri("http://ex.org/p"), blank("b0")),
706 triple(blank("b0"), iri("http://ex.org/name"), lit("Alice")),
707 ];
708 let ser = CompactSerializer::new();
709 let (output, stats) = ser.serialize(&triples);
710 assert_eq!(stats.inlined_blanks, 0);
712 assert!(output.contains("_:b0"));
713 }
714
715 #[test]
718 fn test_detect_list() {
719 let triples = vec![
720 triple(blank("l0"), iri(RDF_FIRST), lit("a")),
721 triple(blank("l0"), iri(RDF_REST), blank("l1")),
722 triple(blank("l1"), iri(RDF_FIRST), lit("b")),
723 triple(blank("l1"), iri(RDF_REST), iri(RDF_NIL)),
724 ];
725 let ser = CompactSerializer::new();
726 let list = ser.detect_list("l0", &triples);
727 assert!(list.is_some());
728 let items = list.expect("list should exist");
729 assert_eq!(items.len(), 2);
730 assert_eq!(items[0], lit("a"));
731 assert_eq!(items[1], lit("b"));
732 }
733
734 #[test]
735 fn test_detect_list_not_a_list() {
736 let triples = vec![triple(
737 blank("x"),
738 iri("http://ex.org/p"),
739 lit("not a list"),
740 )];
741 let ser = CompactSerializer::new();
742 let list = ser.detect_list("x", &triples);
743 assert!(list.is_none());
744 }
745
746 #[test]
747 fn test_format_collection() {
748 let ser = CompactSerializer::new();
749 let items = vec![lit("a"), lit("b"), lit("c")];
750 let output = ser.format_collection(&items);
751 assert_eq!(output, "( \"a\" \"b\" \"c\" )");
752 }
753
754 #[test]
757 fn test_custom_indent() {
758 let config = CompactConfig {
759 indent_size: 4,
760 ..Default::default()
761 };
762 let ser = CompactSerializer::with_config(config);
763 let triples = vec![
764 triple(iri("http://ex.org/s"), iri("http://ex.org/p1"), lit("a")),
765 triple(iri("http://ex.org/s"), iri("http://ex.org/p2"), lit("b")),
766 ];
767 let (output, _) = ser.serialize(&triples);
768 assert!(output.contains(" "));
770 }
771
772 #[test]
775 fn test_escape_special_chars() {
776 let ser = CompactSerializer::new();
777 let triples = vec![triple(
778 iri("http://ex.org/s"),
779 iri("http://ex.org/p"),
780 lit("hello\n\"world\\"),
781 )];
782 let (output, _) = ser.serialize(&triples);
783 assert!(output.contains("\\n"));
784 assert!(output.contains("\\\""));
785 assert!(output.contains("\\\\"));
786 }
787
788 #[test]
791 fn test_lang_literal() {
792 let ser = CompactSerializer::new();
793 let triples = vec![triple(
794 iri("http://ex.org/s"),
795 iri("http://ex.org/p"),
796 lit_lang("hello", "en"),
797 )];
798 let (output, _) = ser.serialize(&triples);
799 assert!(output.contains("\"hello\"@en"));
800 }
801
802 #[test]
805 fn test_typed_literal() {
806 let mut ser = CompactSerializer::new();
807 ser.add_prefix("xsd", "http://www.w3.org/2001/XMLSchema#");
808 let triples = vec![triple(
809 iri("http://ex.org/s"),
810 iri("http://ex.org/p"),
811 lit_typed("42", "http://www.w3.org/2001/XMLSchema#integer"),
812 )];
813 let (output, _) = ser.serialize(&triples);
814 assert!(output.contains("^^xsd:integer"));
815 }
816
817 #[test]
820 fn test_empty_graph() {
821 let ser = CompactSerializer::new();
822 let (output, stats) = ser.serialize(&[]);
823 assert!(output.is_empty() || output.trim().is_empty());
824 assert_eq!(stats.triples_count, 0);
825 }
826
827 #[test]
830 fn test_default_config() {
831 let c = CompactConfig::default();
832 assert_eq!(c.indent_size, 2);
833 assert_eq!(c.max_line_width, 80);
834 assert!(c.use_a_shorthand);
835 assert!(c.inline_blank_nodes);
836 assert!(c.use_collection_syntax);
837 assert!(c.sort_subjects);
838 }
839
840 #[test]
843 fn test_stats_triples_count() {
844 let ser = CompactSerializer::new();
845 let triples = vec![
846 triple(iri("http://ex.org/s1"), iri("http://ex.org/p"), lit("a")),
847 triple(iri("http://ex.org/s2"), iri("http://ex.org/p"), lit("b")),
848 triple(iri("http://ex.org/s3"), iri("http://ex.org/p"), lit("c")),
849 ];
850 let (_, stats) = ser.serialize(&triples);
851 assert_eq!(stats.triples_count, 3);
852 assert_eq!(stats.subjects_count, 3);
853 }
854
855 #[test]
856 fn test_stats_prefix_count() {
857 let mut ser = CompactSerializer::new();
858 ser.add_prefix("ex", "http://example.org/");
859 ser.add_prefix("foaf", "http://xmlns.com/foaf/0.1/");
860 let (_, stats) = ser.serialize(&[]);
861 assert_eq!(stats.prefix_count, 2);
862 }
863
864 #[test]
867 fn test_suggest_prefixes_respects_max() {
868 let triples = vec![
869 triple(iri("http://a.org/s"), iri("http://b.org/p"), lit("v")),
870 triple(iri("http://c.org/s"), iri("http://d.org/p"), lit("v")),
871 ];
872 let suggested = CompactSerializer::suggest_prefixes(&triples, 2);
873 assert!(suggested.len() <= 2);
874 }
875
876 #[test]
877 fn test_suggest_prefixes_empty() {
878 let suggested = CompactSerializer::suggest_prefixes(&[], 5);
879 assert!(suggested.is_empty());
880 }
881
882 #[test]
885 fn test_extract_namespace_hash() {
886 let ns = CompactSerializer::extract_namespace("http://ex.org/ns#term");
887 assert_eq!(ns, Some("http://ex.org/ns#".to_string()));
888 }
889
890 #[test]
891 fn test_extract_namespace_slash() {
892 let ns = CompactSerializer::extract_namespace("http://ex.org/ns/term");
893 assert_eq!(ns, Some("http://ex.org/ns/".to_string()));
894 }
895
896 #[test]
897 fn test_extract_namespace_none() {
898 let ns = CompactSerializer::extract_namespace("urn:simple");
899 assert!(ns.is_none());
901 }
902
903 #[test]
906 fn test_sort_subjects_enabled() {
907 let triples = vec![
908 triple(iri("http://z.org/s"), iri("http://ex.org/p"), lit("z")),
909 triple(iri("http://a.org/s"), iri("http://ex.org/p"), lit("a")),
910 ];
911 let ser = CompactSerializer::new();
912 let (output, _) = ser.serialize(&triples);
913 let a_pos = output.find("<http://a.org/s>").unwrap_or(usize::MAX);
914 let z_pos = output.find("<http://z.org/s>").unwrap_or(usize::MAX);
915 assert!(a_pos < z_pos, "Subjects should be sorted");
916 }
917
918 #[test]
919 fn test_sort_subjects_disabled() {
920 let config = CompactConfig {
921 sort_subjects: false,
922 ..Default::default()
923 };
924 let ser = CompactSerializer::with_config(config);
925 let triples = vec![triple(
926 iri("http://z.org/s"),
927 iri("http://ex.org/p"),
928 lit("z"),
929 )];
930 let (output, _) = ser.serialize(&triples);
931 assert!(output.contains("<http://z.org/s>"));
932 }
933
934 #[test]
937 fn test_default_serializer() {
938 let ser = CompactSerializer::default();
939 let (output, stats) = ser.serialize(&[triple(
940 iri("http://ex.org/s"),
941 iri("http://ex.org/p"),
942 lit("v"),
943 )]);
944 assert_eq!(stats.triples_count, 1);
945 assert!(!output.is_empty());
946 }
947
948 #[test]
951 fn test_longest_prefix_match() {
952 let mut ser = CompactSerializer::new();
953 ser.add_prefix("short", "http://ex.org/");
954 ser.add_prefix("long", "http://ex.org/ns/");
955 let triples = vec![triple(
956 iri("http://ex.org/ns/term"),
957 iri("http://ex.org/p"),
958 lit("v"),
959 )];
960 let (output, _) = ser.serialize(&triples);
961 assert!(output.contains("long:term"));
962 }
963
964 #[test]
967 fn test_blank_node_subject() {
968 let ser = CompactSerializer::new();
969 let triples = vec![triple(blank("b0"), iri("http://ex.org/p"), lit("v"))];
970 let (output, _) = ser.serialize(&triples);
971 assert!(output.contains("_:b0"));
972 }
973
974 #[test]
977 fn test_multiple_prefixes() {
978 let mut ser = CompactSerializer::new();
979 ser.add_prefix("ex", "http://example.org/");
980 ser.add_prefix("foaf", "http://xmlns.com/foaf/0.1/");
981 ser.add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
982 let triples = vec![triple(
983 iri("http://example.org/alice"),
984 iri("http://xmlns.com/foaf/0.1/name"),
985 lit("Alice"),
986 )];
987 let (output, _) = ser.serialize(&triples);
988 assert!(output.contains("ex:alice"));
989 assert!(output.contains("foaf:name"));
990 }
991
992 #[test]
993 fn test_three_predicates_same_subject() {
994 let ser = CompactSerializer::new();
995 let triples = vec![
996 triple(iri("http://ex.org/s"), iri("http://ex.org/p1"), lit("a")),
997 triple(iri("http://ex.org/s"), iri("http://ex.org/p2"), lit("b")),
998 triple(iri("http://ex.org/s"), iri("http://ex.org/p3"), lit("c")),
999 ];
1000 let (output, stats) = ser.serialize(&triples);
1001 assert_eq!(stats.subjects_count, 1);
1002 assert_eq!(output.matches(';').count(), 2);
1003 }
1004
1005 #[test]
1006 fn test_three_objects_same_predicate() {
1007 let ser = CompactSerializer::new();
1008 let triples = vec![
1009 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), lit("a")),
1010 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), lit("b")),
1011 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), lit("c")),
1012 ];
1013 let (output, _) = ser.serialize(&triples);
1014 assert_eq!(output.matches(',').count(), 2);
1015 }
1016
1017 #[test]
1018 fn test_rdf_type_with_prefix() {
1019 let mut ser = CompactSerializer::new();
1020 ser.add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
1021 let triples = vec![triple(
1022 iri("http://ex.org/s"),
1023 iri(RDF_TYPE),
1024 iri("http://ex.org/Class"),
1025 )];
1026 let (output, stats) = ser.serialize(&triples);
1027 assert!(output.contains(" a "));
1028 assert_eq!(stats.a_shorthand_count, 1);
1029 }
1030
1031 #[test]
1032 fn test_escape_tab_and_return() {
1033 let ser = CompactSerializer::new();
1034 let triples = vec![triple(
1035 iri("http://ex.org/s"),
1036 iri("http://ex.org/p"),
1037 lit("line1\tline2\rline3"),
1038 )];
1039 let (output, _) = ser.serialize(&triples);
1040 assert!(output.contains("\\t"));
1041 assert!(output.contains("\\r"));
1042 }
1043
1044 #[test]
1045 fn test_single_element_list() {
1046 let triples = vec![
1047 triple(blank("l0"), iri(RDF_FIRST), lit("only")),
1048 triple(blank("l0"), iri(RDF_REST), iri(RDF_NIL)),
1049 ];
1050 let ser = CompactSerializer::new();
1051 let list = ser.detect_list("l0", &triples);
1052 assert!(list.is_some());
1053 let items = list.expect("single item list");
1054 assert_eq!(items.len(), 1);
1055 assert_eq!(items[0], lit("only"));
1056 }
1057
1058 #[test]
1059 fn test_detect_list_missing_first() {
1060 let triples = vec![triple(blank("l0"), iri(RDF_REST), iri(RDF_NIL))];
1061 let ser = CompactSerializer::new();
1062 let list = ser.detect_list("l0", &triples);
1063 assert!(list.is_none());
1064 }
1065
1066 #[test]
1067 fn test_detect_list_missing_rest() {
1068 let triples = vec![triple(blank("l0"), iri(RDF_FIRST), lit("a"))];
1069 let ser = CompactSerializer::new();
1070 let list = ser.detect_list("l0", &triples);
1071 assert!(list.is_none());
1072 }
1073
1074 #[test]
1075 fn test_format_collection_single() {
1076 let ser = CompactSerializer::new();
1077 let items = vec![lit("x")];
1078 let output = ser.format_collection(&items);
1079 assert_eq!(output, "( \"x\" )");
1080 }
1081
1082 #[test]
1083 fn test_format_collection_empty() {
1084 let ser = CompactSerializer::new();
1085 let items: Vec<RdfTerm> = vec![];
1086 let output = ser.format_collection(&items);
1087 assert_eq!(output, "( )");
1088 }
1089
1090 #[test]
1091 fn test_inline_blank_disabled() {
1092 let config = CompactConfig {
1093 inline_blank_nodes: false,
1094 ..Default::default()
1095 };
1096 let ser = CompactSerializer::with_config(config);
1097 let triples = vec![
1098 triple(iri("http://ex.org/s"), iri("http://ex.org/p"), blank("b0")),
1099 triple(blank("b0"), iri("http://ex.org/name"), lit("Alice")),
1100 ];
1101 let (output, stats) = ser.serialize(&triples);
1102 assert!(!output.contains('['));
1103 assert_eq!(stats.inlined_blanks, 0);
1104 }
1105
1106 #[test]
1107 fn test_suggest_prefixes_frequent_wins() {
1108 let triples = vec![
1109 triple(iri("http://ex.org/s1"), iri("http://ex.org/p1"), lit("a")),
1110 triple(iri("http://ex.org/s2"), iri("http://ex.org/p2"), lit("b")),
1111 triple(iri("http://ex.org/s3"), iri("http://other.org/q"), lit("c")),
1112 ];
1113 let suggested = CompactSerializer::suggest_prefixes(&triples, 1);
1114 assert_eq!(suggested.len(), 1);
1115 let (_, ns) = suggested.iter().next().expect("one prefix");
1117 assert!(ns.contains("ex.org"));
1118 }
1119}