1use std::collections::{HashMap, HashSet};
7
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Error)]
14pub enum ConstructError {
15 #[error("unbound variable in template: ?{name}")]
17 UnboundVariable { name: String },
18
19 #[error("empty term in template triple at position {position}")]
21 EmptyTerm { position: &'static str },
22
23 #[error("blank node in predicate position: {node}")]
25 BlankNodeInPredicate { node: String },
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum RdfTerm {
33 Iri(String),
35 Literal {
37 value: String,
38 datatype: Option<String>,
39 lang_tag: Option<String>,
40 },
41 BlankNode(String),
43 Variable(String),
45}
46
47impl RdfTerm {
48 pub fn is_variable(&self) -> bool {
50 matches!(self, RdfTerm::Variable(_))
51 }
52
53 pub fn is_blank_node(&self) -> bool {
55 matches!(self, RdfTerm::BlankNode(_))
56 }
57
58 pub fn variable_name(&self) -> Option<&str> {
60 match self {
61 RdfTerm::Variable(n) => Some(n.as_str()),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct TemplateTriple {
70 pub subject: RdfTerm,
71 pub predicate: RdfTerm,
72 pub object: RdfTerm,
73}
74
75impl TemplateTriple {
76 pub fn new(subject: RdfTerm, predicate: RdfTerm, object: RdfTerm) -> Self {
78 TemplateTriple {
79 subject,
80 predicate,
81 object,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
88pub struct GroundTriple {
89 pub subject: String,
90 pub predicate: String,
91 pub object: String,
92}
93
94#[derive(Debug, Clone, Default)]
97pub struct SolutionRow {
98 bindings: HashMap<String, String>,
99}
100
101impl SolutionRow {
102 pub fn new() -> Self {
104 SolutionRow {
105 bindings: HashMap::new(),
106 }
107 }
108
109 pub fn bind(&mut self, var: impl Into<String>, value: impl Into<String>) {
111 self.bindings.insert(var.into(), value.into());
112 }
113
114 pub fn get(&self, var: &str) -> Option<&str> {
116 self.bindings.get(var).map(String::as_str)
117 }
118
119 pub fn is_bound(&self, var: &str) -> bool {
121 self.bindings.contains_key(var)
122 }
123
124 pub fn bound_vars(&self) -> impl Iterator<Item = &str> {
126 self.bindings.keys().map(String::as_str)
127 }
128}
129
130#[derive(Debug, Clone, Default)]
132pub struct ConstructStats {
133 pub rows_processed: usize,
135 pub raw_triple_count: usize,
137 pub skipped_unbound: usize,
139 pub duplicates_eliminated: usize,
141 pub blank_nodes_generated: usize,
143}
144
145pub struct BlankNodeAllocator {
149 counter: u64,
150}
151
152impl BlankNodeAllocator {
153 fn new() -> Self {
154 BlankNodeAllocator { counter: 0 }
155 }
156
157 fn fresh(&mut self, row_index: usize, template_label: &str) -> String {
163 self.counter += 1;
164 format!("_:b{}_{}_r{}", self.counter, template_label, row_index)
165 }
166}
167
168pub struct ConstructBuilder {
172 template: Vec<TemplateTriple>,
173 skip_on_unbound: bool,
174}
175
176impl ConstructBuilder {
177 pub fn new(template: Vec<TemplateTriple>) -> Self {
179 ConstructBuilder {
180 template,
181 skip_on_unbound: true,
182 }
183 }
184
185 pub fn from_where_shorthand(pattern: Vec<TemplateTriple>) -> Self {
190 Self::new(pattern)
191 }
192
193 pub fn skip_unbound(mut self, skip: bool) -> Self {
196 self.skip_on_unbound = skip;
197 self
198 }
199
200 pub fn instantiate_row(
206 &self,
207 row: &SolutionRow,
208 row_index: usize,
209 alloc: &mut BlankNodeAllocator,
210 stats: &mut ConstructStats,
211 ) -> Result<Vec<GroundTriple>, ConstructError> {
212 let mut bnode_map: HashMap<String, String> = HashMap::new();
214 let mut triples = Vec::new();
215
216 for tpl in &self.template {
217 let s =
218 self.resolve_term(&tpl.subject, row, row_index, alloc, &mut bnode_map, stats)?;
219 let p =
220 self.resolve_term(&tpl.predicate, row, row_index, alloc, &mut bnode_map, stats)?;
221 let o = self.resolve_term(&tpl.object, row, row_index, alloc, &mut bnode_map, stats)?;
222
223 match (s, p, o) {
224 (Some(s_val), Some(p_val), Some(o_val)) => {
225 if p_val.starts_with("_:") {
227 return Err(ConstructError::BlankNodeInPredicate { node: p_val });
228 }
229 triples.push(GroundTriple {
230 subject: s_val,
231 predicate: p_val,
232 object: o_val,
233 });
234 }
235 _ => {
236 stats.skipped_unbound += 1;
238 }
239 }
240 }
241 Ok(triples)
242 }
243
244 fn resolve_term(
247 &self,
248 term: &RdfTerm,
249 row: &SolutionRow,
250 row_index: usize,
251 alloc: &mut BlankNodeAllocator,
252 bnode_map: &mut HashMap<String, String>,
253 stats: &mut ConstructStats,
254 ) -> Result<Option<String>, ConstructError> {
255 match term {
256 RdfTerm::Iri(iri) => Ok(Some(format!("<{}>", iri))),
257 RdfTerm::Literal {
258 value,
259 datatype,
260 lang_tag,
261 } => {
262 let serialised = if let Some(dt) = datatype {
263 format!("\"{}\"^^<{}>", value, dt)
264 } else if let Some(lang) = lang_tag {
265 format!("\"{}\"@{}", value, lang)
266 } else {
267 format!("\"{}\"", value)
268 };
269 Ok(Some(serialised))
270 }
271 RdfTerm::BlankNode(label) => {
272 let fresh = bnode_map.entry(label.clone()).or_insert_with(|| {
273 stats.blank_nodes_generated += 1;
274 alloc.fresh(row_index, label)
275 });
276 Ok(Some(fresh.clone()))
277 }
278 RdfTerm::Variable(name) => {
279 if let Some(val) = row.get(name) {
280 Ok(Some(val.to_owned()))
281 } else if self.skip_on_unbound {
282 Ok(None)
283 } else {
284 Err(ConstructError::UnboundVariable { name: name.clone() })
285 }
286 }
287 }
288 }
289
290 pub fn build(
294 &self,
295 solutions: &[SolutionRow],
296 ) -> Result<(Vec<GroundTriple>, ConstructStats), ConstructError> {
297 let mut stats = ConstructStats::default();
298 let mut alloc = BlankNodeAllocator::new();
299 let mut seen: HashSet<GroundTriple> = HashSet::new();
300 let mut result: Vec<GroundTriple> = Vec::new();
301
302 for (row_index, row) in solutions.iter().enumerate() {
303 stats.rows_processed += 1;
304 let row_triples = self.instantiate_row(row, row_index, &mut alloc, &mut stats)?;
305 stats.raw_triple_count += row_triples.len();
306
307 for triple in row_triples {
308 if seen.insert(triple.clone()) {
309 result.push(triple);
310 } else {
311 stats.duplicates_eliminated += 1;
312 }
313 }
314 }
315
316 Ok((result, stats))
317 }
318
319 pub fn variable_present_in_all(var: &str, solutions: &[SolutionRow]) -> bool {
321 solutions.iter().all(|r| r.is_bound(var))
322 }
323
324 pub fn variable_present_in_any(var: &str, solutions: &[SolutionRow]) -> bool {
326 solutions.iter().any(|r| r.is_bound(var))
327 }
328
329 pub fn template(&self) -> &[TemplateTriple] {
331 &self.template
332 }
333}
334
335impl RdfTerm {
338 pub fn iri(iri: impl Into<String>) -> Self {
340 RdfTerm::Iri(iri.into())
341 }
342
343 pub fn string_literal(value: impl Into<String>) -> Self {
345 RdfTerm::Literal {
346 value: value.into(),
347 datatype: None,
348 lang_tag: None,
349 }
350 }
351
352 pub fn typed_literal(value: impl Into<String>, datatype: impl Into<String>) -> Self {
354 RdfTerm::Literal {
355 value: value.into(),
356 datatype: Some(datatype.into()),
357 lang_tag: None,
358 }
359 }
360
361 pub fn lang_literal(value: impl Into<String>, lang: impl Into<String>) -> Self {
363 RdfTerm::Literal {
364 value: value.into(),
365 datatype: None,
366 lang_tag: Some(lang.into()),
367 }
368 }
369
370 pub fn boolean(v: bool) -> Self {
372 RdfTerm::typed_literal(v.to_string(), "http://www.w3.org/2001/XMLSchema#boolean")
373 }
374
375 pub fn integer(v: i64) -> Self {
377 RdfTerm::typed_literal(v.to_string(), "http://www.w3.org/2001/XMLSchema#integer")
378 }
379
380 pub fn double(v: f64) -> Self {
382 RdfTerm::typed_literal(v.to_string(), "http://www.w3.org/2001/XMLSchema#double")
383 }
384
385 pub fn var(name: impl Into<String>) -> Self {
387 RdfTerm::Variable(name.into())
388 }
389
390 pub fn blank(label: impl Into<String>) -> Self {
392 RdfTerm::BlankNode(label.into())
393 }
394}
395
396#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn make_row(pairs: &[(&str, &str)]) -> SolutionRow {
403 let mut row = SolutionRow::new();
404 for (k, v) in pairs {
405 row.bind(*k, *v);
406 }
407 row
408 }
409
410 #[test]
413 fn test_single_iri_triple() {
414 let template = vec![TemplateTriple::new(
415 RdfTerm::iri("http://example.org/s"),
416 RdfTerm::iri("http://example.org/p"),
417 RdfTerm::iri("http://example.org/o"),
418 )];
419 let builder = ConstructBuilder::new(template);
420 let row = SolutionRow::new();
421 let (triples, stats) = builder.build(&[row]).expect("build");
422 assert_eq!(triples.len(), 1);
423 assert_eq!(stats.rows_processed, 1);
424 assert_eq!(stats.raw_triple_count, 1);
425 }
426
427 #[test]
428 fn test_variable_binding() {
429 let template = vec![TemplateTriple::new(
430 RdfTerm::var("s"),
431 RdfTerm::iri("http://example.org/type"),
432 RdfTerm::var("t"),
433 )];
434 let builder = ConstructBuilder::new(template);
435 let row = make_row(&[
436 ("s", "<http://example.org/Alice>"),
437 ("t", "<http://example.org/Person>"),
438 ]);
439 let (triples, _stats) = builder.build(&[row]).expect("build");
440 assert_eq!(triples.len(), 1);
441 assert_eq!(triples[0].subject, "<http://example.org/Alice>");
442 assert_eq!(triples[0].object, "<http://example.org/Person>");
443 }
444
445 #[test]
446 fn test_unbound_variable_skipped_by_default() {
447 let template = vec![TemplateTriple::new(
448 RdfTerm::var("s"),
449 RdfTerm::iri("http://example.org/p"),
450 RdfTerm::var("o"),
451 )];
452 let builder = ConstructBuilder::new(template);
453 let row = make_row(&[("s", "<http://example.org/Alice>")]);
455 let (triples, stats) = builder.build(&[row]).expect("build");
456 assert_eq!(triples.len(), 0);
457 assert_eq!(stats.skipped_unbound, 1);
458 }
459
460 #[test]
461 fn test_unbound_variable_error_mode() {
462 let template = vec![TemplateTriple::new(
463 RdfTerm::var("s"),
464 RdfTerm::iri("http://example.org/p"),
465 RdfTerm::var("o"),
466 )];
467 let builder = ConstructBuilder::new(template).skip_unbound(false);
468 let row = make_row(&[("s", "<http://example.org/Alice>")]);
469 let result = builder.build(&[row]);
470 assert!(result.is_err());
471 assert!(
472 matches!(result.unwrap_err(), ConstructError::UnboundVariable { name } if name == "o")
473 );
474 }
475
476 #[test]
479 fn test_blank_node_unique_per_row() {
480 let template = vec![TemplateTriple::new(
481 RdfTerm::blank("b"),
482 RdfTerm::iri("http://example.org/value"),
483 RdfTerm::var("v"),
484 )];
485 let builder = ConstructBuilder::new(template);
486 let rows = vec![make_row(&[("v", "\"1\"")]), make_row(&[("v", "\"2\"")])];
487 let (triples, stats) = builder.build(&rows).expect("build");
488 assert_eq!(triples.len(), 2);
489 assert_ne!(triples[0].subject, triples[1].subject);
491 assert_eq!(stats.blank_nodes_generated, 2);
492 }
493
494 #[test]
495 fn test_blank_node_shared_within_row() {
496 let template = vec![
499 TemplateTriple::new(
500 RdfTerm::blank("b"),
501 RdfTerm::iri("http://example.org/type"),
502 RdfTerm::iri("http://example.org/Thing"),
503 ),
504 TemplateTriple::new(
505 RdfTerm::blank("b"),
506 RdfTerm::iri("http://example.org/name"),
507 RdfTerm::string_literal("test"),
508 ),
509 ];
510 let builder = ConstructBuilder::new(template);
511 let row = SolutionRow::new();
512 let (triples, stats) = builder.build(&[row]).expect("build");
513 assert_eq!(triples.len(), 2);
514 assert_eq!(triples[0].subject, triples[1].subject);
515 assert_eq!(stats.blank_nodes_generated, 1);
517 }
518
519 #[test]
522 fn test_duplicate_triple_elimination() {
523 let template = vec![TemplateTriple::new(
524 RdfTerm::iri("http://example.org/s"),
525 RdfTerm::iri("http://example.org/p"),
526 RdfTerm::iri("http://example.org/o"),
527 )];
528 let builder = ConstructBuilder::new(template);
529 let rows = vec![SolutionRow::new(), SolutionRow::new(), SolutionRow::new()];
531 let (triples, stats) = builder.build(&rows).expect("build");
532 assert_eq!(triples.len(), 1);
533 assert_eq!(stats.duplicates_eliminated, 2);
534 assert_eq!(stats.raw_triple_count, 3);
535 }
536
537 #[test]
538 fn test_no_duplicates_when_all_distinct() {
539 let template = vec![TemplateTriple::new(
540 RdfTerm::var("s"),
541 RdfTerm::iri("http://example.org/p"),
542 RdfTerm::iri("http://example.org/o"),
543 )];
544 let builder = ConstructBuilder::new(template);
545 let rows = vec![
546 make_row(&[("s", "<http://example.org/A>")]),
547 make_row(&[("s", "<http://example.org/B>")]),
548 ];
549 let (triples, stats) = builder.build(&rows).expect("build");
550 assert_eq!(triples.len(), 2);
551 assert_eq!(stats.duplicates_eliminated, 0);
552 }
553
554 #[test]
557 fn test_construct_where_shorthand() {
558 let pattern = vec![TemplateTriple::new(
559 RdfTerm::var("x"),
560 RdfTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
561 RdfTerm::var("t"),
562 )];
563 let builder = ConstructBuilder::from_where_shorthand(pattern.clone());
564 assert_eq!(builder.template().len(), pattern.len());
565 }
566
567 #[test]
570 fn test_string_literal_binding() {
571 let template = vec![TemplateTriple::new(
572 RdfTerm::iri("http://example.org/s"),
573 RdfTerm::iri("http://example.org/name"),
574 RdfTerm::string_literal("Alice"),
575 )];
576 let builder = ConstructBuilder::new(template);
577 let (triples, _stats) = builder.build(&[SolutionRow::new()]).expect("build");
578 assert_eq!(triples[0].object, "\"Alice\"");
579 }
580
581 #[test]
582 fn test_integer_literal_binding() {
583 let template = vec![TemplateTriple::new(
584 RdfTerm::iri("http://example.org/s"),
585 RdfTerm::iri("http://example.org/age"),
586 RdfTerm::integer(42),
587 )];
588 let builder = ConstructBuilder::new(template);
589 let (triples, _stats) = builder.build(&[SolutionRow::new()]).expect("build");
590 assert!(triples[0].object.contains("42"));
591 assert!(triples[0].object.contains("integer"));
592 }
593
594 #[test]
595 fn test_boolean_literal_binding() {
596 let term = RdfTerm::boolean(true);
597 if let RdfTerm::Literal {
598 value, datatype, ..
599 } = &term
600 {
601 assert_eq!(value, "true");
602 assert!(datatype.as_deref().unwrap_or("").contains("boolean"));
603 } else {
604 panic!("expected Literal");
605 }
606 }
607
608 #[test]
609 #[allow(clippy::approx_constant)]
610 fn test_double_literal_binding() {
611 let term = RdfTerm::double(3.14);
612 if let RdfTerm::Literal {
613 value, datatype, ..
614 } = &term
615 {
616 assert!(value.contains("3.14"));
617 assert!(datatype.as_deref().unwrap_or("").contains("double"));
618 } else {
619 panic!("expected Literal");
620 }
621 }
622
623 #[test]
624 fn test_lang_tagged_literal() {
625 let template = vec![TemplateTriple::new(
626 RdfTerm::iri("http://example.org/s"),
627 RdfTerm::iri("http://example.org/label"),
628 RdfTerm::lang_literal("Hallo", "de"),
629 )];
630 let builder = ConstructBuilder::new(template);
631 let (triples, _) = builder.build(&[SolutionRow::new()]).expect("build");
632 assert_eq!(triples[0].object, "\"Hallo\"@de");
633 }
634
635 #[test]
638 fn test_variable_present_in_all() {
639 let rows = vec![
640 make_row(&[("x", "1"), ("y", "2")]),
641 make_row(&[("x", "3"), ("y", "4")]),
642 ];
643 assert!(ConstructBuilder::variable_present_in_all("x", &rows));
644 assert!(ConstructBuilder::variable_present_in_all("y", &rows));
645 assert!(!ConstructBuilder::variable_present_in_all("z", &rows));
646 }
647
648 #[test]
649 fn test_variable_present_in_any() {
650 let rows = vec![make_row(&[("x", "1")]), make_row(&[("y", "2")])];
651 assert!(ConstructBuilder::variable_present_in_any("x", &rows));
652 assert!(ConstructBuilder::variable_present_in_any("y", &rows));
653 assert!(!ConstructBuilder::variable_present_in_any("z", &rows));
654 }
655
656 #[test]
657 fn test_variable_present_partial_binding() {
658 let rows = vec![
659 make_row(&[("x", "1"), ("y", "2")]),
660 make_row(&[("x", "3")]), ];
662 assert!(!ConstructBuilder::variable_present_in_all("y", &rows));
664 assert!(ConstructBuilder::variable_present_in_any("y", &rows));
666 }
667
668 #[test]
671 fn test_construct_stats_populated() {
672 let template = vec![
673 TemplateTriple::new(
674 RdfTerm::var("s"),
675 RdfTerm::iri("http://example.org/p"),
676 RdfTerm::var("o"),
677 ),
678 TemplateTriple::new(
679 RdfTerm::var("s"),
680 RdfTerm::iri("http://example.org/q"),
681 RdfTerm::var("missing"),
682 ),
683 ];
684 let builder = ConstructBuilder::new(template);
685 let rows = vec![
686 make_row(&[
687 ("s", "<http://example.org/A>"),
688 ("o", "<http://example.org/B>"),
689 ]),
690 make_row(&[
691 ("s", "<http://example.org/A>"),
692 ("o", "<http://example.org/B>"),
693 ]),
694 ];
695 let (triples, stats) = builder.build(&rows).expect("build");
696 assert_eq!(stats.rows_processed, 2);
697 assert_eq!(stats.raw_triple_count, 2);
699 assert_eq!(stats.skipped_unbound, 2);
700 assert_eq!(triples.len(), 1);
702 assert_eq!(stats.duplicates_eliminated, 1);
703 }
704
705 #[test]
706 fn test_empty_solution_set() {
707 let template = vec![TemplateTriple::new(
708 RdfTerm::iri("http://example.org/s"),
709 RdfTerm::iri("http://example.org/p"),
710 RdfTerm::iri("http://example.org/o"),
711 )];
712 let builder = ConstructBuilder::new(template);
713 let (triples, stats) = builder.build(&[]).expect("build");
714 assert_eq!(triples.len(), 0);
715 assert_eq!(stats.rows_processed, 0);
716 }
717
718 #[test]
719 fn test_blank_node_in_predicate_rejected() {
720 let template = vec![TemplateTriple::new(
721 RdfTerm::iri("http://example.org/s"),
722 RdfTerm::blank("b"),
723 RdfTerm::iri("http://example.org/o"),
724 )];
725 let builder = ConstructBuilder::new(template);
726 let result = builder.build(&[SolutionRow::new()]);
727 assert!(result.is_err());
728 assert!(matches!(
729 result.unwrap_err(),
730 ConstructError::BlankNodeInPredicate { .. }
731 ));
732 }
733
734 #[test]
735 fn test_multiple_variables_multiple_rows() {
736 let template = vec![
737 TemplateTriple::new(
738 RdfTerm::var("person"),
739 RdfTerm::iri("http://schema.org/name"),
740 RdfTerm::var("name"),
741 ),
742 TemplateTriple::new(
743 RdfTerm::var("person"),
744 RdfTerm::iri("http://schema.org/age"),
745 RdfTerm::var("age"),
746 ),
747 ];
748 let builder = ConstructBuilder::new(template);
749 let rows = vec![
750 make_row(&[
751 ("person", "<http://example.org/Alice>"),
752 ("name", "\"Alice\""),
753 ("age", "\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>"),
754 ]),
755 make_row(&[
756 ("person", "<http://example.org/Bob>"),
757 ("name", "\"Bob\""),
758 ("age", "\"25\"^^<http://www.w3.org/2001/XMLSchema#integer>"),
759 ]),
760 ];
761 let (triples, stats) = builder.build(&rows).expect("build");
762 assert_eq!(triples.len(), 4);
763 assert_eq!(stats.rows_processed, 2);
764 assert_eq!(stats.duplicates_eliminated, 0);
765 }
766
767 #[test]
768 fn test_typed_literal_serialisation() {
769 let term = RdfTerm::typed_literal("2024-01-01", "http://www.w3.org/2001/XMLSchema#date");
770 let template = vec![TemplateTriple::new(
771 RdfTerm::iri("http://example.org/s"),
772 RdfTerm::iri("http://example.org/date"),
773 term,
774 )];
775 let builder = ConstructBuilder::new(template);
776 let (triples, _) = builder.build(&[SolutionRow::new()]).expect("build");
777 assert_eq!(
778 triples[0].object,
779 "\"2024-01-01\"^^<http://www.w3.org/2001/XMLSchema#date>"
780 );
781 }
782
783 #[test]
784 fn test_solution_row_is_bound() {
785 let mut row = SolutionRow::new();
786 row.bind("x", "value");
787 assert!(row.is_bound("x"));
788 assert!(!row.is_bound("y"));
789 }
790
791 #[test]
792 fn test_solution_row_bound_vars_iteration() {
793 let row = make_row(&[("a", "1"), ("b", "2"), ("c", "3")]);
794 let vars: Vec<&str> = row.bound_vars().collect();
795 assert_eq!(vars.len(), 3);
796 }
797
798 #[test]
799 fn test_large_result_set_deduplication() {
800 let template = vec![TemplateTriple::new(
802 RdfTerm::iri("http://example.org/s"),
803 RdfTerm::iri("http://example.org/p"),
804 RdfTerm::iri("http://example.org/o"),
805 )];
806 let builder = ConstructBuilder::new(template);
807 let rows: Vec<SolutionRow> = (0..100).map(|_| SolutionRow::new()).collect();
808 let (triples, stats) = builder.build(&rows).expect("build");
809 assert_eq!(triples.len(), 1);
810 assert_eq!(stats.duplicates_eliminated, 99);
811 }
812
813 #[test]
816 fn test_rdf_term_is_variable() {
817 assert!(RdfTerm::var("x").is_variable());
818 assert!(!RdfTerm::iri("http://example.org/x").is_variable());
819 }
820
821 #[test]
822 fn test_rdf_term_is_blank_node() {
823 assert!(RdfTerm::blank("b0").is_blank_node());
824 assert!(!RdfTerm::var("x").is_blank_node());
825 }
826
827 #[test]
828 fn test_rdf_term_variable_name() {
829 assert_eq!(RdfTerm::var("foo").variable_name(), Some("foo"));
830 assert_eq!(RdfTerm::iri("http://example.org/").variable_name(), None);
831 }
832
833 #[test]
834 fn test_iri_term_serialisation() {
835 let template = vec![TemplateTriple::new(
836 RdfTerm::iri("http://example.org/s"),
837 RdfTerm::iri("http://example.org/p"),
838 RdfTerm::iri("http://example.org/o"),
839 )];
840 let builder = ConstructBuilder::new(template);
841 let (triples, _) = builder.build(&[SolutionRow::new()]).expect("build");
842 assert!(triples[0].subject.starts_with('<'));
844 assert!(triples[0].subject.ends_with('>'));
845 }
846
847 #[test]
850 fn test_ground_triple_equality() {
851 let t1 = GroundTriple {
852 subject: "s".into(),
853 predicate: "p".into(),
854 object: "o".into(),
855 };
856 let t2 = GroundTriple {
857 subject: "s".into(),
858 predicate: "p".into(),
859 object: "o".into(),
860 };
861 assert_eq!(t1, t2);
862 }
863
864 #[test]
865 fn test_ground_triple_inequality() {
866 let t1 = GroundTriple {
867 subject: "s1".into(),
868 predicate: "p".into(),
869 object: "o".into(),
870 };
871 let t2 = GroundTriple {
872 subject: "s2".into(),
873 predicate: "p".into(),
874 object: "o".into(),
875 };
876 assert_ne!(t1, t2);
877 }
878
879 #[test]
882 fn test_blank_node_allocator_monotonic() {
883 let mut alloc = BlankNodeAllocator::new();
884 let a = alloc.fresh(0, "b");
885 let b = alloc.fresh(0, "b");
886 assert_ne!(a, b);
888 }
889
890 #[test]
891 fn test_blank_node_allocator_different_rows() {
892 let mut alloc = BlankNodeAllocator::new();
893 let r0 = alloc.fresh(0, "same");
894 let r1 = alloc.fresh(1, "same");
895 assert_ne!(r0, r1);
896 }
897
898 #[test]
901 fn test_template_triple_fields() {
902 let t = TemplateTriple::new(
903 RdfTerm::iri("http://s"),
904 RdfTerm::iri("http://p"),
905 RdfTerm::iri("http://o"),
906 );
907 assert!(matches!(t.subject, RdfTerm::Iri(_)));
908 assert!(matches!(t.predicate, RdfTerm::Iri(_)));
909 assert!(matches!(t.object, RdfTerm::Iri(_)));
910 }
911
912 #[test]
915 fn test_stats_blank_nodes_counted_across_rows() {
916 let template = vec![TemplateTriple::new(
917 RdfTerm::blank("b"),
918 RdfTerm::iri("http://p"),
919 RdfTerm::iri("http://o"),
920 )];
921 let builder = ConstructBuilder::new(template);
922 let rows = vec![SolutionRow::new(), SolutionRow::new(), SolutionRow::new()];
923 let (_, stats) = builder.build(&rows).expect("build");
924 assert_eq!(stats.blank_nodes_generated, 3);
926 }
927
928 #[test]
929 fn test_template_clone() {
930 let t = TemplateTriple::new(RdfTerm::var("x"), RdfTerm::iri("p"), RdfTerm::var("y"));
931 let t2 = t.clone();
932 assert_eq!(t, t2);
933 }
934
935 #[test]
938 fn test_template_accessor() {
939 let template = vec![
940 TemplateTriple::new(RdfTerm::var("a"), RdfTerm::iri("b"), RdfTerm::var("c")),
941 TemplateTriple::new(RdfTerm::iri("x"), RdfTerm::iri("y"), RdfTerm::iri("z")),
942 ];
943 let builder = ConstructBuilder::new(template);
944 assert_eq!(builder.template().len(), 2);
945 }
946
947 #[test]
948 fn test_skip_unbound_chained() {
949 let builder = ConstructBuilder::new(vec![]).skip_unbound(false);
950 let (triples, _) = builder.build(&[]).expect("build empty");
953 assert!(triples.is_empty());
954 }
955}