1use crate::graphiql_integration::{generate_graphiql_html, GraphiQLConfig};
7use crate::juniper_schema::{create_schema, GraphQLContext, Schema};
8use crate::RdfStore;
9use anyhow::Result;
10use chrono;
11use hyper::service::service_fn;
12use hyper::{body::Incoming, Method, Request, Response, StatusCode};
13use hyper_util::rt::{TokioExecutor, TokioIo};
14use hyper_util::server::conn::auto::Builder;
15use juniper_hyper::{graphql, playground};
16use serde_json;
17use std::convert::Infallible;
18use std::net::SocketAddr;
19use std::sync::Arc;
20use tokio::net::TcpListener;
21use tracing::{debug, error, info, warn};
22
23#[derive(Debug, Clone)]
25pub struct GraphQLServerConfig {
26 pub enable_graphiql: bool,
28 pub enable_playground: bool,
30 pub enable_introspection: bool,
32 pub max_query_depth: Option<usize>,
34 pub max_query_complexity: Option<usize>,
36 pub cors_enabled: bool,
38 pub cors_origins: Option<Vec<String>>,
40}
41
42impl Default for GraphQLServerConfig {
43 fn default() -> Self {
44 Self {
45 enable_graphiql: true,
46 enable_playground: true,
47 enable_introspection: true,
48 max_query_depth: Some(15),
49 max_query_complexity: Some(1000),
50 cors_enabled: true,
51 cors_origins: None, }
53 }
54}
55
56pub struct JuniperGraphQLServer {
58 schema: Arc<Schema>,
59 context: GraphQLContext,
60 config: GraphQLServerConfig,
61}
62
63impl JuniperGraphQLServer {
64 pub fn new(store: Arc<RdfStore>) -> Self {
66 let schema = Arc::new(create_schema());
67 let context = GraphQLContext { store };
68 let config = GraphQLServerConfig::default();
69
70 Self {
71 schema,
72 context,
73 config,
74 }
75 }
76
77 pub fn with_config(store: Arc<RdfStore>, config: GraphQLServerConfig) -> Self {
79 let schema = Arc::new(create_schema());
80 let context = GraphQLContext { store };
81
82 Self {
83 schema,
84 context,
85 config,
86 }
87 }
88
89 pub async fn start(&self, addr: SocketAddr) -> Result<()> {
91 info!("Starting Juniper GraphQL server on {}", addr);
92
93 let schema = self.schema.clone();
95 let context = self.context.clone();
96 let config = self.config.clone();
97
98 let listener = TcpListener::bind(addr).await?;
100
101 info!("GraphQL server running on http://{}", addr);
102 info!("GraphQL endpoint: http://{}/graphql", addr);
103
104 loop {
106 let (stream, _) = match listener.accept().await {
107 Ok(result) => result,
108 Err(e) => {
109 error!("Failed to accept connection: {}", e);
110 continue;
111 }
112 };
113
114 let schema_clone = schema.clone();
115 let context_clone = context.clone();
116 let config_clone = config.clone();
117
118 tokio::spawn(async move {
120 let io = TokioIo::new(stream);
121 let builder = Builder::new(TokioExecutor::new());
122
123 let service = service_fn(move |req| {
124 Self::handle_request(
125 req,
126 schema_clone.clone(),
127 context_clone.clone(),
128 config_clone.clone(),
129 )
130 });
131
132 if let Err(e) = builder.serve_connection(io, service).await {
133 error!("Connection error: {}", e);
134 }
135 });
136 }
137 }
138
139 async fn handle_request(
141 req: Request<Incoming>,
142 schema: Arc<Schema>,
143 context: GraphQLContext,
144 config: GraphQLServerConfig,
145 ) -> Result<Response<String>, Infallible> {
146 let response = match Self::handle_request_inner(req, schema, context, config).await {
147 Ok(response) => response,
148 Err(err) => {
149 error!("Request handling error: {}", err);
150 Response::builder()
151 .status(StatusCode::INTERNAL_SERVER_ERROR)
152 .header("content-type", "application/json")
153 .body(format!(
154 r#"{{"errors": [{{ "message": "{}" }}]}}"#,
155 err.to_string().replace('"', "\\\"")
156 ))
157 .expect("building error response should succeed")
158 }
159 };
160
161 Ok(response)
162 }
163
164 async fn handle_request_inner(
166 req: Request<Incoming>,
167 schema: Arc<Schema>,
168 context: GraphQLContext,
169 config: GraphQLServerConfig,
170 ) -> Result<Response<String>> {
171 let method = req.method();
172 let path = req.uri().path();
173
174 debug!("Handling {} request to {}", method, path);
175
176 let mut response_builder = Response::builder();
178 if config.cors_enabled {
179 response_builder = response_builder
180 .header("Access-Control-Allow-Origin", "*")
181 .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
182 .header(
183 "Access-Control-Allow-Headers",
184 "Content-Type, Authorization",
185 );
186 }
187
188 if method == Method::OPTIONS {
190 return Ok(response_builder
191 .status(StatusCode::OK)
192 .body(String::new())?);
193 }
194
195 match (method, path) {
196 (&Method::GET, "/graphql") | (&Method::POST, "/graphql") => {
198 debug!("Processing GraphQL request");
199 let mut response = graphql(schema, Arc::new(context), req).await;
200
201 if config.cors_enabled {
203 let headers = response.headers_mut();
204 headers.insert(
205 "Access-Control-Allow-Origin",
206 "*".parse().expect("parse should succeed for valid input"),
207 );
208 headers.insert(
209 "Access-Control-Allow-Methods",
210 "GET, POST, OPTIONS"
211 .parse()
212 .expect("parse should succeed for valid input"),
213 );
214 headers.insert(
215 "Access-Control-Allow-Headers",
216 "Content-Type, Authorization"
217 .parse()
218 .expect("parse should succeed for valid input"),
219 );
220 }
221 Ok(response)
222 }
223
224 (&Method::GET, "/graphiql") if config.enable_graphiql => {
226 debug!("Serving enhanced GraphiQL interface");
227 let graphiql_config = GraphiQLConfig {
228 endpoint: "/graphql".to_string(),
229 enable_history: true,
230 enable_templates: true,
231 enable_custom_headers: true,
232 enable_metrics: true,
233 default_dark_theme: false,
234 enable_sharing: true,
235 enable_export: true,
236 custom_css: None,
237 title: "OxiRS GraphQL Explorer".to_string(),
238 subscription_endpoint: None,
239 ..Default::default()
240 };
241
242 let html = generate_graphiql_html(&graphiql_config);
243
244 Ok(response_builder
245 .status(StatusCode::OK)
246 .header("content-type", "text/html; charset=utf-8")
247 .body(html)?)
248 }
249
250 (&Method::GET, "/playground") if config.enable_playground => {
252 debug!("Serving GraphQL Playground");
253 let response = playground("/graphql", None).await;
254 Ok(response)
255 }
256
257 (&Method::GET, "/health") => {
259 debug!("Health check request");
260 let health_info = serde_json::json!({
261 "status": "healthy",
262 "service": "oxirs-graphql",
263 "version": env!("CARGO_PKG_VERSION"),
264 "timestamp": chrono::Utc::now().to_rfc3339(),
265 "endpoints": {
266 "graphql": "/graphql",
267 "graphiql": if config.enable_graphiql { serde_json::Value::String("/graphiql".to_string()) } else { serde_json::Value::Null },
268 "playground": if config.enable_playground { serde_json::Value::String("/playground".to_string()) } else { serde_json::Value::Null }
269 }
270 });
271
272 Ok(response_builder
273 .status(StatusCode::OK)
274 .header("content-type", "application/json")
275 .body(health_info.to_string())?)
276 }
277
278 (&Method::GET, "/schema") if config.enable_introspection => {
280 debug!("Schema introspection request");
281
282 let sdl = r#"
284"""
285An RDF IRI (Internationalized Resource Identifier)
286"""
287scalar IRI
288
289"""
290An RDF Literal with optional language tag and datatype
291"""
292scalar RdfLiteral
293
294"""
295An RDF Named Node (IRI)
296"""
297type RdfNamedNode {
298 """The IRI of this named node"""
299 iri: IRI!
300 """A human-readable label for this resource (if available)"""
301 label: String
302 """A description of this resource (if available)"""
303 description: String
304}
305
306"""
307An RDF Literal value
308"""
309type RdfLiteralNode {
310 """The literal value"""
311 literal: RdfLiteral!
312 """The string representation of the value"""
313 value: String!
314 """The language tag if this is a language-tagged string"""
315 language: String
316 """The datatype IRI if this is a typed literal"""
317 datatype: IRI
318}
319
320"""
321An RDF Blank Node
322"""
323type RdfBlankNode {
324 """The identifier of the blank node"""
325 id: ID!
326 """Human-readable representation"""
327 label: String!
328}
329
330"""
331An RDF term which can be an IRI, Literal, or Blank Node
332"""
333union RdfTerm = RdfNamedNode | RdfLiteralNode | RdfBlankNode
334
335"""
336An RDF Triple (subject-predicate-object statement)
337"""
338type RdfTriple {
339 """The subject of the triple"""
340 subject: RdfTerm!
341 """The predicate of the triple"""
342 predicate: RdfNamedNode!
343 """The object of the triple"""
344 object: RdfTerm!
345}
346
347"""
348An RDF Quad (triple + named graph)
349"""
350type RdfQuad {
351 """The subject of the quad"""
352 subject: RdfTerm!
353 """The predicate of the quad"""
354 predicate: RdfNamedNode!
355 """The object of the quad"""
356 object: RdfTerm!
357 """The named graph (None for default graph)"""
358 graph: RdfNamedNode
359}
360
361"""
362A variable binding in a SPARQL result
363"""
364type SparqlBinding {
365 """The variable name"""
366 variable: String!
367 """The bound value"""
368 value: RdfTerm!
369}
370
371"""
372A single row from a SPARQL query result set
373"""
374type SparqlResultRow {
375 """Variable bindings as key-value pairs"""
376 bindings: [SparqlBinding!]!
377}
378
379"""
380Results from a SPARQL SELECT query
381"""
382type SparqlSolutions {
383 """Variable names in the result set"""
384 variables: [String!]!
385 """Result rows"""
386 rows: [SparqlResultRow!]!
387 """Total number of results"""
388 count: Int!
389}
390
391"""
392Result from a SPARQL ASK query
393"""
394type SparqlBoolean {
395 """The boolean result"""
396 result: Boolean!
397}
398
399"""
400Graph results from a SPARQL CONSTRUCT or DESCRIBE query
401"""
402type SparqlGraph {
403 """The resulting triples"""
404 triples: [RdfTriple!]!
405 """Total number of triples"""
406 count: Int!
407}
408
409"""
410Result of a SPARQL query
411"""
412union SparqlResult = SparqlSolutions | SparqlBoolean | SparqlGraph
413
414"""
415Information about the RDF store
416"""
417type StoreInfo {
418 """Total number of triples in the store"""
419 tripleCount: Int!
420 """Version of the GraphQL server"""
421 version: String!
422 """Description of the store"""
423 description: String!
424}
425
426"""
427Input for executing SPARQL queries
428"""
429input SparqlQueryInput {
430 """The SPARQL query string"""
431 query: String!
432 """Optional result limit"""
433 limit: Int
434 """Optional result offset"""
435 offset: Int
436}
437
438"""
439Filters for querying RDF data
440"""
441input RdfQueryFilter {
442 """Filter by subject IRI pattern"""
443 subject: String
444 """Filter by predicate IRI pattern"""
445 predicate: String
446 """Filter by object value pattern"""
447 object: String
448 """Filter by named graph"""
449 graph: String
450 """Result limit"""
451 limit: Int
452 """Result offset"""
453 offset: Int
454}
455
456"""
457The root query type
458"""
459type Query {
460 """Get basic information about the RDF store"""
461 info: StoreInfo!
462 """Execute a SPARQL query"""
463 sparql(input: SparqlQueryInput!): SparqlResult!
464 """Get all triples matching optional filters"""
465 triples(filter: RdfQueryFilter): [RdfTriple!]!
466 """Get all subjects in the store"""
467 subjects(limit: Int): [RdfNamedNode!]!
468 """Get all predicates in the store"""
469 predicates(limit: Int): [RdfNamedNode!]!
470 """Search for resources by label or IRI pattern"""
471 search(pattern: String!, limit: Int): [RdfNamedNode!]!
472}
473
474schema {
475 query: Query
476}
477"#;
478
479 Ok(response_builder
480 .status(StatusCode::OK)
481 .header("content-type", "text/plain")
482 .body(sdl.to_string())?)
483 }
484
485 (&Method::GET, "/") => {
487 if config.enable_graphiql {
488 Ok(Response::builder()
489 .status(StatusCode::FOUND)
490 .header("location", "/graphiql")
491 .body(String::new())?)
492 } else if config.enable_playground {
493 Ok(Response::builder()
494 .status(StatusCode::FOUND)
495 .header("location", "/playground")
496 .body(String::new())?)
497 } else {
498 let info = serde_json::json!({
499 "service": "OxiRS GraphQL Server",
500 "version": env!("CARGO_PKG_VERSION"),
501 "description": "GraphQL interface for RDF data using Juniper",
502 "endpoints": {
503 "graphql": "/graphql",
504 "health": "/health",
505 "schema": "/schema"
506 }
507 });
508
509 Ok(response_builder
510 .status(StatusCode::OK)
511 .header("content-type", "application/json")
512 .body(info.to_string())?)
513 }
514 }
515
516 _ => {
518 warn!("Unknown endpoint requested: {} {}", method, path);
519 let error_response = serde_json::json!({
520 "error": "Not Found",
521 "message": format!("Endpoint {} {} not found", method, path),
522 "available_endpoints": [
523 "/graphql",
524 "/health",
525 if config.enable_graphiql { "/graphiql" } else { "" },
526 if config.enable_playground { "/playground" } else { "" },
527 if config.enable_introspection { "/schema" } else { "" }
528 ]
529 });
530
531 Ok(response_builder
532 .status(StatusCode::NOT_FOUND)
533 .header("content-type", "application/json")
534 .body(error_response.to_string())?)
535 }
536 }
537 }
538}
539
540pub struct GraphQLServerBuilder {
542 config: GraphQLServerConfig,
543}
544
545impl GraphQLServerBuilder {
546 pub fn new() -> Self {
547 Self {
548 config: GraphQLServerConfig::default(),
549 }
550 }
551
552 pub fn enable_graphiql(mut self, enable: bool) -> Self {
553 self.config.enable_graphiql = enable;
554 self
555 }
556
557 pub fn enable_playground(mut self, enable: bool) -> Self {
558 self.config.enable_playground = enable;
559 self
560 }
561
562 pub fn enable_introspection(mut self, enable: bool) -> Self {
563 self.config.enable_introspection = enable;
564 self
565 }
566
567 pub fn max_query_depth(mut self, depth: Option<usize>) -> Self {
568 self.config.max_query_depth = depth;
569 self
570 }
571
572 pub fn max_query_complexity(mut self, complexity: Option<usize>) -> Self {
573 self.config.max_query_complexity = complexity;
574 self
575 }
576
577 pub fn cors_enabled(mut self, enabled: bool) -> Self {
578 self.config.cors_enabled = enabled;
579 self
580 }
581
582 pub fn cors_origins(mut self, origins: Vec<String>) -> Self {
583 self.config.cors_origins = Some(origins);
584 self
585 }
586
587 pub fn build(self, store: Arc<RdfStore>) -> JuniperGraphQLServer {
588 JuniperGraphQLServer::with_config(store, self.config)
589 }
590}
591
592impl Default for GraphQLServerBuilder {
593 fn default() -> Self {
594 Self::new()
595 }
596}
597
598pub async fn start_graphql_server(store: Arc<RdfStore>, addr: SocketAddr) -> Result<()> {
600 let server = JuniperGraphQLServer::new(store);
601 server.start(addr).await
602}
603
604pub async fn start_graphql_server_with_config(
606 store: Arc<RdfStore>,
607 addr: SocketAddr,
608 config: GraphQLServerConfig,
609) -> Result<()> {
610 let server = JuniperGraphQLServer::with_config(store, config);
611 server.start(addr).await
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[tokio::test]
619 async fn test_server_creation() {
620 let store = Arc::new(RdfStore::new().expect("Failed to create store"));
621 let server = JuniperGraphQLServer::new(store);
622
623 assert!(server.config.enable_graphiql);
625 assert!(server.config.enable_playground);
626 assert!(server.config.enable_introspection);
627 }
628
629 #[tokio::test]
630 async fn test_server_builder() {
631 let store = Arc::new(RdfStore::new().expect("Failed to create store"));
632
633 let server = GraphQLServerBuilder::new()
634 .enable_graphiql(false)
635 .enable_playground(true)
636 .enable_introspection(false)
637 .max_query_depth(Some(10))
638 .cors_enabled(true)
639 .build(store);
640
641 assert!(!server.config.enable_graphiql);
642 assert!(server.config.enable_playground);
643 assert!(!server.config.enable_introspection);
644 assert_eq!(server.config.max_query_depth, Some(10));
645 assert!(server.config.cors_enabled);
646 }
647
648 #[tokio::test]
649 async fn test_health_endpoint() {
650 let store = Arc::new(RdfStore::new().expect("Failed to create store"));
653 let _server = JuniperGraphQLServer::new(store);
654
655 }
661}