pub mod report_export;
pub mod validation;
pub use report_export::{ReportExportError, ShaclReportExporter, ShaclReportFormat};
use anyhow::Result;
use oxrdf::{Graph, NamedNode, NamedOrBlankNodeRef, TermRef};
use oxttl::TurtleParser;
use std::collections::HashMap;
use std::io::BufReader;
use tensorlogic_adapters::SymbolTable;
use tensorlogic_ir::{TLExpr, Term};
const SHACL_NS: &str = "http://www.w3.org/ns/shacl#";
#[derive(Debug, Clone)]
pub struct Shape {
pub target_class: Option<String>,
pub properties: Vec<PropertyConstraint>,
}
#[derive(Debug, Clone)]
pub struct PropertyConstraint {
pub path: String,
pub min_count: Option<u32>,
pub max_count: Option<u32>,
pub class: Option<String>,
pub datatype: Option<String>,
pub pattern: Option<String>,
pub min_length: Option<u32>,
pub max_length: Option<u32>,
pub min_inclusive: Option<f64>,
pub max_inclusive: Option<f64>,
pub in_values: Option<Vec<String>>,
pub node: Option<String>,
pub and: Option<Vec<String>>,
pub or: Option<Vec<String>>,
pub not: Option<String>,
pub xone: Option<Vec<String>>,
}
pub struct ShaclConverter {
pub symbol_table: SymbolTable,
shapes: HashMap<String, Shape>,
}
impl ShaclConverter {
pub fn new(symbol_table: SymbolTable) -> Self {
ShaclConverter {
symbol_table,
shapes: HashMap::new(),
}
}
pub fn parse_shapes(&mut self, shacl_turtle: &str) -> Result<()> {
let mut graph = Graph::new();
let reader = BufReader::new(shacl_turtle.as_bytes());
let parser = TurtleParser::new().for_reader(reader);
for result in parser {
let triple = result?;
graph.insert(&triple);
}
self.extract_shapes_from_graph(&graph)?;
Ok(())
}
fn extract_shapes_from_graph(&mut self, graph: &Graph) -> Result<()> {
let node_shape = NamedNode::new(format!("{}NodeShape", SHACL_NS))?;
let target_class = NamedNode::new(format!("{}targetClass", SHACL_NS))?;
let property_pred = NamedNode::new(format!("{}property", SHACL_NS))?;
let rdf_type = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
for triple in graph.iter() {
if triple.predicate == rdf_type.as_ref()
&& matches!(triple.object, TermRef::NamedNode(n) if n == node_shape.as_ref())
{
if let NamedOrBlankNodeRef::NamedNode(shape_node) = triple.subject {
let mut shape = Shape {
target_class: None,
properties: Vec::new(),
};
for t in graph.triples_for_subject(shape_node) {
if t.predicate == target_class.as_ref() {
if let TermRef::NamedNode(class_node) = t.object {
shape.target_class =
Some(self.extract_local_name(class_node.as_str()));
}
} else if t.predicate == property_pred.as_ref() {
if let TermRef::BlankNode(prop_node) = t.object {
if let Some(constraint) =
self.extract_property_constraint(graph, prop_node.as_str())?
{
shape.properties.push(constraint);
}
}
}
}
let shape_name = self.extract_local_name(shape_node.as_str());
self.shapes.insert(shape_name, shape);
}
}
}
Ok(())
}
fn extract_property_constraint(
&self,
graph: &Graph,
blank_id: &str,
) -> Result<Option<PropertyConstraint>> {
let path_pred = NamedNode::new(format!("{}path", SHACL_NS))?;
let min_count_pred = NamedNode::new(format!("{}minCount", SHACL_NS))?;
let max_count_pred = NamedNode::new(format!("{}maxCount", SHACL_NS))?;
let class_pred = NamedNode::new(format!("{}class", SHACL_NS))?;
let datatype_pred = NamedNode::new(format!("{}datatype", SHACL_NS))?;
let pattern_pred = NamedNode::new(format!("{}pattern", SHACL_NS))?;
let min_length_pred = NamedNode::new(format!("{}minLength", SHACL_NS))?;
let max_length_pred = NamedNode::new(format!("{}maxLength", SHACL_NS))?;
let min_inclusive_pred = NamedNode::new(format!("{}minInclusive", SHACL_NS))?;
let max_inclusive_pred = NamedNode::new(format!("{}maxInclusive", SHACL_NS))?;
let in_pred = NamedNode::new(format!("{}in", SHACL_NS))?;
let node_pred = NamedNode::new(format!("{}node", SHACL_NS))?;
let and_pred = NamedNode::new(format!("{}and", SHACL_NS))?;
let or_pred = NamedNode::new(format!("{}or", SHACL_NS))?;
let not_pred = NamedNode::new(format!("{}not", SHACL_NS))?;
let xone_pred = NamedNode::new(format!("{}xone", SHACL_NS))?;
let mut constraint = PropertyConstraint {
path: String::new(),
min_count: None,
max_count: None,
class: None,
datatype: None,
pattern: None,
min_length: None,
max_length: None,
min_inclusive: None,
max_inclusive: None,
in_values: None,
node: None,
and: None,
or: None,
not: None,
xone: None,
};
for triple in graph.iter() {
if let NamedOrBlankNodeRef::BlankNode(subj_blank) = triple.subject {
if subj_blank.as_str() == blank_id {
if triple.predicate == path_pred.as_ref() {
if let TermRef::NamedNode(path_node) = triple.object {
constraint.path = self.extract_local_name(path_node.as_str());
}
} else if triple.predicate == min_count_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.min_count = lit.value().parse().ok();
}
} else if triple.predicate == max_count_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.max_count = lit.value().parse().ok();
}
} else if triple.predicate == class_pred.as_ref() {
if let TermRef::NamedNode(class_node) = triple.object {
constraint.class = Some(self.extract_local_name(class_node.as_str()));
}
} else if triple.predicate == datatype_pred.as_ref() {
if let TermRef::NamedNode(datatype_node) = triple.object {
constraint.datatype =
Some(self.extract_local_name(datatype_node.as_str()));
}
} else if triple.predicate == pattern_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.pattern = Some(lit.value().to_string());
}
} else if triple.predicate == min_length_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.min_length = lit.value().parse().ok();
}
} else if triple.predicate == max_length_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.max_length = lit.value().parse().ok();
}
} else if triple.predicate == min_inclusive_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.min_inclusive = lit.value().parse().ok();
}
} else if triple.predicate == max_inclusive_pred.as_ref() {
if let TermRef::Literal(lit) = triple.object {
constraint.max_inclusive = lit.value().parse().ok();
}
} else if triple.predicate == in_pred.as_ref() {
let mut values = Vec::new();
if let TermRef::BlankNode(list_node) = triple.object {
values.extend(self.extract_rdf_list(graph, list_node.as_str()));
}
if !values.is_empty() {
constraint.in_values = Some(values);
}
} else if triple.predicate == node_pred.as_ref() {
if let TermRef::NamedNode(node_shape) = triple.object {
constraint.node = Some(self.extract_local_name(node_shape.as_str()));
}
} else if triple.predicate == and_pred.as_ref() {
let mut shapes = Vec::new();
if let TermRef::BlankNode(list_node) = triple.object {
shapes.extend(self.extract_rdf_list(graph, list_node.as_str()));
}
if !shapes.is_empty() {
constraint.and = Some(shapes);
}
} else if triple.predicate == or_pred.as_ref() {
let mut shapes = Vec::new();
if let TermRef::BlankNode(list_node) = triple.object {
shapes.extend(self.extract_rdf_list(graph, list_node.as_str()));
}
if !shapes.is_empty() {
constraint.or = Some(shapes);
}
} else if triple.predicate == not_pred.as_ref() {
if let TermRef::NamedNode(shape_node) = triple.object {
constraint.not = Some(self.extract_local_name(shape_node.as_str()));
}
} else if triple.predicate == xone_pred.as_ref() {
let mut shapes = Vec::new();
if let TermRef::BlankNode(list_node) = triple.object {
shapes.extend(self.extract_rdf_list(graph, list_node.as_str()));
}
if !shapes.is_empty() {
constraint.xone = Some(shapes);
}
}
}
}
}
if !constraint.path.is_empty() {
Ok(Some(constraint))
} else {
Ok(None)
}
}
fn extract_local_name(&self, iri: &str) -> String {
iri.split(['/', '#']).next_back().unwrap_or(iri).to_string()
}
fn extract_rdf_list(&self, graph: &Graph, list_id: &str) -> Vec<String> {
let mut values = Vec::new();
let rdf_first = match NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#first") {
Ok(n) => n,
Err(_) => return values,
};
let rdf_rest = match NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest") {
Ok(n) => n,
Err(_) => return values,
};
let rdf_nil = match NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil") {
Ok(n) => n,
Err(_) => return values,
};
let mut current = list_id.to_string();
loop {
let mut found_first = false;
let mut next_node = None;
for triple in graph.iter() {
if let NamedOrBlankNodeRef::BlankNode(subj) = triple.subject {
if subj.as_str() == current {
if triple.predicate == rdf_first.as_ref() {
match triple.object {
TermRef::NamedNode(n) => {
values.push(self.extract_local_name(n.as_str()));
found_first = true;
}
TermRef::Literal(lit) => {
values.push(lit.value().to_string());
found_first = true;
}
_ => {}
}
} else if triple.predicate == rdf_rest.as_ref() {
match triple.object {
TermRef::BlankNode(rest) => {
next_node = Some(rest.as_str().to_string());
}
TermRef::NamedNode(n) if n == rdf_nil.as_ref() => {
return values;
}
_ => {}
}
}
}
}
}
if !found_first {
break;
}
if let Some(next) = next_node {
current = next;
} else {
break;
}
}
values
}
pub fn convert_to_rules(&self, shacl_data: &str) -> Result<Vec<TLExpr>> {
let mut converter = Self::new(self.symbol_table.clone());
converter.parse_shapes(shacl_data)?;
let mut rules = Vec::new();
for shape in converter.shapes.values() {
for prop_constraint in &shape.properties {
if let Some(min_count) = prop_constraint.min_count {
if min_count >= 1 {
let predicate = TLExpr::pred(
&prop_constraint.path,
vec![Term::var("x"), Term::var("y")],
);
let exists_rule = TLExpr::exists("y", "Value", predicate);
rules.push(exists_rule);
}
}
if let Some(max_count) = prop_constraint.max_count {
if max_count == 1 {
let prop_y = TLExpr::pred(
&prop_constraint.path,
vec![Term::var("x"), Term::var("y")],
);
let prop_z = TLExpr::pred(
&prop_constraint.path,
vec![Term::var("x"), Term::var("z")],
);
let both_exist = TLExpr::and(prop_y, prop_z);
let distinct =
TLExpr::pred("distinct", vec![Term::var("y"), Term::var("z")]);
let not_distinct = TLExpr::negate(distinct);
let uniqueness_rule = TLExpr::imply(both_exist, not_distinct);
rules.push(uniqueness_rule);
}
}
if let Some(ref class_name) = prop_constraint.class {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let type_pred =
TLExpr::pred("hasType", vec![Term::var("y"), Term::constant(class_name)]);
let type_rule = TLExpr::imply(property_pred, type_pred);
rules.push(type_rule);
}
if let Some(ref datatype_name) = prop_constraint.datatype {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let datatype_pred = TLExpr::pred(
"hasDatatype",
vec![Term::var("y"), Term::constant(datatype_name)],
);
let datatype_rule = TLExpr::imply(property_pred, datatype_pred);
rules.push(datatype_rule);
}
if let Some(ref pattern) = prop_constraint.pattern {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let pattern_pred = TLExpr::pred(
"matchesPattern",
vec![Term::var("y"), Term::constant(pattern)],
);
let pattern_rule = TLExpr::imply(property_pred, pattern_pred);
rules.push(pattern_rule);
}
if let Some(min_len) = prop_constraint.min_length {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let length_pred = TLExpr::pred(
"lengthAtLeast",
vec![Term::var("y"), Term::constant(min_len.to_string())],
);
let length_rule = TLExpr::imply(property_pred, length_pred);
rules.push(length_rule);
}
if let Some(max_len) = prop_constraint.max_length {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let length_pred = TLExpr::pred(
"lengthAtMost",
vec![Term::var("y"), Term::constant(max_len.to_string())],
);
let length_rule = TLExpr::imply(property_pred, length_pred);
rules.push(length_rule);
}
if let Some(min_val) = prop_constraint.min_inclusive {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let range_pred = TLExpr::pred(
"greaterOrEqual",
vec![Term::var("y"), Term::constant(min_val.to_string())],
);
let range_rule = TLExpr::imply(property_pred, range_pred);
rules.push(range_rule);
}
if let Some(max_val) = prop_constraint.max_inclusive {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let range_pred = TLExpr::pred(
"lessOrEqual",
vec![Term::var("y"), Term::constant(max_val.to_string())],
);
let range_rule = TLExpr::imply(property_pred, range_pred);
rules.push(range_rule);
}
if let Some(ref values) = prop_constraint.in_values {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
if !values.is_empty() {
let mut or_expr = TLExpr::pred(
"equals",
vec![Term::var("y"), Term::constant(&values[0])],
);
for val in &values[1..] {
let eq =
TLExpr::pred("equals", vec![Term::var("y"), Term::constant(val)]);
or_expr = TLExpr::or(or_expr, eq);
}
let in_rule = TLExpr::imply(property_pred, or_expr);
rules.push(in_rule);
}
}
if let Some(ref node_shape) = prop_constraint.node {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let node_pred = TLExpr::pred(
"nodeConformsTo",
vec![Term::var("y"), Term::constant(node_shape)],
);
let node_rule = TLExpr::imply(property_pred, node_pred);
rules.push(node_rule);
}
if let Some(ref and_shapes) = prop_constraint.and {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
if !and_shapes.is_empty() {
let mut and_expr = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(&and_shapes[0])],
);
for shape in &and_shapes[1..] {
let conforms = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(shape)],
);
and_expr = TLExpr::and(and_expr, conforms);
}
let and_rule = TLExpr::imply(property_pred, and_expr);
rules.push(and_rule);
}
}
if let Some(ref or_shapes) = prop_constraint.or {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
if !or_shapes.is_empty() {
let mut or_expr = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(&or_shapes[0])],
);
for shape in &or_shapes[1..] {
let conforms = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(shape)],
);
or_expr = TLExpr::or(or_expr, conforms);
}
let or_rule = TLExpr::imply(property_pred, or_expr);
rules.push(or_rule);
}
}
if let Some(ref not_shape) = prop_constraint.not {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
let conforms = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(not_shape)],
);
let not_expr = TLExpr::negate(conforms);
let not_rule = TLExpr::imply(property_pred, not_expr);
rules.push(not_rule);
}
if let Some(ref xone_shapes) = prop_constraint.xone {
let property_pred =
TLExpr::pred(&prop_constraint.path, vec![Term::var("x"), Term::var("y")]);
if !xone_shapes.is_empty() {
let mut or_expr = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(&xone_shapes[0])],
);
for shape in &xone_shapes[1..] {
let conforms = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(shape)],
);
or_expr = TLExpr::or(or_expr, conforms);
}
let mut exclusion_expr = None;
for i in 0..xone_shapes.len() {
for j in (i + 1)..xone_shapes.len() {
let conf_i = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(&xone_shapes[i])],
);
let conf_j = TLExpr::pred(
"conformsTo",
vec![Term::var("y"), Term::constant(&xone_shapes[j])],
);
let both = TLExpr::and(conf_i, conf_j);
let not_both = TLExpr::negate(both);
exclusion_expr = Some(match exclusion_expr {
None => not_both,
Some(acc) => TLExpr::and(acc, not_both),
});
}
}
let xone_expr = match exclusion_expr {
Some(excl) => TLExpr::and(or_expr, excl),
None => or_expr, };
let xone_rule = TLExpr::imply(property_pred, xone_expr);
rules.push(xone_rule);
}
}
}
}
Ok(rules)
}
pub fn shapes(&self) -> &HashMap<String, Shape> {
&self.shapes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shacl_converter_basic() {
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
assert!(converter.shapes().is_empty());
}
#[test]
fn test_parse_simple_shacl() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
] .
"#;
let symbol_table = SymbolTable::new();
let mut converter = ShaclConverter::new(symbol_table);
let result = converter.parse_shapes(shacl_turtle);
assert!(result.is_ok());
}
#[test]
fn test_convert_mincount_to_exists() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:name ;
sh:minCount 1 ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty());
}
#[test]
fn test_maxcount_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:email ;
sh:maxCount 1 ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty());
let rule_str = format!("{:?}", rules[0]);
assert!(rule_str.contains("distinct") || rule_str.contains("Not"));
}
#[test]
fn test_datatype_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty());
let rule_str = format!("{:?}", rules[0]);
assert!(rule_str.contains("hasDatatype") || rule_str.contains("integer"));
}
#[test]
fn test_pattern_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:EmailShape a sh:NodeShape ;
sh:targetClass ex:Email ;
sh:property [
sh:path ex:address ;
sh:pattern "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty());
let rule_str = format!("{:?}", rules[0]);
assert!(rule_str.contains("matchesPattern"));
}
#[test]
fn test_multiple_constraints() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:email ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
sh:pattern ".*@.*" ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(
rules.len() >= 4,
"Expected at least 4 rules, got {}",
rules.len()
);
}
#[test]
fn test_class_and_datatype_together() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:BookShape a sh:NodeShape ;
sh:targetClass ex:Book ;
sh:property [
sh:path ex:author ;
sh:class ex:Person ;
] ;
sh:property [
sh:path ex:isbn ;
sh:datatype ex:ISBN ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(rules.len() >= 2, "Expected at least 2 rules");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("hasType"));
assert!(all_rules.contains("hasDatatype"));
}
#[test]
fn test_min_max_length_constraints() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:StringShape a sh:NodeShape ;
sh:targetClass ex:Text ;
sh:property [
sh:path ex:value ;
sh:minLength 5 ;
sh:maxLength 100 ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(rules.len() >= 2, "Expected at least 2 rules");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("lengthAtLeast"));
assert!(all_rules.contains("lengthAtMost"));
}
#[test]
fn test_min_max_inclusive_constraints() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:RangeShape a sh:NodeShape ;
sh:targetClass ex:Number ;
sh:property [
sh:path ex:value ;
sh:minInclusive 0.0 ;
sh:maxInclusive 100.0 ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(rules.len() >= 2, "Expected at least 2 rules");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("greaterOrEqual"));
assert!(all_rules.contains("lessOrEqual"));
}
#[test]
fn test_in_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:EnumShape a sh:NodeShape ;
sh:targetClass ex:Status ;
sh:property [
sh:path ex:state ;
sh:in ( "pending" "approved" "rejected" ) ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty(), "Expected at least 1 rule");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("equals") || all_rules.contains("Or"));
}
#[test]
fn test_node_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [
sh:path ex:address ;
sh:node ex:AddressShape ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty(), "Expected at least 1 rule");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("nodeConformsTo"));
}
#[test]
fn test_and_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:CompositeShape a sh:NodeShape ;
sh:targetClass ex:Entity ;
sh:property [
sh:path ex:value ;
sh:and ( ex:Shape1 ex:Shape2 ) ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
let all_rules = format!("{:?}", rules);
if !rules.is_empty() {
assert!(all_rules.contains("conformsTo") || all_rules.contains("Shape"));
}
}
#[test]
fn test_or_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:DisjunctiveShape a sh:NodeShape ;
sh:targetClass ex:Entity ;
sh:property [
sh:path ex:value ;
sh:or ( ex:Shape1 ex:Shape2 ) ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
let all_rules = format!("{:?}", rules);
if !rules.is_empty() {
assert!(all_rules.contains("conformsTo") || all_rules.contains("Shape"));
}
}
#[test]
fn test_not_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
ex:NegationShape a sh:NodeShape ;
sh:targetClass ex:Entity ;
sh:property [
sh:path ex:value ;
sh:not ex:ForbiddenShape ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(!rules.is_empty(), "Expected at least 1 rule");
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("conformsTo"));
assert!(all_rules.contains("Not"));
}
#[test]
fn test_xone_constraint() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:XoneShape a sh:NodeShape ;
sh:targetClass ex:Entity ;
sh:property [
sh:path ex:value ;
sh:xone ( ex:Shape1 ex:Shape2 ) ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
let all_rules = format!("{:?}", rules);
if !rules.is_empty() {
assert!(all_rules.contains("conformsTo") || all_rules.contains("Shape"));
}
}
#[test]
fn test_complex_combined_constraints() {
let shacl_turtle = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:ComplexShape a sh:NodeShape ;
sh:targetClass ex:Product ;
sh:property [
sh:path ex:name ;
sh:minLength 3 ;
sh:maxLength 50 ;
sh:datatype xsd:string ;
] ;
sh:property [
sh:path ex:price ;
sh:minInclusive 0.0 ;
sh:maxInclusive 1000.0 ;
sh:datatype xsd:decimal ;
] ;
sh:property [
sh:path ex:category ;
sh:in ( "electronics" "books" "clothing" ) ;
] .
"#;
let symbol_table = SymbolTable::new();
let converter = ShaclConverter::new(symbol_table);
let rules = converter.convert_to_rules(shacl_turtle).expect("unwrap");
assert!(
rules.len() >= 6,
"Expected at least 6 rules, got {}",
rules.len()
);
let all_rules = format!("{:?}", rules);
assert!(all_rules.contains("lengthAtLeast"));
assert!(all_rules.contains("lengthAtMost"));
assert!(all_rules.contains("greaterOrEqual"));
assert!(all_rules.contains("lessOrEqual"));
assert!(all_rules.contains("equals") || all_rules.contains("Or"));
}
}