use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::Term;
use oxigraph::sparql::QueryResults;
use oxigraph::store::Store;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DialectResult {
pub conforms: bool,
pub message: String,
pub supported: bool,
}
#[allow(deprecated)]
pub fn check_sparql(sparql: &str) -> Result<DialectResult, String> {
let store = Store::new().map_err(|e| e.to_string())?;
let seed_ttl = "<http://www.w3.org/ns/prov#s> <http://www.w3.org/ns/prov#p> <http://www.w3.org/ns/prov#o> .";
store
.load_from_reader(
RdfParser::from_format(RdfFormat::Turtle),
seed_ttl.as_bytes(),
)
.map_err(|e| e.to_string())?;
let query = sparql.trim();
let upper = query.to_uppercase();
if !upper.starts_with("ASK")
&& !upper.starts_with("SELECT")
&& !upper.starts_with("CONSTRUCT")
&& !upper.contains("PREFIX")
{
return Err("Invalid SPARQL query: must be ASK, SELECT, or CONSTRUCT".to_string());
}
let parsed_query = store
.query(query)
.map_err(|e| format!("SPARQL parse error: {}", e))?;
match parsed_query {
QueryResults::Boolean(b) => Ok(DialectResult {
conforms: b,
message: format!("SPARQL ASK returned {}", b),
supported: true,
}),
QueryResults::Solutions(solutions) => {
let count = solutions.count();
Ok(DialectResult {
conforms: count > 0,
message: format!("SPARQL SELECT returned {} rows", count),
supported: true,
})
}
QueryResults::Graph(triples) => {
let count = triples.count();
Ok(DialectResult {
conforms: count > 0,
message: format!("SPARQL CONSTRUCT returned {} triples", count),
supported: true,
})
}
}
}
#[allow(deprecated)]
pub fn check_shacl(shacl: &str) -> Result<DialectResult, String> {
let store = Store::new().map_err(|e| e.to_string())?;
store
.load_from_reader(RdfParser::from_format(RdfFormat::Turtle), shacl.as_bytes())
.map_err(|e| e.to_string())?;
let shape_query = "
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?shape WHERE {
?shape a sh:NodeShape .
}
";
let mut shapes = Vec::new();
let query_results = store.query(shape_query).map_err(|e| e.to_string())?;
if let QueryResults::Solutions(solutions) = query_results {
for sol in solutions {
let sol = sol.map_err(|e| e.to_string())?;
if let Some(Term::NamedNode(n)) = sol.get("shape") {
shapes.push(n.clone());
}
}
}
if shapes.is_empty() {
return Err("No valid SHACL NodeShapes found".to_string());
}
let mut violations: Vec<String> = Vec::new();
for shape in &shapes {
let target_query = format!(
"PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?target WHERE {{ <{}> sh:targetClass ?target }}",
shape.as_str()
);
let mut target_classes: Vec<String> = Vec::new();
let target_results = store.query(&target_query).map_err(|e| e.to_string())?;
if let QueryResults::Solutions(solutions) = target_results {
for sol in solutions.flatten() {
if let Some(Term::NamedNode(n)) = sol.get("target") {
target_classes.push(n.as_str().to_string());
}
}
}
if target_classes.is_empty() {
target_classes.push(shape.as_str().to_string());
}
let mut instances: Vec<String> = Vec::new();
for target_class in &target_classes {
let inst_query = format!("SELECT ?inst WHERE {{ ?inst a <{}> }}", target_class);
let inst_results = store.query(&inst_query).map_err(|e| e.to_string())?;
if let QueryResults::Solutions(solutions) = inst_results {
for sol in solutions.flatten() {
if let Some(Term::NamedNode(n)) = sol.get("inst") {
instances.push(n.as_str().to_string());
}
}
}
}
let prop_query = format!(
"PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?path ?min WHERE {{ <{}> sh:property [ sh:path ?path ; sh:minCount ?min ] }}",
shape.as_str()
);
let prop_results = store.query(&prop_query).map_err(|e| e.to_string())?;
let mut required_props: Vec<(String, i64)> = Vec::new();
if let QueryResults::Solutions(solutions) = prop_results {
for sol in solutions.flatten() {
let path = sol.get("path").map(|t| t.to_string()).unwrap_or_default();
let min = if let Some(Term::Literal(l)) = sol.get("min") {
l.value().parse::<i64>().unwrap_or(0)
} else {
0
};
if min > 0 {
required_props.push((path, min));
}
}
}
for inst in &instances {
for (prop, min) in &required_props {
let prop_iri = prop.trim_start_matches('<').trim_end_matches('>');
let count_query = format!(
"SELECT (COUNT(?v) AS ?c) WHERE {{ <{}> <{}> ?v }}",
inst, prop_iri
);
let count_res = store.query(&count_query).map_err(|e| e.to_string())?;
if let QueryResults::Solutions(mut sols) = count_res {
if let Some(Ok(sol)) = sols.next() {
let count = if let Some(Term::Literal(l)) = sol.get("c") {
l.value().parse::<i64>().unwrap_or(0)
} else {
0
};
if count < *min {
violations.push(format!(
"<{}> violates shape <{}>: property <{}> requires minCount {} but found {}",
inst, shape.as_str(), prop_iri, min, count
));
}
}
}
}
}
}
if violations.is_empty() {
Ok(DialectResult {
conforms: true,
message: format!("SHACL verification passed ({} shapes found)", shapes.len()),
supported: true,
})
} else {
Ok(DialectResult {
conforms: false,
message: format!(
"SHACL: {} violation(s) — instance violates shape: {}",
violations.len(),
violations[0]
),
supported: true,
})
}
}
pub fn check_n3(n3: &str) -> Result<DialectResult, String> {
let is_n3_formula = n3.contains("=>") || n3.contains("@forAll");
let mut depth: i64 = 0;
for ch in n3.chars() {
match ch {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
if depth < 0 {
return Err("Unbalanced braces in N3 input".to_string());
}
}
if depth != 0 {
return Err("Unbalanced braces in N3 input".to_string());
}
if !is_n3_formula {
let store = Store::new().map_err(|e| e.to_string())?;
store
.load_from_reader(RdfParser::from_format(RdfFormat::Turtle), n3.as_bytes())
.map_err(|e| format!("N3/Turtle parse error: {}", e))?;
}
Ok(DialectResult {
conforms: false,
message: "N3 syntax verified (valid), but rule execution is an unsupported capability in this engine".to_string(),
supported: false,
})
}
pub fn check_datalog(datalog: &str) -> Result<DialectResult, String> {
let trimmed = datalog.trim();
if trimmed.is_empty() {
return Err("Empty Datalog input".to_string());
}
let mut rules_count = 0;
for line in trimmed.lines() {
let l = line.trim();
if l.is_empty() || l.starts_with('%') || l.starts_with('#') {
continue;
}
let open = l.chars().filter(|&c| c == '(').count() as i64;
let close = l.chars().filter(|&c| c == ')').count() as i64;
if open != close {
return Err(format!("Mismatched parentheses in Datalog at: {}", l));
}
if l.contains(":-") && l.ends_with('.') {
rules_count += 1;
} else if l.contains('(') && l.ends_with('.') {
rules_count += 1;
} else if !l.is_empty() {
return Err(format!("Invalid Datalog syntax at: {}", l));
}
}
if rules_count == 0 {
return Err("No valid Datalog rules or facts found".to_string());
}
Ok(DialectResult {
conforms: false,
message: "Datalog syntax verified (valid), but execution is an unsupported capability in this engine".to_string(),
supported: false,
})
}
pub fn check_shex(shex: &str) -> Result<DialectResult, String> {
let trimmed = shex.trim();
if trimmed.is_empty() {
return Err("Empty ShEx input".to_string());
}
let mut shapes_count = 0;
let mut open_braces = 0;
for line in trimmed.lines() {
let l = line.trim();
if l.is_empty() || l.starts_with('#') || l.starts_with("PREFIX") || l.starts_with("BASE") {
continue;
}
if l.contains('{') {
open_braces += 1;
shapes_count += 1;
}
if l.contains('}') {
open_braces -= 1;
}
}
if open_braces != 0 {
return Err("Unbalanced braces in ShEx input".to_string());
}
if shapes_count == 0 && !shex.trim().is_empty() {
return Err("No valid shape definitions found in ShEx input".to_string());
}
Ok(DialectResult {
conforms: false,
message: "ShEx syntax verified (valid), but execution is an unsupported capability in this engine".to_string(),
supported: false,
})
}