use data_generator::config::OutputFormat;
use data_generator::{DataGenerator, GeneratorConfig};
use srdf::{Literal, NeighsRDF, RDFFormat, ReaderMode, SRDFGraph};
use std::collections::HashMap;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_shex_datatype_passthrough() {
let shex_schema = r#"
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:PersonShape {
ex:name xsd:string ;
ex:age xsd:integer ;
ex:active xsd:boolean
}
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shex_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 5;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shex_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let graph = SRDFGraph::from_path(
output_file.path(),
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.expect("Failed to parse generated RDF");
let mut datatype_counts = HashMap::new();
for triple in graph.triples().unwrap() {
if let oxrdf::Term::Literal(lit) = &triple.object {
let datatype = lit.datatype().to_string();
let clean_datatype = datatype.trim_start_matches('<').trim_end_matches('>');
*datatype_counts
.entry(clean_datatype.to_string())
.or_insert(0) += 1;
}
}
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#string"));
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#integer"));
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#boolean"));
}
#[tokio::test]
async fn test_shacl_datatype_passthrough() {
let shacl_schema = 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:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:score ;
sh:datatype xsd:decimal ;
sh:minCount 0 ;
sh:maxCount 1 ;
] .
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shacl_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 5;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shacl_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let graph = SRDFGraph::from_path(
output_file.path(),
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.expect("Failed to parse generated RDF");
let mut datatype_counts = HashMap::new();
for triple in graph.triples().unwrap() {
if let oxrdf::Term::Literal(lit) = &triple.object {
let datatype = lit.datatype().to_string();
let clean_datatype = if datatype.starts_with('<') && datatype.ends_with('>') {
datatype[1..datatype.len() - 1].to_string()
} else {
datatype
};
*datatype_counts.entry(clean_datatype).or_insert(0) += 1;
}
}
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#string"));
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#integer"));
assert!(datatype_counts.contains_key("http://www.w3.org/2001/XMLSchema#decimal"));
}
#[tokio::test]
async fn test_shex_cardinality_passthrough() {
let shex_schema = r#"
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:PersonShape {
ex:name xsd:string {1,1} ; # exactly one name
ex:email xsd:string {0,2} ; # zero to two emails
ex:phone xsd:string * # zero or more phones
}
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shex_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 10; config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shex_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let graph = SRDFGraph::from_path(
output_file.path(),
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.expect("Failed to parse generated RDF");
let mut entity_properties: HashMap<String, HashMap<String, u32>> = HashMap::new();
for triple in graph.triples().unwrap() {
let subject = triple.subject.to_string();
let predicate = triple.predicate.to_string();
entity_properties
.entry(subject)
.or_default()
.entry(predicate)
.and_modify(|count| *count += 1)
.or_insert(1);
}
for properties in entity_properties.values() {
if let Some(&name_count) = properties.get("http://example.org/name") {
assert_eq!(name_count, 1, "Entity should have exactly 1 name");
}
if let Some(&email_count) = properties.get("http://example.org/email") {
assert!(email_count <= 2, "Entity should have at most 2 emails");
}
}
}
#[tokio::test]
async fn test_shacl_cardinality_passthrough() {
let shacl_schema = 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:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:email ;
sh:datatype xsd:string ;
sh:minCount 0 ;
sh:maxCount 3 ;
] ;
sh:property [
sh:path ex:hobby ;
sh:datatype xsd:string ;
sh:minCount 2 ;
sh:maxCount 5 ;
] .
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shacl_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 8;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shacl_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let graph = SRDFGraph::from_path(
output_file.path(),
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.expect("Failed to parse generated RDF");
let mut entity_properties: HashMap<String, HashMap<String, u32>> = HashMap::new();
for triple in graph.triples().unwrap() {
let subject = triple.subject.to_string();
let predicate = triple.predicate.to_string();
entity_properties
.entry(subject)
.or_default()
.entry(predicate)
.and_modify(|count| *count += 1)
.or_insert(1);
}
for properties in entity_properties.values() {
if let Some(&name_count) = properties.get("http://example.org/name") {
assert_eq!(name_count, 1, "Entity should have exactly 1 name");
}
if let Some(&email_count) = properties.get("http://example.org/email") {
assert!(email_count <= 3, "Entity should have at most 3 emails");
}
if let Some(&hobby_count) = properties.get("http://example.org/hobby") {
assert!(
(2..=5).contains(&hobby_count),
"Entity should have 2-5 hobbies"
);
}
}
}
#[tokio::test]
async fn test_shex_shape_reference_passthrough() {
let shex_schema = r#"
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:PersonShape {
ex:name xsd:string ;
ex:address @ex:AddressShape
}
ex:AddressShape {
ex:street xsd:string ;
ex:city xsd:string ;
ex:country xsd:string
}
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shex_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 3;
config.output.format = OutputFormat::Turtle;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shex_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let generated_data = std::fs::read_to_string(output_file.path()).unwrap();
let graph = SRDFGraph::from_str(
&generated_data,
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.unwrap();
let triples = graph.triples().unwrap();
let mut has_person_name = false;
let mut has_address_street = false;
let mut has_address_city = false;
for triple in triples {
let predicate = triple.predicate.to_string();
match predicate.as_str() {
"<http://example.org/name>" => has_person_name = true,
"<http://example.org/street>" => has_address_street = true,
"<http://example.org/city>" => has_address_city = true,
_ => {}
}
}
assert!(has_person_name, "Should have person names");
assert!(has_address_street, "Should have address streets");
assert!(has_address_city, "Should have address cities");
}
#[tokio::test]
async fn test_shacl_shape_reference_passthrough() {
let shacl_schema = 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:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:address ;
sh:node ex:AddressShape ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
ex:AddressShape a sh:NodeShape ;
sh:property [
sh:path ex:street ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:city ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shacl_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 3;
config.output.format = OutputFormat::Turtle;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shacl_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let generated_data = std::fs::read_to_string(output_file.path()).unwrap();
let graph = SRDFGraph::from_str(
&generated_data,
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.unwrap();
let triples = graph.triples().unwrap();
let mut has_person_name = false;
let mut has_address_street = false;
let mut has_address_city = false;
for triple in triples {
let predicate = triple.predicate.to_string();
match predicate.as_str() {
"<http://example.org/name>" => has_person_name = true,
"<http://example.org/street>" => has_address_street = true,
"<http://example.org/city>" => has_address_city = true,
_ => {}
}
}
assert!(has_person_name, "Should have person names");
assert!(has_address_street, "Should have address streets");
assert!(has_address_city, "Should have address cities");
}
#[tokio::test]
async fn test_shacl_value_constraints_passthrough() {
let shacl_schema = 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:name ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:age ;
sh:datatype xsd:integer ;
sh:minCount 1 ;
sh:maxCount 1 ;
] ;
sh:property [
sh:path ex:status ;
sh:datatype xsd:string ;
sh:minCount 1 ;
sh:maxCount 1 ;
] .
"#;
let mut schema_file = NamedTempFile::new().unwrap();
writeln!(schema_file, "{shacl_schema}").unwrap();
let output_file = NamedTempFile::new().unwrap();
let mut config = GeneratorConfig::default();
config.generation.entity_count = 5;
config.output.format = OutputFormat::Turtle;
config.output.path = output_file.path().to_path_buf();
let mut generator = DataGenerator::new(config).unwrap();
generator
.load_shacl_schema(schema_file.path())
.await
.unwrap();
generator.generate().await.unwrap();
let generated_data = std::fs::read_to_string(output_file.path()).unwrap();
let graph = SRDFGraph::from_str(
&generated_data,
&RDFFormat::Turtle,
None,
&ReaderMode::Strict,
)
.unwrap();
let triples = graph.triples().unwrap();
for triple in triples {
let predicate = triple.predicate.to_string();
let object = triple.object;
match predicate.as_str() {
"http://example.org/name" => {
if let oxrdf::Term::Literal(literal) = object {
let value = literal.lexical_form();
assert!(
value.len() >= 2 && value.len() <= 50,
"Name length should be between 2 and 50 characters, got: {value}"
);
}
}
"http://example.org/age" => {
if let oxrdf::Term::Literal(literal) = object {
let value: i32 = literal.lexical_form().parse().unwrap();
assert!(
(0..=150).contains(&value),
"Age should be between 0 and 150, got: {value}"
);
}
}
"http://example.org/status" => {
if let oxrdf::Term::Literal(literal) = object {
let value = literal.lexical_form();
assert!(
["active", "inactive", "pending"].contains(&value),
"Status should be one of active/inactive/pending, got: {value}"
);
}
}
_ => {}
}
}
}