Skip to main content

oxirs_gql/
juniper_server.rs

1//! Juniper GraphQL HTTP server implementation
2//!
3//! This module provides a complete HTTP server implementation using Juniper
4//! for GraphQL processing and Hyper for HTTP handling.
5
6use 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/// Configuration for the GraphQL server
24#[derive(Debug, Clone)]
25pub struct GraphQLServerConfig {
26    /// Enable GraphiQL web interface
27    pub enable_graphiql: bool,
28    /// Enable GraphQL Playground web interface  
29    pub enable_playground: bool,
30    /// Enable introspection queries
31    pub enable_introspection: bool,
32    /// Maximum query depth allowed
33    pub max_query_depth: Option<usize>,
34    /// Maximum query complexity allowed
35    pub max_query_complexity: Option<usize>,
36    /// CORS configuration
37    pub cors_enabled: bool,
38    /// Allowed CORS origins (None means all origins allowed)
39    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, // Allow all origins by default
52        }
53    }
54}
55
56/// The main GraphQL server using Juniper
57pub struct JuniperGraphQLServer {
58    schema: Arc<Schema>,
59    context: GraphQLContext,
60    config: GraphQLServerConfig,
61}
62
63impl JuniperGraphQLServer {
64    /// Create a new GraphQL server with an RDF store
65    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    /// Create a new GraphQL server with custom configuration
78    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    /// Start the GraphQL server on the specified address
90    pub async fn start(&self, addr: SocketAddr) -> Result<()> {
91        info!("Starting Juniper GraphQL server on {}", addr);
92
93        // Create the service with proper cloning for the closure
94        let schema = self.schema.clone();
95        let context = self.context.clone();
96        let config = self.config.clone();
97
98        // Create TCP listener
99        let listener = TcpListener::bind(addr).await?;
100
101        info!("GraphQL server running on http://{}", addr);
102        info!("GraphQL endpoint: http://{}/graphql", addr);
103
104        // Accept connections in a loop
105        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            // Handle each connection in a separate task
119            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    /// Handle individual HTTP requests
140    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    /// Inner request handling with proper error propagation
165    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        // Apply CORS headers if enabled
177        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        // Handle CORS preflight requests
189        if method == Method::OPTIONS {
190            return Ok(response_builder
191                .status(StatusCode::OK)
192                .body(String::new())?);
193        }
194
195        match (method, path) {
196            // Main GraphQL endpoint
197            (&Method::GET, "/graphql") | (&Method::POST, "/graphql") => {
198                debug!("Processing GraphQL request");
199                let mut response = graphql(schema, Arc::new(context), req).await;
200
201                // Add CORS headers to GraphQL response
202                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            // GraphiQL interface (enhanced)
225            (&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            // GraphQL Playground
251            (&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            // Health check endpoint
258            (&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            // Schema introspection endpoint (SDL)
279            (&Method::GET, "/schema") if config.enable_introspection => {
280                debug!("Schema introspection request");
281
282                // Complete SDL generated from Juniper schema
283                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            // Root endpoint - redirect to GraphiQL or provide info
486            (&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            // 404 for unknown endpoints
517            _ => {
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
540/// Builder for GraphQL server configuration
541pub 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
598/// Convenience function to start a GraphQL server with default configuration
599pub async fn start_graphql_server(store: Arc<RdfStore>, addr: SocketAddr) -> Result<()> {
600    let server = JuniperGraphQLServer::new(store);
601    server.start(addr).await
602}
603
604/// Convenience function to start a GraphQL server with custom configuration
605pub 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        // Just test that we can create the server
624        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        // This test would require actually starting a server
651        // For now, just test the builder functionality
652        let store = Arc::new(RdfStore::new().expect("Failed to create store"));
653        let _server = JuniperGraphQLServer::new(store);
654
655        // In a real test, we would:
656        // 1. Start the server on a random port
657        // 2. Make HTTP requests to test endpoints
658        // 3. Verify responses
659        // This requires more complex test infrastructure
660    }
661}