use crate::model::{StarTerm, StarTriple};
use crate::w3c_compliance::Annotation;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct ComplianceViolation {
pub rule_id: &'static str,
pub message: String,
pub severity: Severity,
}
impl ComplianceViolation {
fn new(rule_id: &'static str, message: impl Into<String>, severity: Severity) -> Self {
Self {
rule_id,
message: message.into(),
severity,
}
}
fn error(rule_id: &'static str, message: impl Into<String>) -> Self {
Self::new(rule_id, message, Severity::Error)
}
fn warning(rule_id: &'static str, message: impl Into<String>) -> Self {
Self::new(rule_id, message, Severity::Warning)
}
#[allow(dead_code)]
fn info(rule_id: &'static str, message: impl Into<String>) -> Self {
Self::new(rule_id, message, Severity::Info)
}
}
#[derive(Debug, Clone, Default)]
pub struct ComplianceReport {
pub violations: Vec<ComplianceViolation>,
pub conformant: bool,
}
impl ComplianceReport {
fn new(violations: Vec<ComplianceViolation>) -> Self {
let conformant = violations.iter().all(|v| v.severity != Severity::Error);
Self {
violations,
conformant,
}
}
pub fn count_severity(&self, severity: &Severity) -> usize {
self.violations
.iter()
.filter(|v| &v.severity == severity)
.count()
}
pub fn errors(&self) -> Vec<&ComplianceViolation> {
self.violations
.iter()
.filter(|v| v.severity == Severity::Error)
.collect()
}
pub fn warnings(&self) -> Vec<&ComplianceViolation> {
self.violations
.iter()
.filter(|v| v.severity == Severity::Warning)
.collect()
}
}
const DEFAULT_MAX_NESTING_DEPTH: usize = 20;
pub struct W3cRdfStarChecker {
max_nesting_depth: usize,
}
impl Default for W3cRdfStarChecker {
fn default() -> Self {
Self::new()
}
}
impl W3cRdfStarChecker {
pub fn new() -> Self {
Self {
max_nesting_depth: DEFAULT_MAX_NESTING_DEPTH,
}
}
pub fn with_max_nesting_depth(max_nesting_depth: usize) -> Self {
Self { max_nesting_depth }
}
pub fn check_embedded_triple(&self, triple: &StarTriple) -> Vec<ComplianceViolation> {
let mut violations = Vec::new();
self.check_term_ground(&triple.subject, "subject", &mut violations);
self.check_term_ground(&triple.predicate, "predicate", &mut violations);
self.check_term_ground(&triple.object, "object", &mut violations);
violations
}
fn check_term_ground(
&self,
term: &StarTerm,
position: &str,
violations: &mut Vec<ComplianceViolation>,
) {
match term {
StarTerm::Variable(v) => {
violations.push(ComplianceViolation::error(
"STAR-001",
format!(
"Embedded triple {} position contains variable '{}'; \
embedded triples must be ground (no variables)",
position, v.name
),
));
}
StarTerm::QuotedTriple(inner) => {
self.check_term_ground(&inner.subject, "subject", violations);
self.check_term_ground(&inner.predicate, "predicate", violations);
self.check_term_ground(&inner.object, "object", violations);
}
StarTerm::NamedNode(_) | StarTerm::BlankNode(_) | StarTerm::Literal(_) => {}
}
}
pub fn check_annotation(&self, annotation: &Annotation) -> Vec<ComplianceViolation> {
let mut violations = Vec::new();
if !matches!(annotation.predicate, StarTerm::NamedNode(_)) {
violations.push(ComplianceViolation::error(
"STAR-002",
format!(
"Annotation predicate is not a named node: {:?}",
annotation.predicate
),
));
}
if matches!(annotation.object, StarTerm::Variable(_)) {
violations.push(ComplianceViolation::warning(
"STAR-002",
"Annotation object is a variable; annotations in data graphs should use ground terms",
));
}
let base_violations = self.check_embedded_triple(&annotation.base_triple);
violations.extend(base_violations);
violations
}
pub fn check_embedded_blank_subject(&self, triple: &StarTriple) -> Vec<ComplianceViolation> {
let mut violations = Vec::new();
self.check_term_blank_as_embedded_subject(triple, &mut violations);
violations
}
fn check_term_blank_as_embedded_subject(
&self,
triple: &StarTriple,
violations: &mut Vec<ComplianceViolation>,
) {
if matches!(triple.subject, StarTerm::BlankNode(_)) {
violations.push(ComplianceViolation::warning(
"STAR-003",
format!(
"Embedded triple has blank node as subject: {:?}; \
this may cause interoperability issues",
triple.subject
),
));
}
if let StarTerm::QuotedTriple(inner) = &triple.subject {
self.check_term_blank_as_embedded_subject(inner, violations);
}
if let StarTerm::QuotedTriple(inner) = &triple.object {
self.check_term_blank_as_embedded_subject(inner, violations);
}
}
pub fn check_nesting_depth(&self, triple: &StarTriple) -> Vec<ComplianceViolation> {
let mut violations = Vec::new();
let depth = triple.nesting_depth();
if depth > self.max_nesting_depth {
violations.push(ComplianceViolation::warning(
"STAR-004",
format!(
"Embedded triple nesting depth {} exceeds maximum {}; \
deeply nested quoted triples may indicate a modelling error",
depth, self.max_nesting_depth
),
));
}
violations
}
pub fn check_graph(&self, graph: &[StarTriple]) -> ComplianceReport {
let mut violations = Vec::new();
for triple in graph {
self.check_embedded_triple_in_graph(triple, &mut violations);
self.check_blank_subjects_in_graph(triple, &mut violations);
violations.extend(self.check_nesting_depth(triple));
}
self.check_graph_annotations(graph, &mut violations);
ComplianceReport::new(violations)
}
fn check_embedded_triple_in_graph(
&self,
triple: &StarTriple,
violations: &mut Vec<ComplianceViolation>,
) {
if let StarTerm::QuotedTriple(inner) = &triple.subject {
violations.extend(self.check_embedded_triple(inner));
}
if let StarTerm::QuotedTriple(inner) = &triple.predicate {
violations.extend(self.check_embedded_triple(inner));
}
if let StarTerm::QuotedTriple(inner) = &triple.object {
violations.extend(self.check_embedded_triple(inner));
}
}
fn check_blank_subjects_in_graph(
&self,
triple: &StarTriple,
violations: &mut Vec<ComplianceViolation>,
) {
if let StarTerm::QuotedTriple(inner) = &triple.subject {
self.check_term_blank_as_embedded_subject(inner, violations);
}
if let StarTerm::QuotedTriple(inner) = &triple.object {
self.check_term_blank_as_embedded_subject(inner, violations);
}
}
fn check_graph_annotations(
&self,
graph: &[StarTriple],
violations: &mut Vec<ComplianceViolation>,
) {
for triple in graph {
if matches!(triple.subject, StarTerm::QuotedTriple(_))
&& !matches!(triple.predicate, StarTerm::NamedNode(_))
{
violations.push(ComplianceViolation::error(
"STAR-002",
format!(
"Annotation triple has non-IRI predicate: {:?}",
triple.predicate
),
));
}
}
}
pub fn check_triple(&self, triple: &StarTriple) -> Vec<ComplianceViolation> {
let mut violations = Vec::new();
self.check_embedded_triple_in_graph(triple, &mut violations);
self.check_blank_subjects_in_graph(triple, &mut violations);
violations.extend(self.check_nesting_depth(triple));
if matches!(triple.subject, StarTerm::QuotedTriple(_))
&& !matches!(triple.predicate, StarTerm::NamedNode(_))
{
violations.push(ComplianceViolation::error(
"STAR-002",
format!(
"Annotation triple has non-IRI predicate: {:?}",
triple.predicate
),
));
}
violations
}
}
#[cfg(test)]
fn iri(s: &str) -> StarTerm {
StarTerm::iri(s).expect("non-empty IRI")
}
#[cfg(test)]
fn lit(s: &str) -> StarTerm {
StarTerm::literal(s).expect("literal")
}
#[cfg(test)]
fn blank(id: &str) -> StarTerm {
StarTerm::blank_node(id).expect("non-empty blank node id")
}
#[cfg(test)]
fn var(name: &str) -> StarTerm {
StarTerm::variable(name).expect("non-empty variable name")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::StarTriple;
use crate::w3c_compliance::Annotation;
fn ground_triple() -> StarTriple {
StarTriple::new(
iri("http://example.org/s"),
iri("http://example.org/p"),
lit("42"),
)
}
#[test]
fn test_star001_ground_triple_passes() {
let checker = W3cRdfStarChecker::new();
let violations = checker.check_embedded_triple(&ground_triple());
assert!(
violations.is_empty(),
"ground triple should have no violations"
);
}
#[test]
fn test_star001_variable_subject_fails() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(var("s"), iri("http://example.org/p"), lit("42"));
let violations = checker.check_embedded_triple(&triple);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "STAR-001");
assert_eq!(violations[0].severity, Severity::Error);
}
#[test]
fn test_star001_variable_predicate_fails() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(iri("http://example.org/s"), var("p"), lit("42"));
let violations = checker.check_embedded_triple(&triple);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "STAR-001");
}
#[test]
fn test_star001_variable_object_fails() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(
iri("http://example.org/s"),
iri("http://example.org/p"),
var("o"),
);
let violations = checker.check_embedded_triple(&triple);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "STAR-001");
}
#[test]
fn test_star001_multiple_variables_all_reported() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(var("s"), var("p"), var("o"));
let violations = checker.check_embedded_triple(&triple);
assert_eq!(violations.len(), 3, "all three variable positions reported");
}
#[test]
fn test_star001_nested_variable_fails() {
let checker = W3cRdfStarChecker::new();
let inner = StarTriple::new(var("s"), iri("http://example.org/p"), lit("obj"));
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://example.org/p2"),
lit("val"),
);
let violations = checker.check_embedded_triple(&outer);
assert!(!violations.is_empty());
assert!(violations.iter().any(|v| v.rule_id == "STAR-001"));
}
#[test]
fn test_star001_blank_node_is_ground() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(blank("b1"), iri("http://example.org/p"), lit("42"));
let violations = checker.check_embedded_triple(&triple);
assert!(
violations.iter().all(|v| v.rule_id != "STAR-001"),
"blank nodes are ground terms"
);
}
#[test]
fn test_star002_valid_annotation() {
let checker = W3cRdfStarChecker::new();
let annotation = Annotation::new(
ground_triple(),
iri("http://example.org/certainty"),
lit("0.9"),
);
let violations = checker.check_annotation(&annotation);
assert!(violations.is_empty(), "valid annotation should pass");
}
#[test]
fn test_star002_literal_predicate_fails() {
let checker = W3cRdfStarChecker::new();
let annotation = Annotation::new(ground_triple(), lit("invalid_predicate"), lit("value"));
let violations = checker.check_annotation(&annotation);
assert!(!violations.is_empty());
assert!(violations.iter().any(|v| v.rule_id == "STAR-002"));
assert!(violations.iter().any(|v| v.severity == Severity::Error));
}
#[test]
fn test_star002_blank_predicate_fails() {
let checker = W3cRdfStarChecker::new();
let annotation = Annotation::new(ground_triple(), blank("pred_blank"), lit("value"));
let violations = checker.check_annotation(&annotation);
assert!(violations
.iter()
.any(|v| v.rule_id == "STAR-002" && v.severity == Severity::Error));
}
#[test]
fn test_star002_variable_object_is_warning() {
let checker = W3cRdfStarChecker::new();
let annotation =
Annotation::new(ground_triple(), iri("http://example.org/source"), var("x"));
let violations = checker.check_annotation(&annotation);
assert!(violations
.iter()
.any(|v| v.rule_id == "STAR-002" && v.severity == Severity::Warning));
assert!(!violations.iter().any(|v| v.severity == Severity::Error));
}
#[test]
fn test_star002_graph_annotation_triple() {
let checker = W3cRdfStarChecker::new();
let inner = ground_triple();
let valid_ann = StarTriple::new(
StarTerm::quoted_triple(inner.clone()),
iri("http://example.org/certainty"),
lit("high"),
);
let report = checker.check_graph(&[valid_ann]);
assert!(report.conformant);
let invalid_ann = StarTriple::new(
StarTerm::quoted_triple(inner),
lit("not-a-predicate"),
lit("high"),
);
let report = checker.check_graph(&[invalid_ann]);
assert!(!report.conformant);
assert!(report.errors().iter().any(|v| v.rule_id == "STAR-002"));
}
#[test]
fn test_star003_blank_subject_in_embedded_triple() {
let checker = W3cRdfStarChecker::new();
let inner = StarTriple::new(blank("b1"), iri("http://example.org/p"), lit("val"));
let violations = checker.check_embedded_blank_subject(&inner);
assert!(!violations.is_empty());
assert!(violations.iter().any(|v| v.rule_id == "STAR-003"));
assert!(violations.iter().any(|v| v.severity == Severity::Warning));
}
#[test]
fn test_star003_iri_subject_is_fine() {
let checker = W3cRdfStarChecker::new();
let violations = checker.check_embedded_blank_subject(&ground_triple());
assert!(violations.iter().all(|v| v.rule_id != "STAR-003"));
}
#[test]
fn test_star003_detected_in_graph_check() {
let checker = W3cRdfStarChecker::new();
let inner = StarTriple::new(blank("b1"), iri("http://example.org/p"), lit("val"));
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://example.org/meta"),
lit("x"),
);
let report = checker.check_graph(&[outer]);
assert!(report.violations.iter().any(|v| v.rule_id == "STAR-003"));
}
#[test]
fn test_star004_shallow_nesting_ok() {
let checker = W3cRdfStarChecker::new();
let inner = ground_triple();
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://example.org/p"),
lit("v"),
);
let violations = checker.check_nesting_depth(&outer);
assert!(
violations.iter().all(|v| v.rule_id != "STAR-004"),
"single-level nesting should not trigger STAR-004"
);
}
#[test]
fn test_star004_excessive_nesting_warns() {
let checker = W3cRdfStarChecker::with_max_nesting_depth(2);
let level1 = ground_triple();
let level2 = StarTriple::new(
StarTerm::quoted_triple(level1),
iri("http://example.org/p"),
lit("v"),
);
let level3 = StarTriple::new(
StarTerm::quoted_triple(level2),
iri("http://example.org/p"),
lit("v"),
);
let level4 = StarTriple::new(
StarTerm::quoted_triple(level3),
iri("http://example.org/p"),
lit("v"),
);
let violations = checker.check_nesting_depth(&level4);
assert!(!violations.is_empty(), "excessive nesting should warn");
assert!(violations
.iter()
.any(|v| v.rule_id == "STAR-004" && v.severity == Severity::Warning));
}
#[test]
fn test_check_graph_empty_is_conformant() {
let checker = W3cRdfStarChecker::new();
let report = checker.check_graph(&[]);
assert!(report.conformant);
assert!(report.violations.is_empty());
}
#[test]
fn test_check_graph_simple_ground_triples_conformant() {
let checker = W3cRdfStarChecker::new();
let triples = vec![
ground_triple(),
StarTriple::new(
iri("http://example.org/alice"),
iri("http://example.org/knows"),
iri("http://example.org/bob"),
),
];
let report = checker.check_graph(&triples);
assert!(report.conformant);
}
#[test]
fn test_check_graph_mixed_violations_collected() {
let checker = W3cRdfStarChecker::new();
let annotation_triple = StarTriple::new(
StarTerm::quoted_triple(ground_triple()),
iri("http://example.org/confidence"),
lit("high"),
);
let bad_inner = StarTriple::new(var("s"), iri("http://example.org/p"), lit("v"));
let bad_triple = StarTriple::new(
StarTerm::quoted_triple(bad_inner),
iri("http://example.org/source"),
iri("http://example.org/db"),
);
let report = checker.check_graph(&[annotation_triple, bad_triple]);
assert!(!report.conformant);
assert!(report.count_severity(&Severity::Error) > 0);
}
#[test]
fn test_compliance_report_count_severity() {
let violations = vec![
ComplianceViolation::error("STAR-001", "e1"),
ComplianceViolation::error("STAR-002", "e2"),
ComplianceViolation::warning("STAR-003", "w1"),
ComplianceViolation::info("STAR-004", "i1"),
];
let report = ComplianceReport::new(violations);
assert!(!report.conformant);
assert_eq!(report.count_severity(&Severity::Error), 2);
assert_eq!(report.count_severity(&Severity::Warning), 1);
assert_eq!(report.count_severity(&Severity::Info), 1);
}
#[test]
fn test_compliance_report_no_errors_is_conformant() {
let violations = vec![
ComplianceViolation::warning("STAR-003", "w1"),
ComplianceViolation::info("STAR-004", "i1"),
];
let report = ComplianceReport::new(violations);
assert!(
report.conformant,
"warnings/info alone should be conformant"
);
}
#[test]
fn test_check_triple_convenience_method() {
let checker = W3cRdfStarChecker::new();
let triple = StarTriple::new(
StarTerm::quoted_triple(ground_triple()),
iri("http://example.org/p"),
lit("v"),
);
let violations = checker.check_triple(&triple);
assert!(
violations.is_empty(),
"valid triple should have no violations"
);
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn test_checker_custom_depth() {
let checker = W3cRdfStarChecker::with_max_nesting_depth(1);
let inner = ground_triple();
let outer = StarTriple::new(
StarTerm::quoted_triple(inner),
iri("http://example.org/p"),
lit("v"),
);
let double = StarTriple::new(
StarTerm::quoted_triple(outer),
iri("http://example.org/p"),
lit("v"),
);
let violations = checker.check_nesting_depth(&double);
assert!(violations.iter().any(|v| v.rule_id == "STAR-004"));
}
#[test]
fn test_report_errors_and_warnings_accessors() {
let violations = vec![
ComplianceViolation::error("STAR-001", "err"),
ComplianceViolation::warning("STAR-003", "warn"),
];
let report = ComplianceReport::new(violations);
assert_eq!(report.errors().len(), 1);
assert_eq!(report.warnings().len(), 1);
}
}