use crate::error::{Result, Error};
use crate::model::{Dataset, Graph, Triple, Quad, NamedNode, Literal, Term};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::RwLock;
use serde::{Serialize, Deserialize};
pub struct GraphQLSchemaGenerator {
dataset: Arc<RwLock<Dataset>>,
schema: Option<GraphQLSchema>,
config: GraphQLConfig,
}
impl GraphQLSchemaGenerator {
pub fn new(dataset: Arc<RwLock<Dataset>>) -> Result<Self> {
Ok(Self {
dataset,
schema: None,
config: GraphQLConfig::default(),
})
}
pub async fn initialize(&self) -> Result<()> {
Ok(())
}
pub async fn generate_schema(&mut self) -> Result<GraphQLSchema> {
let dataset = self.dataset.read().await;
let mut types = HashMap::new();
let mut predicates = HashMap::new();
let mut type_definitions = Vec::new();
for quad in dataset.iter() {
self.analyze_quad(&quad, &mut types, &mut predicates)?;
}
for (type_uri, properties) in &types {
let graphql_type = self.generate_graphql_type(type_uri, properties)?;
type_definitions.push(graphql_type);
}
let query_type = self.generate_query_type(&types)?;
type_definitions.push(query_type);
let schema = GraphQLSchema {
types: type_definitions,
query_type: "Query".to_string(),
mutation_type: Some("Mutation".to_string()),
subscription_type: Some("Subscription".to_string()),
directives: self.generate_custom_directives(),
};
self.schema = Some(schema.clone());
Ok(schema)
}
fn analyze_quad(
&self,
quad: &Quad,
types: &mut HashMap<String, HashSet<String>>,
predicates: &mut HashMap<String, PredicateInfo>,
) -> Result<()> {
if let Term::NamedNode(predicate) = &quad.predicate {
let pred_str = predicate.as_str();
if pred_str == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" {
if let (Term::NamedNode(subject), Term::NamedNode(object)) = (&quad.subject, &quad.object) {
let type_uri = object.as_str().to_string();
types.entry(type_uri).or_insert_with(HashSet::new);
}
} else {
if let Term::NamedNode(subject) = &quad.subject {
self.add_property_to_types(types, pred_str, &quad.object)?;
}
predicates.insert(pred_str.to_string(), PredicateInfo {
uri: pred_str.to_string(),
object_type: self.determine_object_type(&quad.object)?,
cardinality: Cardinality::Many, });
}
}
Ok(())
}
fn add_property_to_types(
&self,
types: &mut HashMap<String, HashSet<String>>,
predicate: &str,
_object: &Term,
) -> Result<()> {
for properties in types.values_mut() {
properties.insert(predicate.to_string());
}
Ok(())
}
fn determine_object_type(&self, object: &Term) -> Result<GraphQLObjectType> {
match object {
Term::NamedNode(_) => Ok(GraphQLObjectType::NamedNode),
Term::BlankNode(_) => Ok(GraphQLObjectType::BlankNode),
Term::Literal(literal) => {
match literal.datatype() {
Some(datatype) => match datatype.as_str() {
"http://www.w3.org/2001/XMLSchema#string" => Ok(GraphQLObjectType::String),
"http://www.w3.org/2001/XMLSchema#integer" => Ok(GraphQLObjectType::Int),
"http://www.w3.org/2001/XMLSchema#decimal" |
"http://www.w3.org/2001/XMLSchema#double" => Ok(GraphQLObjectType::Float),
"http://www.w3.org/2001/XMLSchema#boolean" => Ok(GraphQLObjectType::Boolean),
"http://www.w3.org/2001/XMLSchema#dateTime" => Ok(GraphQLObjectType::DateTime),
_ => Ok(GraphQLObjectType::String), },
None => Ok(GraphQLObjectType::String), }
}
}
}
fn generate_graphql_type(
&self,
type_uri: &str,
properties: &HashSet<String>,
) -> Result<GraphQLTypeDefinition> {
let type_name = self.uri_to_graphql_name(type_uri);
let mut fields = Vec::new();
fields.push(GraphQLField {
name: "id".to_string(),
field_type: GraphQLFieldType::NonNull(Box::new(GraphQLFieldType::Scalar("ID".to_string()))),
description: Some("Unique identifier for this resource".to_string()),
arguments: Vec::new(),
});
for property in properties {
let field_name = self.uri_to_graphql_name(property);
let field_type = self.determine_graphql_field_type(property)?;
fields.push(GraphQLField {
name: field_name,
field_type,
description: Some(format!("Property mapped from {property}")),
arguments: Vec::new(),
});
}
Ok(GraphQLTypeDefinition {
name: type_name,
fields,
description: Some(format!("Type generated from RDF class {type_uri}")),
interfaces: Vec::new(),
})
}
fn generate_query_type(&self, types: &HashMap<String, HashSet<String>>) -> Result<GraphQLTypeDefinition> {
let mut fields = Vec::new();
for type_uri in types.keys() {
let type_name = self.uri_to_graphql_name(type_uri);
fields.push(GraphQLField {
name: format!("get{type_name}"),
field_type: GraphQLFieldType::Object(type_name.clone()),
description: Some(format!("Find a single {} by ID", type_name)),
arguments: vec![
GraphQLArgument {
name: "id".to_string(),
arg_type: GraphQLFieldType::NonNull(Box::new(GraphQLFieldType::Scalar("ID".to_string()))),
description: Some("The ID of the resource to retrieve".to_string()),
default_value: None,
}
],
});
fields.push(GraphQLField {
name: format!("list{type_name}"),
field_type: GraphQLFieldType::List(Box::new(GraphQLFieldType::Object(type_name.clone()))),
description: Some(format!("List all instances of {type_name}")),
arguments: vec![
GraphQLArgument {
name: "limit".to_string(),
arg_type: GraphQLFieldType::Scalar("Int".to_string()),
description: Some("Maximum number of results to return".to_string()),
default_value: Some("10".to_string()),
},
GraphQLArgument {
name: "offset".to_string(),
arg_type: GraphQLFieldType::Scalar("Int".to_string()),
description: Some("Number of results to skip".to_string()),
default_value: Some("0".to_string()),
},
],
});
}
Ok(GraphQLTypeDefinition {
name: "Query".to_string(),
fields,
description: Some("Root query type for accessing RDF data".to_string()),
interfaces: Vec::new(),
})
}
fn generate_custom_directives(&self) -> Vec<GraphQLDirective> {
vec![
GraphQLDirective {
name: "rdfProperty".to_string(),
description: Some("Maps a GraphQL field to an RDF property".to_string()),
locations: vec!["FIELD_DEFINITION".to_string()],
arguments: vec![
GraphQLArgument {
name: "uri".to_string(),
arg_type: GraphQLFieldType::NonNull(Box::new(GraphQLFieldType::Scalar("String".to_string()))),
description: Some("The RDF property URI".to_string()),
default_value: None,
}
],
},
GraphQLDirective {
name: "rdfType".to_string(),
description: Some("Maps a GraphQL type to an RDF class".to_string()),
locations: vec!["OBJECT".to_string()],
arguments: vec![
GraphQLArgument {
name: "uri".to_string(),
arg_type: GraphQLFieldType::NonNull(Box::new(GraphQLFieldType::Scalar("String".to_string()))),
description: Some("The RDF class URI".to_string()),
default_value: None,
}
],
},
]
}
fn uri_to_graphql_name(&self, uri: &str) -> String {
let local_name = uri.split(['/', '#']).last().unwrap_or(uri);
local_name
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>()
.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
fn determine_graphql_field_type(&self, _property: &str) -> Result<GraphQLFieldType> {
Ok(GraphQLFieldType::Scalar("String".to_string()))
}
pub async fn health_check(&self) -> crate::api::ServiceStatus {
match &self.schema {
Some(_) => crate::api::ServiceStatus::Healthy,
None => crate::api::ServiceStatus::Degraded("Schema not generated yet".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLConfig {
pub enable_introspection: bool,
pub enable_playground: bool,
pub max_query_depth: usize,
pub max_query_complexity: usize,
pub auto_refresh: bool,
}
impl Default for GraphQLConfig {
fn default() -> Self {
Self {
enable_introspection: true,
enable_playground: true,
max_query_depth: 10,
max_query_complexity: 1000,
auto_refresh: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLSchema {
pub types: Vec<GraphQLTypeDefinition>,
pub query_type: String,
pub mutation_type: Option<String>,
pub subscription_type: Option<String>,
pub directives: Vec<GraphQLDirective>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLTypeDefinition {
pub name: String,
pub fields: Vec<GraphQLField>,
pub description: Option<String>,
pub interfaces: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLField {
pub name: String,
pub field_type: GraphQLFieldType,
pub description: Option<String>,
pub arguments: Vec<GraphQLArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GraphQLFieldType {
Scalar(String),
Object(String),
List(Box<GraphQLFieldType>),
NonNull(Box<GraphQLFieldType>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLArgument {
pub name: String,
pub arg_type: GraphQLFieldType,
pub description: Option<String>,
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLDirective {
pub name: String,
pub description: Option<String>,
pub locations: Vec<String>,
pub arguments: Vec<GraphQLArgument>,
}
#[derive(Debug, Clone)]
struct PredicateInfo {
uri: String,
object_type: GraphQLObjectType,
cardinality: Cardinality,
}
#[derive(Debug, Clone)]
enum GraphQLObjectType {
NamedNode,
BlankNode,
String,
Int,
Float,
Boolean,
DateTime,
}
#[derive(Debug, Clone)]
enum Cardinality {
One,
Many,
}