use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use tracing::{debug, info};
use crate::model::{StarTerm, StarTriple};
use crate::store::StarStore;
use crate::{StarError, StarResult};
#[derive(Clone)]
pub struct GraphQLStarEngine {
store: Arc<RwLock<StarStore>>,
schema: Arc<RwLock<GraphQLSchema>>,
config: GraphQLConfig,
stats: Arc<RwLock<GraphQLStats>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLConfig {
pub max_query_depth: usize,
pub max_results: usize,
pub enable_introspection: bool,
pub enable_subscriptions: bool,
pub timeout_ms: u64,
}
impl Default for GraphQLConfig {
fn default() -> Self {
Self {
max_query_depth: 10,
max_results: 1000,
enable_introspection: true,
enable_subscriptions: false,
timeout_ms: 30000,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GraphQLStats {
pub total_queries: usize,
pub total_mutations: usize,
pub total_subscriptions: usize,
pub avg_query_time_us: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLSchema {
pub types: HashMap<String, GraphQLType>,
pub query_type: String,
pub mutation_type: Option<String>,
pub subscription_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLType {
pub name: String,
pub kind: TypeKind,
pub fields: Vec<GraphQLField>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TypeKind {
Object,
Interface,
Enum,
Scalar,
List,
NonNull,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLField {
pub name: String,
pub field_type: String,
pub args: Vec<GraphQLArgument>,
pub description: Option<String>,
pub resolver: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLArgument {
pub name: String,
pub arg_type: String,
pub default_value: Option<JsonValue>,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLResult {
pub data: Option<JsonValue>,
pub errors: Vec<GraphQLError>,
pub extensions: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphQLError {
pub message: String,
pub locations: Option<Vec<ErrorLocation>>,
pub path: Option<Vec<JsonValue>>,
pub extensions: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorLocation {
pub line: usize,
pub column: usize,
}
impl GraphQLStarEngine {
pub fn new(store: StarStore) -> Self {
Self::with_config(store, GraphQLConfig::default())
}
pub fn with_config(store: StarStore, config: GraphQLConfig) -> Self {
let schema = Self::generate_default_schema();
Self {
store: Arc::new(RwLock::new(store)),
schema: Arc::new(RwLock::new(schema)),
config,
stats: Arc::new(RwLock::new(GraphQLStats::default())),
}
}
fn generate_default_schema() -> GraphQLSchema {
let mut types = HashMap::new();
types.insert(
"QuotedTriple".to_string(),
GraphQLType {
name: "QuotedTriple".to_string(),
kind: TypeKind::Object,
description: Some("A quoted RDF triple".to_string()),
fields: vec![
GraphQLField {
name: "subject".to_string(),
field_type: "Term".to_string(),
args: vec![],
description: Some("The subject of the triple".to_string()),
resolver: "resolve_subject".to_string(),
},
GraphQLField {
name: "predicate".to_string(),
field_type: "Term".to_string(),
args: vec![],
description: Some("The predicate of the triple".to_string()),
resolver: "resolve_predicate".to_string(),
},
GraphQLField {
name: "object".to_string(),
field_type: "Term".to_string(),
args: vec![],
description: Some("The object of the triple".to_string()),
resolver: "resolve_object".to_string(),
},
GraphQLField {
name: "nestingDepth".to_string(),
field_type: "Int".to_string(),
args: vec![],
description: Some("The nesting depth of this triple".to_string()),
resolver: "resolve_nesting_depth".to_string(),
},
],
},
);
types.insert(
"Term".to_string(),
GraphQLType {
name: "Term".to_string(),
kind: TypeKind::Interface,
description: Some(
"An RDF term (IRI, literal, blank node, or quoted triple)".to_string(),
),
fields: vec![
GraphQLField {
name: "value".to_string(),
field_type: "String".to_string(),
args: vec![],
description: Some("String representation of the term".to_string()),
resolver: "resolve_term_value".to_string(),
},
GraphQLField {
name: "termType".to_string(),
field_type: "TermType".to_string(),
args: vec![],
description: Some("Type of the term".to_string()),
resolver: "resolve_term_type".to_string(),
},
],
},
);
types.insert(
"Query".to_string(),
GraphQLType {
name: "Query".to_string(),
kind: TypeKind::Object,
description: Some("Root query type".to_string()),
fields: vec![
GraphQLField {
name: "quotedTriples".to_string(),
field_type: "[QuotedTriple]".to_string(),
args: vec![
GraphQLArgument {
name: "limit".to_string(),
arg_type: "Int".to_string(),
default_value: Some(json!(100)),
description: Some("Maximum number of results".to_string()),
},
GraphQLArgument {
name: "offset".to_string(),
arg_type: "Int".to_string(),
default_value: Some(json!(0)),
description: Some("Result offset for pagination".to_string()),
},
GraphQLArgument {
name: "maxDepth".to_string(),
arg_type: "Int".to_string(),
default_value: None,
description: Some("Filter by maximum nesting depth".to_string()),
},
],
description: Some("Query all quoted triples".to_string()),
resolver: "resolve_quoted_triples".to_string(),
},
GraphQLField {
name: "tripleCount".to_string(),
field_type: "Int".to_string(),
args: vec![],
description: Some("Total number of quoted triples".to_string()),
resolver: "resolve_triple_count".to_string(),
},
],
},
);
GraphQLSchema {
types,
query_type: "Query".to_string(),
mutation_type: None,
subscription_type: None,
}
}
pub fn execute(&self, query: &str) -> StarResult<GraphQLResult> {
info!("Executing GraphQL query");
debug!("Query: {}", query);
let start = std::time::Instant::now();
let parsed = self.parse_query(query)?;
let result = self.execute_parsed_query(&parsed)?;
let elapsed = start.elapsed().as_micros() as u64;
let mut stats = self.stats.write().unwrap_or_else(|e| e.into_inner());
stats.total_queries += 1;
let new_avg = if stats.total_queries == 1 {
elapsed
} else {
(stats.avg_query_time_us * (stats.total_queries as u64 - 1) + elapsed)
/ stats.total_queries as u64
};
stats.avg_query_time_us = new_avg;
info!("Query executed in {}μs", elapsed);
Ok(result)
}
fn parse_query(&self, query: &str) -> StarResult<ParsedQuery> {
let trimmed = query.trim();
if trimmed.contains("quotedTriples") {
let limit = self
.extract_arg_value(trimmed, "limit")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(100);
let offset = self
.extract_arg_value(trimmed, "offset")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
let max_depth = self
.extract_arg_value(trimmed, "maxDepth")
.and_then(|s| s.parse::<usize>().ok());
return Ok(ParsedQuery {
operation: Operation::Query,
selection: Selection::QuotedTriples {
limit,
offset,
max_depth,
},
});
}
if trimmed.contains("tripleCount") {
return Ok(ParsedQuery {
operation: Operation::Query,
selection: Selection::TripleCount,
});
}
Err(StarError::query_error("Unsupported GraphQL query"))
}
fn extract_arg_value(&self, query: &str, arg_name: &str) -> Option<String> {
let pattern = format!("{}:", arg_name);
if let Some(start) = query.find(&pattern) {
let after = &query[start + pattern.len()..];
let value_end = after
.find(|c: char| c == ',' || c == ')' || c.is_whitespace())
.unwrap_or(after.len());
let value = after[..value_end].trim();
return Some(value.to_string());
}
None
}
fn execute_parsed_query(&self, query: &ParsedQuery) -> StarResult<GraphQLResult> {
match query.operation {
Operation::Query => match &query.selection {
Selection::QuotedTriples {
limit,
offset,
max_depth,
} => self.resolve_quoted_triples(*limit, *offset, *max_depth),
Selection::TripleCount => self.resolve_triple_count(),
},
Operation::Mutation => Err(StarError::query_error("Mutations not implemented yet")),
Operation::Subscription => {
Err(StarError::query_error("Subscriptions not implemented yet"))
}
}
}
fn resolve_quoted_triples(
&self,
limit: usize,
offset: usize,
max_depth: Option<usize>,
) -> StarResult<GraphQLResult> {
let store = self.store.read().unwrap_or_else(|e| e.into_inner());
let mut triples = store.query(None, None, None)?;
let original_count = triples.len();
if let Some(max_d) = max_depth {
triples.retain(|t| t.nesting_depth() <= max_d);
}
let paginated: Vec<_> = triples
.into_iter()
.skip(offset)
.take(limit.min(self.config.max_results))
.collect();
let data = json!({
"quotedTriples": paginated.iter().map(|t| self.triple_to_json(t)).collect::<Vec<_>>()
});
Ok(GraphQLResult {
data: Some(data),
errors: vec![],
extensions: Some(json!({
"count": paginated.len(),
"hasMore": offset + paginated.len() < original_count
})),
})
}
fn resolve_triple_count(&self) -> StarResult<GraphQLResult> {
let store = self.store.read().unwrap_or_else(|e| e.into_inner());
let count = store.len();
Ok(GraphQLResult {
data: Some(json!({
"tripleCount": count
})),
errors: vec![],
extensions: None,
})
}
fn triple_to_json(&self, triple: &StarTriple) -> JsonValue {
json!({
"subject": self.term_to_json(&triple.subject),
"predicate": self.term_to_json(&triple.predicate),
"object": self.term_to_json(&triple.object),
"nestingDepth": triple.nesting_depth()
})
}
fn term_to_json(&self, term: &StarTerm) -> JsonValue {
match term {
StarTerm::NamedNode(nn) => json!({
"value": nn.iri,
"termType": "NamedNode"
}),
StarTerm::BlankNode(bn) => json!({
"value": bn.id,
"termType": "BlankNode"
}),
StarTerm::Literal(lit) => json!({
"value": lit.value,
"termType": "Literal",
"language": lit.language,
"datatype": lit.datatype.as_ref().map(|dt| &dt.iri)
}),
StarTerm::QuotedTriple(qt) => json!({
"value": format!("<< {} {} {} >>", qt.subject, qt.predicate, qt.object),
"termType": "QuotedTriple",
"triple": self.triple_to_json(qt)
}),
StarTerm::Variable(var) => json!({
"value": var.name,
"termType": "Variable"
}),
}
}
pub fn get_schema(&self) -> GraphQLSchema {
self.schema
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn get_statistics(&self) -> GraphQLStats {
self.stats.read().unwrap_or_else(|e| e.into_inner()).clone()
}
}
#[derive(Debug, Clone)]
struct ParsedQuery {
operation: Operation,
selection: Selection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum Operation {
Query,
Mutation,
Subscription,
}
#[derive(Debug, Clone)]
enum Selection {
QuotedTriples {
limit: usize,
offset: usize,
max_depth: Option<usize>,
},
TripleCount,
}
pub struct SchemaGenerator {
config: SchemaGeneratorConfig,
}
#[derive(Debug, Clone)]
pub struct SchemaGeneratorConfig {
pub include_introspection: bool,
pub include_mutations: bool,
pub include_subscriptions: bool,
}
impl Default for SchemaGeneratorConfig {
fn default() -> Self {
Self {
include_introspection: true,
include_mutations: false,
include_subscriptions: false,
}
}
}
impl SchemaGenerator {
pub fn new(config: SchemaGeneratorConfig) -> Self {
Self { config }
}
pub fn generate(&self, store: &StarStore) -> StarResult<GraphQLSchema> {
let mut schema = GraphQLStarEngine::generate_default_schema();
let stats = store.statistics();
debug!(
"Generating schema for store with {} quoted triples",
stats.quoted_triples_count
);
if self.config.include_mutations {
let mutation_type = self.generate_mutation_type();
schema.types.insert("Mutation".to_string(), mutation_type);
schema.mutation_type = Some("Mutation".to_string());
}
if self.config.include_subscriptions {
let subscription_type = self.generate_subscription_type();
schema
.types
.insert("Subscription".to_string(), subscription_type);
schema.subscription_type = Some("Subscription".to_string());
}
Ok(schema)
}
fn generate_mutation_type(&self) -> GraphQLType {
GraphQLType {
name: "Mutation".to_string(),
kind: TypeKind::Object,
description: Some("Root mutation type".to_string()),
fields: vec![GraphQLField {
name: "insertQuotedTriple".to_string(),
field_type: "QuotedTriple".to_string(),
args: vec![
GraphQLArgument {
name: "subject".to_string(),
arg_type: "String!".to_string(),
default_value: None,
description: Some("Subject IRI".to_string()),
},
GraphQLArgument {
name: "predicate".to_string(),
arg_type: "String!".to_string(),
default_value: None,
description: Some("Predicate IRI".to_string()),
},
GraphQLArgument {
name: "object".to_string(),
arg_type: "String!".to_string(),
default_value: None,
description: Some("Object value".to_string()),
},
],
description: Some("Insert a new quoted triple".to_string()),
resolver: "mutate_insert_quoted_triple".to_string(),
}],
}
}
fn generate_subscription_type(&self) -> GraphQLType {
GraphQLType {
name: "Subscription".to_string(),
kind: TypeKind::Object,
description: Some("Root subscription type".to_string()),
fields: vec![GraphQLField {
name: "quotedTripleAdded".to_string(),
field_type: "QuotedTriple".to_string(),
args: vec![],
description: Some("Subscribe to new quoted triple additions".to_string()),
resolver: "subscribe_quoted_triple_added".to_string(),
}],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{StarTerm, StarTriple};
#[test]
fn test_graphql_quoted_triples_query() -> StarResult<()> {
let store = StarStore::new();
for i in 0..5 {
let subject = format!("http://example.org/s{}", i);
let object = format!("object{}", i);
let triple = StarTriple::new(
StarTerm::iri(&subject)?,
StarTerm::iri("http://example.org/p")?,
StarTerm::literal(&object)?,
);
store.insert(&triple)?;
}
let engine = GraphQLStarEngine::new(store);
let query = r#"
{
quotedTriples(limit: 3, offset: 0) {
subject { value }
predicate { value }
object { value }
}
}
"#;
let result = engine.execute(query)?;
assert!(result.data.is_some());
assert!(result.errors.is_empty());
Ok(())
}
#[test]
fn test_graphql_triple_count() -> StarResult<()> {
let store = StarStore::new();
for i in 0..10 {
let subject = format!("http://example.org/s{}", i);
let object = format!("o{}", i);
let triple = StarTriple::new(
StarTerm::iri(&subject)?,
StarTerm::iri("http://example.org/p")?,
StarTerm::literal(&object)?,
);
store.insert(&triple)?;
}
let engine = GraphQLStarEngine::new(store);
let query = "{ tripleCount }";
let result = engine.execute(query)?;
assert!(result.data.is_some());
let data = result.data.unwrap();
assert_eq!(data["tripleCount"], 10);
Ok(())
}
#[test]
fn test_schema_generation() -> StarResult<()> {
let store = StarStore::new();
let generator = SchemaGenerator::new(SchemaGeneratorConfig::default());
let schema = generator.generate(&store)?;
assert!(schema.types.contains_key("QuotedTriple"));
assert!(schema.types.contains_key("Term"));
assert!(schema.types.contains_key("Query"));
assert_eq!(schema.query_type, "Query");
Ok(())
}
}