use crate::oxirs_executor::OxirsSparqlExecutor;
use anyhow::Result;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tensorlogic_adapters::{DomainInfo, PredicateInfo, SymbolTable};
use tensorlogic_ir::{TLExpr, Term};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLType {
pub name: String,
pub is_scalar: bool,
pub is_list: bool,
pub is_non_null: bool,
pub inner_type: Option<Box<GraphQLType>>,
}
impl GraphQLType {
pub fn scalar(name: &str) -> Self {
Self {
name: name.to_string(),
is_scalar: true,
is_list: false,
is_non_null: false,
inner_type: None,
}
}
pub fn object(name: &str) -> Self {
Self {
name: name.to_string(),
is_scalar: false,
is_list: false,
is_non_null: false,
inner_type: None,
}
}
pub fn list(inner: GraphQLType) -> Self {
Self {
name: format!("[{}]", inner.name),
is_scalar: false,
is_list: true,
is_non_null: false,
inner_type: Some(Box::new(inner)),
}
}
pub fn non_null(mut self) -> Self {
self.is_non_null = true;
self
}
pub fn to_sdl(&self) -> String {
let base = if self.is_list {
format!("[{}]", self.inner_type.as_ref().map_or("ID", |t| &t.name))
} else {
self.name.clone()
};
if self.is_non_null {
format!("{}!", base)
} else {
base
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLField {
pub name: String,
pub field_type: GraphQLType,
pub description: Option<String>,
pub arguments: Vec<GraphQLArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLArgument {
pub name: String,
pub arg_type: GraphQLType,
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLObjectType {
pub name: String,
pub description: Option<String>,
pub fields: IndexMap<String, GraphQLField>,
pub interfaces: Vec<String>,
}
impl GraphQLObjectType {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
description: None,
fields: IndexMap::new(),
interfaces: Vec::new(),
}
}
pub fn add_field(&mut self, field: GraphQLField) {
self.fields.insert(field.name.clone(), field);
}
pub fn to_sdl(&self) -> String {
let mut sdl = format!("type {} {{\n", self.name);
for (_, field) in &self.fields {
if let Some(desc) = &field.description {
sdl.push_str(&format!(" \"\"\"{}\"\"\"\n", desc));
}
sdl.push_str(&format!(
" {}: {}\n",
field.name,
field.field_type.to_sdl()
));
}
sdl.push_str("}\n");
sdl
}
}
#[derive(Debug, Clone, Default)]
pub struct GraphQLSchema {
pub types: IndexMap<String, GraphQLObjectType>,
pub query_type: Option<String>,
pub mutation_type: Option<String>,
pub sdl: String,
}
impl GraphQLSchema {
pub fn new() -> Self {
Self::default()
}
pub fn parse(sdl: &str) -> Result<Self> {
Ok(Self {
types: IndexMap::new(),
query_type: Some("Query".to_string()),
mutation_type: None,
sdl: sdl.to_string(),
})
}
pub fn add_type(&mut self, object_type: GraphQLObjectType) {
self.types.insert(object_type.name.clone(), object_type);
}
pub fn to_sdl(&self) -> String {
if !self.sdl.is_empty() {
return self.sdl.clone();
}
let mut sdl = String::new();
for (_, object_type) in &self.types {
sdl.push_str(&object_type.to_sdl());
sdl.push('\n');
}
sdl
}
}
pub struct OxirsGraphQLBridge {
executor: OxirsSparqlExecutor,
schema: Option<GraphQLSchema>,
types: IndexMap<String, GraphQLObjectType>,
iri_to_type: HashMap<String, String>,
prefixes: HashMap<String, String>,
}
impl OxirsGraphQLBridge {
pub fn new() -> Result<Self> {
Ok(Self {
executor: OxirsSparqlExecutor::new()?,
schema: None,
types: IndexMap::new(),
iri_to_type: HashMap::new(),
prefixes: HashMap::new(),
})
}
pub fn load_turtle(&mut self, turtle: &str) -> Result<usize> {
self.executor.load_turtle(turtle)
}
pub fn add_prefix(&mut self, prefix: &str, iri: &str) {
self.prefixes.insert(prefix.to_string(), iri.to_string());
}
pub fn generate_schema(&mut self) -> Result<()> {
let classes_query = r#"
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT DISTINCT ?class WHERE {
?class a rdfs:Class .
}
"#;
let class_results = self.executor.execute(classes_query)?;
if let crate::oxirs_executor::QueryResults::Select { bindings, .. } = class_results {
for binding in bindings {
if let Some(class_value) = binding.get("class") {
let class_iri = class_value.as_str();
let type_name = Self::iri_to_type_name(class_iri);
self.iri_to_type
.insert(class_iri.to_string(), type_name.clone());
let object_type = GraphQLObjectType::new(&type_name);
self.types.insert(type_name, object_type);
}
}
}
for (iri, type_name) in &self.iri_to_type.clone() {
let props_query = format!(
r#"
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT DISTINCT ?prop WHERE {{
?prop rdfs:domain <{}> .
}}
"#,
iri
);
let prop_results = self.executor.execute(&props_query)?;
if let crate::oxirs_executor::QueryResults::Select { bindings, .. } = prop_results {
for binding in bindings {
if let Some(prop_value) = binding.get("prop") {
let prop_iri = prop_value.as_str();
let field_name = Self::iri_to_field_name(prop_iri);
let field = GraphQLField {
name: field_name,
field_type: GraphQLType::scalar("String"),
description: None,
arguments: Vec::new(),
};
if let Some(object_type) = self.types.get_mut(type_name) {
object_type.add_field(field);
}
}
}
}
}
let mut query_type = GraphQLObjectType::new("Query");
for (type_name, _) in &self.types {
if type_name == "Query" {
continue;
}
let field = GraphQLField {
name: Self::type_to_query_name(type_name),
field_type: GraphQLType::object(type_name),
description: Some(format!("Get a single {} by ID", type_name)),
arguments: vec![GraphQLArgument {
name: "id".to_string(),
arg_type: GraphQLType::scalar("ID").non_null(),
default_value: None,
}],
};
query_type.add_field(field);
let list_field = GraphQLField {
name: format!("all{}s", type_name),
field_type: GraphQLType::list(GraphQLType::object(type_name)),
description: Some(format!("List all {}s", type_name)),
arguments: vec![
GraphQLArgument {
name: "limit".to_string(),
arg_type: GraphQLType::scalar("Int"),
default_value: Some("10".to_string()),
},
GraphQLArgument {
name: "offset".to_string(),
arg_type: GraphQLType::scalar("Int"),
default_value: Some("0".to_string()),
},
],
};
query_type.add_field(list_field);
}
self.types.insert("Query".to_string(), query_type);
let mut schema = GraphQLSchema::new();
for (_, obj_type) in &self.types {
schema.add_type(obj_type.clone());
}
schema.query_type = Some("Query".to_string());
self.schema = Some(schema);
Ok(())
}
pub fn execute_query(&self, query: &str) -> Result<serde_json::Value> {
let query_trimmed = query.trim();
if query_trimmed.starts_with("query") || query_trimmed.starts_with('{') {
self.execute_graphql_query(query_trimmed)
} else {
Err(anyhow::anyhow!("Unsupported GraphQL operation"))
}
}
fn execute_graphql_query(&self, query: &str) -> Result<serde_json::Value> {
let mut result = serde_json::Map::new();
let data = serde_json::Map::new();
result.insert("data".to_string(), serde_json::Value::Object(data));
if query.contains("__schema") || query.contains("__type") {
if let Some(schema) = &self.schema {
let mut schema_info = serde_json::Map::new();
let types: Vec<serde_json::Value> = schema
.types
.keys()
.map(|k| serde_json::json!({"name": k}))
.collect();
schema_info.insert("types".to_string(), serde_json::Value::Array(types));
result.insert(
"data".to_string(),
serde_json::json!({"__schema": schema_info}),
);
}
}
Ok(serde_json::Value::Object(result))
}
pub fn schema_to_symbol_table(&self) -> Result<SymbolTable> {
let mut symbol_table = SymbolTable::new();
let entity_domain = DomainInfo::new("Entity", usize::MAX);
let _domain_added = symbol_table.add_domain(entity_domain);
for (name, object_type) in &self.types {
let pred_info = PredicateInfo::new(name.clone(), vec!["Entity".to_string()]);
let _result = symbol_table.add_predicate(pred_info);
for (field_name, _field) in &object_type.fields {
let pred_name = format!("{}_{}", name, field_name);
let field_pred_info =
PredicateInfo::new(pred_name, vec!["Entity".to_string(), "Entity".to_string()]);
let _result = symbol_table.add_predicate(field_pred_info);
}
}
Ok(symbol_table)
}
pub fn get_schema_sdl(&self) -> Option<String> {
self.schema.as_ref().map(|s: &GraphQLSchema| s.to_sdl())
}
fn iri_to_type_name(iri: &str) -> String {
let local = iri.split(['/', '#']).next_back().unwrap_or(iri);
let mut chars = local.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn iri_to_field_name(iri: &str) -> String {
let local = iri.split(['/', '#']).next_back().unwrap_or(iri);
local.to_lowercase()
}
fn type_to_query_name(type_name: &str) -> String {
let mut chars = type_name.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
}
}
pub fn result_to_tlexpr(&self, result: &serde_json::Value) -> Result<TLExpr> {
match result {
serde_json::Value::Object(obj) => {
if let Some(data) = obj.get("data") {
self.json_to_tlexpr(data)
} else {
Ok(TLExpr::pred("empty", vec![]))
}
}
_ => Ok(TLExpr::pred("empty", vec![])),
}
}
fn json_to_tlexpr(&self, value: &serde_json::Value) -> Result<TLExpr> {
match value {
serde_json::Value::Null => Ok(TLExpr::pred("null", vec![])),
serde_json::Value::Bool(b) => {
if *b {
Ok(TLExpr::pred("true", vec![]))
} else {
Ok(TLExpr::pred("false", vec![]))
}
}
serde_json::Value::Number(n) => {
Ok(TLExpr::pred("number", vec![Term::constant(n.to_string())]))
}
serde_json::Value::String(s) => Ok(TLExpr::pred("string", vec![Term::constant(s)])),
serde_json::Value::Array(arr) => {
let mut exprs = Vec::new();
for item in arr {
exprs.push(self.json_to_tlexpr(item)?);
}
exprs
.into_iter()
.reduce(TLExpr::and)
.ok_or_else(|| anyhow::anyhow!("Empty array"))
}
serde_json::Value::Object(obj) => {
let mut exprs = Vec::new();
for (key, val) in obj {
let val_expr = self.json_to_tlexpr(val)?;
let pred = TLExpr::pred(key, vec![]);
exprs.push(TLExpr::and(pred, val_expr));
}
exprs
.into_iter()
.reduce(TLExpr::and)
.ok_or_else(|| anyhow::anyhow!("Empty object"))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bridge_creation() {
let bridge = OxirsGraphQLBridge::new();
assert!(bridge.is_ok());
}
#[test]
fn test_graphql_type_scalar() {
let t = GraphQLType::scalar("String");
assert!(t.is_scalar);
assert_eq!(t.to_sdl(), "String");
}
#[test]
fn test_graphql_type_list() {
let inner = GraphQLType::object("Person");
let t = GraphQLType::list(inner);
assert!(t.is_list);
assert_eq!(t.to_sdl(), "[Person]");
}
#[test]
fn test_graphql_type_non_null() {
let t = GraphQLType::scalar("ID").non_null();
assert!(t.is_non_null);
assert_eq!(t.to_sdl(), "ID!");
}
#[test]
fn test_object_type_creation() {
let mut object_type = GraphQLObjectType::new("Person");
object_type.add_field(GraphQLField {
name: "name".to_string(),
field_type: GraphQLType::scalar("String"),
description: None,
arguments: Vec::new(),
});
assert_eq!(object_type.name, "Person");
assert!(object_type.fields.contains_key("name"));
}
#[test]
fn test_iri_to_type_name() {
assert_eq!(
OxirsGraphQLBridge::iri_to_type_name("http://example.org/Person"),
"Person"
);
assert_eq!(
OxirsGraphQLBridge::iri_to_type_name("http://example.org/schema#person"),
"Person"
);
}
#[test]
fn test_type_to_query_name() {
assert_eq!(OxirsGraphQLBridge::type_to_query_name("Person"), "person");
assert_eq!(
OxirsGraphQLBridge::type_to_query_name("BookAuthor"),
"bookAuthor"
);
}
#[test]
fn test_execute_introspection_query() {
let mut bridge = OxirsGraphQLBridge::new().expect("Failed to create bridge");
bridge
.types
.insert("Person".to_string(), GraphQLObjectType::new("Person"));
bridge.schema = Some(GraphQLSchema {
types: bridge.types.clone(),
query_type: Some("Query".to_string()),
mutation_type: None,
sdl: String::new(),
});
let result = bridge.execute_query("{ __schema { types { name } } }");
assert!(result.is_ok());
}
#[test]
fn test_schema_to_symbol_table() {
let mut bridge = OxirsGraphQLBridge::new().expect("Failed to create bridge");
let mut person_type = GraphQLObjectType::new("Person");
person_type.add_field(GraphQLField {
name: "name".to_string(),
field_type: GraphQLType::scalar("String"),
description: None,
arguments: Vec::new(),
});
bridge.types.insert("Person".to_string(), person_type);
let result = bridge.schema_to_symbol_table();
assert!(result.is_ok());
}
}