use crate::RdfStore;
use anyhow::Result;
use juniper::{
EmptyMutation, EmptySubscription, FieldResult, GraphQLInputObject, GraphQLObject, GraphQLUnion,
RootNode, ID,
};
use oxirs_core::model::{Term, Variable};
use oxirs_core::query::QueryResults;
use std::sync::Arc;
pub type IRI = String;
pub type RdfLiteral = String;
#[derive(Debug, Clone, GraphQLUnion)]
#[graphql(description = "An RDF term which can be an IRI, Literal, or Blank Node")]
pub enum RdfTerm {
NamedNode(RdfNamedNode),
Literal(RdfLiteralNode),
BlankNode(RdfBlankNode),
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "An RDF Named Node (IRI)")]
pub struct RdfNamedNode {
pub iri: IRI,
pub label: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "An RDF Literal value")]
pub struct RdfLiteralNode {
pub literal: RdfLiteral,
pub value: String,
pub language: Option<String>,
pub datatype: Option<IRI>,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "An RDF Blank Node")]
pub struct RdfBlankNode {
pub id: ID,
pub label: String,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "An RDF Triple (subject-predicate-object statement)")]
pub struct RdfTriple {
pub subject: RdfTerm,
pub predicate: RdfNamedNode,
pub object: RdfTerm,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "An RDF Quad (triple + named graph)")]
pub struct RdfQuad {
pub subject: RdfTerm,
pub predicate: RdfNamedNode,
pub object: RdfTerm,
pub graph: Option<RdfNamedNode>,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "A single row from a SPARQL query result set")]
pub struct SparqlResultRow {
pub bindings: Vec<SparqlBinding>,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "A variable binding in a SPARQL result")]
pub struct SparqlBinding {
pub variable: String,
pub value: RdfTerm,
}
#[derive(Debug, Clone, GraphQLUnion)]
#[graphql(description = "Result of a SPARQL query")]
pub enum SparqlResult {
Solutions(SparqlSolutions),
Boolean(SparqlBoolean),
Graph(SparqlGraph),
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "Results from a SPARQL SELECT query")]
pub struct SparqlSolutions {
pub variables: Vec<String>,
pub rows: Vec<SparqlResultRow>,
pub count: i32,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "Result from a SPARQL ASK query")]
pub struct SparqlBoolean {
pub result: bool,
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "Graph results from a SPARQL CONSTRUCT or DESCRIBE query")]
pub struct SparqlGraph {
pub triples: Vec<RdfTriple>,
pub count: i32,
}
#[derive(Debug, Clone, GraphQLInputObject)]
#[graphql(description = "Input for executing SPARQL queries")]
pub struct SparqlQueryInput {
pub query: String,
pub limit: Option<i32>,
pub offset: Option<i32>,
}
#[derive(Debug, Clone, GraphQLInputObject)]
#[graphql(description = "Filters for querying RDF data")]
pub struct RdfQueryFilter {
pub subject: Option<String>,
pub predicate: Option<String>,
pub object: Option<String>,
pub graph: Option<String>,
pub limit: Option<i32>,
pub offset: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct GraphQLContext {
pub store: Arc<RdfStore>,
}
impl juniper::Context for GraphQLContext {}
pub struct Query;
#[juniper::graphql_object(context = GraphQLContext)]
impl Query {
fn info(context: &GraphQLContext) -> FieldResult<StoreInfo> {
let count = context.store.triple_count().unwrap_or(0);
Ok(StoreInfo {
triple_count: count as i32,
version: env!("CARGO_PKG_VERSION").to_string(),
description: "OxiRS GraphQL endpoint for RDF data".to_string(),
})
}
fn sparql(context: &GraphQLContext, input: SparqlQueryInput) -> FieldResult<SparqlResult> {
let results = context.store.query(&input.query)?;
Ok(convert_sparql_results(results)?)
}
fn triples(
context: &GraphQLContext,
filter: Option<RdfQueryFilter>,
) -> FieldResult<Vec<RdfTriple>> {
let filter = filter.unwrap_or_default();
let query = build_select_query(&filter);
let results = context.store.query(&query)?;
match results {
QueryResults::Solutions(solutions) => {
let mut triples = Vec::new();
let s_var = Variable::new("s")?;
let p_var = Variable::new("p")?;
let o_var = Variable::new("o")?;
for solution in solutions {
if let (Some(s), Some(p), Some(o)) = (
solution.get(&s_var),
solution.get(&p_var),
solution.get(&o_var),
) {
triples.push(RdfTriple {
subject: convert_term_to_rdf_term(s.clone()),
predicate: convert_named_node(p.clone())?,
object: convert_term_to_rdf_term(o.clone()),
});
}
}
Ok(triples)
}
_ => Ok(Vec::new()),
}
}
fn subjects(context: &GraphQLContext, limit: Option<i32>) -> FieldResult<Vec<RdfNamedNode>> {
let limit_usize = limit.map(|l| l as usize);
let subjects = context.store.get_subjects(limit_usize)?;
Ok(subjects
.into_iter()
.map(|s| RdfNamedNode {
iri: s.clone(),
label: None,
description: None,
})
.collect())
}
fn predicates(context: &GraphQLContext, limit: Option<i32>) -> FieldResult<Vec<RdfNamedNode>> {
let limit_usize = limit.map(|l| l as usize);
let predicates = context.store.get_predicates(limit_usize)?;
Ok(predicates
.into_iter()
.map(|p| RdfNamedNode {
iri: p.clone(),
label: None,
description: None,
})
.collect())
}
fn search(
context: &GraphQLContext,
pattern: String,
limit: Option<i32>,
) -> FieldResult<Vec<RdfNamedNode>> {
let limit_clause = limit.map(|l| format!(" LIMIT {l}")).unwrap_or_default();
let query = format!(
r#"
SELECT DISTINCT ?resource WHERE {{
{{
?resource ?p ?o .
FILTER(CONTAINS(STR(?resource), "{pattern}"))
}} UNION {{
?resource rdfs:label ?label .
FILTER(CONTAINS(LCASE(STR(?label)), LCASE("{pattern}")))
}}
}}{limit_clause}
"#
);
let results = context.store.query(&query)?;
match results {
QueryResults::Solutions(solutions) => {
let mut resources = Vec::new();
let resource_var = Variable::new("resource")?;
for solution in solutions {
if let Some(Term::NamedNode(node)) = solution.get(&resource_var) {
resources.push(RdfNamedNode {
iri: node.to_string(),
label: None,
description: None,
});
}
}
Ok(resources)
}
_ => Ok(Vec::new()),
}
}
}
#[derive(Debug, Clone, GraphQLObject)]
#[graphql(description = "Information about the RDF store")]
pub struct StoreInfo {
pub triple_count: i32,
pub version: String,
pub description: String,
}
pub type Schema = RootNode<Query, EmptyMutation<GraphQLContext>, EmptySubscription<GraphQLContext>>;
pub fn create_schema() -> Schema {
Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
}
impl Default for RdfQueryFilter {
fn default() -> Self {
Self {
subject: None,
predicate: None,
object: None,
graph: None,
limit: Some(100),
offset: Some(0),
}
}
}
fn convert_sparql_results(results: QueryResults) -> Result<SparqlResult> {
match results {
QueryResults::Solutions(solutions) => {
let mut variables = Vec::new();
let mut rows = Vec::new();
for solution in solutions {
if variables.is_empty() {
variables = solution.variables().map(|v| v.to_string()).collect();
}
let mut bindings = Vec::new();
for var in solution.variables() {
if let Some(term) = solution.get(var) {
bindings.push(SparqlBinding {
variable: var.to_string(),
value: convert_term_to_rdf_term(term.clone()),
});
}
}
rows.push(SparqlResultRow { bindings });
}
Ok(SparqlResult::Solutions(SparqlSolutions {
variables,
count: rows.len() as i32,
rows,
}))
}
QueryResults::Boolean(b) => Ok(SparqlResult::Boolean(SparqlBoolean { result: b })),
QueryResults::Graph(_graph) => {
Ok(SparqlResult::Graph(SparqlGraph {
triples: Vec::new(),
count: 0,
}))
}
}
}
fn convert_term_to_rdf_term(term: Term) -> RdfTerm {
match term {
Term::NamedNode(node) => RdfTerm::NamedNode(RdfNamedNode {
iri: node.to_string(),
label: None,
description: None,
}),
Term::Literal(literal) => {
let rdf_literal = literal.value().to_string();
RdfTerm::Literal(RdfLiteralNode {
literal: rdf_literal.clone(),
value: rdf_literal,
language: literal.language().map(|l| l.to_string()),
datatype: if literal.datatype().as_str()
!= "http://www.w3.org/2001/XMLSchema#string"
{
Some(literal.datatype().to_string())
} else {
None
},
})
}
Term::BlankNode(node) => RdfTerm::BlankNode(RdfBlankNode {
id: ID::new(format!("_:{node}")),
label: format!("_:{node}"),
}),
Term::QuotedTriple(_) => {
RdfTerm::NamedNode(RdfNamedNode {
iri: "rdf-star:triple".to_string(),
label: Some("RDF-star Triple".to_string()),
description: Some("An RDF-star quoted triple".to_string()),
})
}
Term::Variable(var) => {
RdfTerm::NamedNode(RdfNamedNode {
iri: format!("var:{}", var.as_str()),
label: Some(format!("Variable: {}", var.as_str())),
description: Some("A SPARQL variable".to_string()),
})
}
}
}
fn convert_named_node(term: Term) -> Result<RdfNamedNode> {
match term {
Term::NamedNode(node) => Ok(RdfNamedNode {
iri: node.to_string(),
label: None,
description: None,
}),
_ => Err(anyhow::anyhow!("Expected named node, got {:?}", term)),
}
}
fn build_select_query(filter: &RdfQueryFilter) -> String {
let mut conditions = Vec::new();
if let Some(ref subject) = filter.subject {
conditions.push(format!("CONTAINS(STR(?s), \"{subject}\")"));
}
if let Some(ref predicate) = filter.predicate {
conditions.push(format!("CONTAINS(STR(?p), \"{predicate}\")"));
}
if let Some(ref object) = filter.object {
conditions.push(format!("CONTAINS(STR(?o), \"{object}\")"));
}
let filter_clause = if !conditions.is_empty() {
format!("FILTER({})", conditions.join(" && "))
} else {
String::new()
};
let limit_clause = filter
.limit
.map(|l| format!(" LIMIT {l}"))
.unwrap_or_default();
let offset_clause = filter
.offset
.map(|o| format!(" OFFSET {o}"))
.unwrap_or_default();
format!("SELECT ?s ?p ?o WHERE {{ ?s ?p ?o {filter_clause} }}{limit_clause}{offset_clause}")
}