#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use crate::annotation_syntax::{
expander::{expand_annotations, to_explicit_turtle},
tokenizer::{find_annotation_blocks, tokenize_annotation_block, AnnotationToken},
AnnotatedTriple, AnnotationLiteral, AnnotationPair, AnnotationParser,
AnnotationSyntaxError, AnnotationValue, RdfStarTriple, StarTerm,
};
fn named(iri: &str) -> StarTerm {
StarTerm::NamedNode(iri.to_string())
}
fn ann_named(iri: &str) -> AnnotationValue {
AnnotationValue::NamedNode(iri.to_string())
}
fn base_triple() -> RdfStarTriple {
RdfStarTriple::new(
named("http://example.org/s"),
named("http://example.org/p"),
named("http://example.org/o"),
)
}
#[test]
fn test_parse_simple_annotation() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/ap> <http://example.org/ao> |}";
let result = parser.parse_annotated_triple(input);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let annotated = result.unwrap();
assert_eq!(annotated.annotations.len(), 1);
assert_eq!(annotated.annotations[0].predicate, "http://example.org/ap");
assert_eq!(
annotated.annotations[0].object,
ann_named("http://example.org/ao")
);
}
#[test]
fn test_parse_multiple_annotations() {
let parser = AnnotationParser::new();
let input =
"<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/ap1> <http://example.org/ao1> ; <http://example.org/ap2> <http://example.org/ao2> |}";
let result = parser.parse_annotated_triple(input);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let annotated = result.unwrap();
assert_eq!(annotated.annotations.len(), 2);
assert_eq!(annotated.annotations[0].predicate, "http://example.org/ap1");
assert_eq!(annotated.annotations[1].predicate, "http://example.org/ap2");
}
#[test]
fn test_parse_annotation_with_literal() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/certainty> \"0.95\" |}";
let result = parser.parse_annotated_triple(input);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let annotated = result.unwrap();
assert_eq!(annotated.annotations.len(), 1);
if let AnnotationValue::Literal(lit) = &annotated.annotations[0].object {
assert_eq!(lit.value, "0.95");
assert!(lit.language.is_none());
assert!(lit.datatype.is_none());
} else {
panic!("expected literal annotation object");
}
}
#[test]
fn test_parse_annotation_with_typed_literal() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/score> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> |}";
let result = parser.parse_annotated_triple(input);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let annotated = result.unwrap();
assert_eq!(annotated.annotations.len(), 1);
if let AnnotationValue::Literal(lit) = &annotated.annotations[0].object {
assert_eq!(lit.value, "42");
assert_eq!(
lit.datatype.as_deref(),
Some("http://www.w3.org/2001/XMLSchema#integer")
);
} else {
panic!("expected typed literal annotation object");
}
}
#[test]
fn test_parse_annotation_with_blank_node() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/ap> _:b0 |}";
let result = parser.parse_annotated_triple(input);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let annotated = result.unwrap();
assert_eq!(annotated.annotations.len(), 1);
assert_eq!(
annotated.annotations[0].object,
AnnotationValue::BlankNode("b0".to_string())
);
}
#[test]
fn test_expand_single_annotation() {
let base = base_triple();
let annotations = vec![AnnotationPair {
predicate: "http://example.org/certainty".to_string(),
object: ann_named("http://example.org/high"),
}];
let annotated = AnnotatedTriple::new(base, annotations);
let expanded = expand_annotations(&annotated);
assert_eq!(expanded.len(), 1);
if let StarTerm::QuotedTriple(qt) = &expanded[0].0 {
assert_eq!(qt.subject, named("http://example.org/s"));
} else {
panic!("expected QuotedTriple subject");
}
assert_eq!(expanded[0].1, "http://example.org/certainty");
}
#[test]
fn test_expand_multiple_annotations() {
let base = base_triple();
let annotations = vec![
AnnotationPair {
predicate: "http://example.org/ap1".to_string(),
object: ann_named("http://example.org/ao1"),
},
AnnotationPair {
predicate: "http://example.org/ap2".to_string(),
object: ann_named("http://example.org/ao2"),
},
AnnotationPair {
predicate: "http://example.org/ap3".to_string(),
object: ann_named("http://example.org/ao3"),
},
];
let annotated = AnnotatedTriple::new(base, annotations);
let expanded = expand_annotations(&annotated);
assert_eq!(expanded.len(), 3);
for (qt_term, _, _) in &expanded {
assert!(matches!(qt_term, StarTerm::QuotedTriple(_)));
}
}
#[test]
fn test_to_explicit_turtle() {
let base = base_triple();
let annotations = vec![AnnotationPair {
predicate: "http://example.org/certainty".to_string(),
object: ann_named("http://example.org/high"),
}];
let annotated = AnnotatedTriple::new(base, annotations);
let turtle = to_explicit_turtle(&annotated);
assert!(turtle.contains("<< "), "missing quoted triple: {}", turtle);
assert!(turtle.contains(" >>"), "missing closing >>: {}", turtle);
assert!(
turtle.contains("http://example.org/certainty"),
"missing annotation predicate: {}",
turtle
);
assert!(
turtle.contains("http://example.org/high"),
"missing annotation object: {}",
turtle
);
}
#[test]
fn test_tokenize_annotation_block() {
let input = "<http://example.org/p> <http://example.org/o>";
let tokens = tokenize_annotation_block(input).unwrap();
assert_eq!(tokens.len(), 2);
assert_eq!(
tokens[0],
AnnotationToken::NamedNode("http://example.org/p".to_string())
);
assert_eq!(
tokens[1],
AnnotationToken::NamedNode("http://example.org/o".to_string())
);
}
#[test]
fn test_find_annotation_blocks() {
let input = "<s> <p> <o> {| <ap1> <ao1> |} .\n<s2> <p2> <o2> {| <ap2> <ao2> |} .";
let blocks = find_annotation_blocks(input);
assert_eq!(
blocks.len(),
2,
"expected 2 annotation blocks, got {}",
blocks.len()
);
let (s1, e1) = blocks[0];
let block1 = &input[s1..e1];
assert!(block1.contains("ap1"), "first block: {}", block1);
let (s2, e2) = blocks[1];
let block2 = &input[s2..e2];
assert!(block2.contains("ap2"), "second block: {}", block2);
}
#[test]
fn test_nested_quoted_triple_subject() {
let inner = RdfStarTriple::new(
named("http://example.org/s"),
named("http://example.org/p"),
named("http://example.org/o"),
);
let quoted_subject = StarTerm::QuotedTriple(Box::new(inner));
let outer = RdfStarTriple::new(
quoted_subject,
named("http://example.org/certainty"),
named("http://example.org/high"),
);
let turtle = outer.to_turtle();
assert!(turtle.contains("<< "), "missing << in: {}", turtle);
assert!(turtle.contains(" >>"), "missing >> in: {}", turtle);
}
#[test]
fn test_error_unclosed_annotation() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/p>";
let result = parser.parse_annotated_triple(input);
assert!(
matches!(result, Err(AnnotationSyntaxError::UnclosedAnnotation)),
"expected UnclosedAnnotation, got {:?}",
result
);
}
#[test]
fn test_error_empty_annotation() {
let input = ""; let tokens = tokenize_annotation_block(input).unwrap();
assert!(tokens.is_empty());
let parser = AnnotationParser::new();
let full_input =
"<http://example.org/s> <http://example.org/p> <http://example.org/o> {| |}";
let result = parser.parse_annotated_triple(full_input);
assert!(
matches!(result, Err(AnnotationSyntaxError::EmptyAnnotation)),
"expected EmptyAnnotation, got {:?}",
result
);
}
#[test]
fn test_error_invalid_predicate() {
let input = "<http://example.org/p> _:b0 <http://example.org/o>";
let _tokens = tokenize_annotation_block(input).unwrap();
let parser = AnnotationParser::new();
let full_input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| _:b0 <http://example.org/ao> |}";
let result = parser.parse_annotated_triple(full_input);
assert!(
matches!(result, Err(AnnotationSyntaxError::InvalidPredicate(_))),
"expected InvalidPredicate, got {:?}",
result
);
}
#[test]
fn test_roundtrip_explicit() {
let parser = AnnotationParser::new();
let input = "<http://example.org/s> <http://example.org/p> <http://example.org/o> \
{| <http://example.org/certainty> \"high\" |}";
let annotated = parser.parse_annotated_triple(input).unwrap();
let explicit = to_explicit_turtle(&annotated);
assert!(
explicit.contains("http://example.org/s"),
"missing subject: {}",
explicit
);
assert!(
explicit.contains("http://example.org/certainty"),
"missing annotation predicate: {}",
explicit
);
assert!(
explicit.contains("high"),
"missing annotation value: {}",
explicit
);
assert!(
explicit.contains("<<"),
"missing quoted triple open: {}",
explicit
);
assert!(
explicit.contains(">>"),
"missing quoted triple close: {}",
explicit
);
}
#[test]
fn test_annotation_literal_with_language() {
let lit = AnnotationLiteral {
value: "hello".to_string(),
language: Some("en".to_string()),
datatype: None,
};
let val = AnnotationValue::Literal(lit);
let turtle = val.to_turtle();
assert_eq!(turtle, "\"hello\"@en");
}
#[test]
fn test_annotation_value_named_node_turtle() {
let val = ann_named("http://example.org/x");
assert_eq!(val.to_turtle(), "<http://example.org/x>");
}
#[test]
fn test_tokenize_with_typed_literal() {
let input = "<http://example.org/p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer>";
let tokens = tokenize_annotation_block(input).unwrap();
assert_eq!(tokens.len(), 2);
if let AnnotationToken::Literal(val, lang, dt) = &tokens[1] {
assert_eq!(val, "42");
assert!(lang.is_none());
assert_eq!(
dt.as_deref(),
Some("http://www.w3.org/2001/XMLSchema#integer")
);
} else {
panic!("expected Literal token, got {:?}", tokens[1]);
}
}
}