allframe_core/router/
graphql.rs

1//! GraphQL protocol adapter
2//!
3//! Provides GraphQL support for the protocol-agnostic router.
4
5use std::{future::Future, pin::Pin};
6
7use super::ProtocolAdapter;
8
9/// GraphQL operation type
10#[derive(Debug, Clone, PartialEq)]
11pub enum OperationType {
12    /// Query operation (read)
13    Query,
14    /// Mutation operation (write)
15    Mutation,
16}
17
18/// GraphQL operation definition
19#[derive(Debug, Clone)]
20pub struct GraphQLOperation {
21    /// Operation type (query or mutation)
22    pub operation_type: OperationType,
23    /// Operation name (e.g., "user", "createUser")
24    pub name: String,
25    /// Handler name to call
26    pub handler: String,
27}
28
29impl GraphQLOperation {
30    /// Create a new GraphQL operation
31    pub fn new(
32        operation_type: OperationType,
33        name: impl Into<String>,
34        handler: impl Into<String>,
35    ) -> Self {
36        Self {
37            operation_type,
38            name: name.into(),
39            handler: handler.into(),
40        }
41    }
42}
43
44/// GraphQL adapter for GraphQL queries and mutations
45///
46/// Handles GraphQL protocol-specific request/response transformation.
47pub struct GraphQLAdapter {
48    operations: Vec<GraphQLOperation>,
49}
50
51impl GraphQLAdapter {
52    /// Create a new GraphQL adapter
53    pub fn new() -> Self {
54        Self {
55            operations: Vec::new(),
56        }
57    }
58
59    /// Register a GraphQL query
60    pub fn query(&mut self, name: &str, handler: &str) -> &mut Self {
61        self.operations
62            .push(GraphQLOperation::new(OperationType::Query, name, handler));
63        self
64    }
65
66    /// Register a GraphQL mutation
67    pub fn mutation(&mut self, name: &str, handler: &str) -> &mut Self {
68        self.operations.push(GraphQLOperation::new(
69            OperationType::Mutation,
70            name,
71            handler,
72        ));
73        self
74    }
75
76    /// Find a matching operation by name and type
77    pub fn match_operation(
78        &self,
79        operation_type: OperationType,
80        name: &str,
81    ) -> Option<&GraphQLOperation> {
82        self.operations
83            .iter()
84            .find(|op| op.operation_type == operation_type && op.name == name)
85    }
86
87    /// Parse a GraphQL query string
88    ///
89    /// Extracts operation type and operation name from a GraphQL query.
90    /// Supports:
91    /// - Named queries: "query GetUser { user }"
92    /// - Shorthand queries: "{ user }"
93    /// - Named mutations: "mutation CreateUser { createUser }"
94    pub fn parse_query(&self, query: &str) -> Result<(OperationType, String), String> {
95        let trimmed = query.trim();
96
97        if trimmed.is_empty() {
98            return Err("Empty GraphQL query".to_string());
99        }
100
101        // Check for explicit operation type
102        if let Some(stripped) = trimmed.strip_prefix("query") {
103            // Extract operation name from query body
104            let name = self.extract_operation_name(stripped)?;
105            Ok((OperationType::Query, name))
106        } else if let Some(stripped) = trimmed.strip_prefix("mutation") {
107            // Extract operation name from mutation body
108            let name = self.extract_operation_name(stripped)?;
109            Ok((OperationType::Mutation, name))
110        } else if trimmed.starts_with('{') {
111            // Shorthand query syntax: { user }
112            let name = self.extract_operation_name(trimmed)?;
113            Ok((OperationType::Query, name))
114        } else {
115            Err("Invalid GraphQL query format".to_string())
116        }
117    }
118
119    /// Extract operation name from query body
120    ///
121    /// Handles: "{ user }", "GetUser { user }", etc.
122    fn extract_operation_name(&self, query_body: &str) -> Result<String, String> {
123        let trimmed = query_body.trim();
124
125        // Find opening brace
126        if let Some(brace_pos) = trimmed.find('{') {
127            // Extract content between braces
128            let content = &trimmed[brace_pos + 1..];
129            if let Some(close_pos) = content.find('}') {
130                let inner = content[..close_pos].trim();
131                // Take first word (operation name)
132                if let Some(name) = inner.split_whitespace().next() {
133                    // Remove parentheses if present (e.g., "user(id: 42)" -> "user")
134                    let name = if let Some(paren_pos) = name.find('(') {
135                        &name[..paren_pos]
136                    } else {
137                        name
138                    };
139                    return Ok(name.to_string());
140                }
141            }
142        }
143
144        Err("Could not extract operation name from query".to_string())
145    }
146
147    /// Generate GraphQL schema for registered operations
148    ///
149    /// Generates GraphQL Schema Definition Language (SDL) from registered
150    /// operations.
151    pub fn generate_schema(&self) -> String {
152        let mut schema = String::new();
153
154        // Generate Query type
155        let queries: Vec<&GraphQLOperation> = self
156            .operations
157            .iter()
158            .filter(|op| op.operation_type == OperationType::Query)
159            .collect();
160
161        if !queries.is_empty() {
162            schema.push_str("type Query {\n");
163            for query in &queries {
164                schema.push_str(&format!("  {}: String\n", query.name));
165            }
166            schema.push_str("}\n\n");
167        }
168
169        // Generate Mutation type
170        let mutations: Vec<&GraphQLOperation> = self
171            .operations
172            .iter()
173            .filter(|op| op.operation_type == OperationType::Mutation)
174            .collect();
175
176        if !mutations.is_empty() {
177            schema.push_str("type Mutation {\n");
178            for mutation in &mutations {
179                schema.push_str(&format!("  {}: String\n", mutation.name));
180            }
181            schema.push_str("}\n\n");
182        }
183
184        // Generate schema definition
185        if !queries.is_empty() || !mutations.is_empty() {
186            schema.push_str("schema {\n");
187            if !queries.is_empty() {
188                schema.push_str("  query: Query\n");
189            }
190            if !mutations.is_empty() {
191                schema.push_str("  mutation: Mutation\n");
192            }
193            schema.push_str("}\n");
194        }
195
196        schema.trim().to_string()
197    }
198}
199
200impl Default for GraphQLAdapter {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206impl ProtocolAdapter for GraphQLAdapter {
207    fn name(&self) -> &str {
208        "graphql"
209    }
210
211    fn handle(
212        &self,
213        request: &str,
214    ) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send + '_>> {
215        // Parse query before async block
216        let parse_result = self.parse_query(request);
217        let operations = self.operations.clone();
218
219        Box::pin(async move {
220            // Handle parse error
221            let (operation_type, operation_name) = match parse_result {
222                Ok(parsed) => parsed,
223                Err(e) => {
224                    let response = format!(r#"{{"errors":[{{"message":"{}"}}]}}"#, e);
225                    return Ok(response);
226                }
227            };
228
229            // Find matching operation
230            let matched_operation = operations
231                .iter()
232                .find(|op| op.operation_type == operation_type && op.name == operation_name);
233
234            match matched_operation {
235                Some(operation) => {
236                    // In full implementation, would call handler here
237                    // For now, return success with handler name
238                    let response = format!(
239                        r#"{{"data":{{"{}":"{}"}},"extensions":{{"handler":"{}"}}}}"#,
240                        operation.name, operation.name, operation.handler
241                    );
242                    Ok(response)
243                }
244                None => {
245                    // Operation not found
246                    let error = format!(
247                        r#"{{"errors":[{{"message":"Operation not found: {}"}}]}}"#,
248                        operation_name
249                    );
250                    Ok(error)
251                }
252            }
253        })
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_graphql_adapter_creation() {
263        let adapter = GraphQLAdapter::new();
264        assert_eq!(adapter.name(), "graphql");
265    }
266
267    #[test]
268    fn test_operation_registration_query() {
269        let mut adapter = GraphQLAdapter::new();
270        adapter.query("user", "get_user");
271
272        assert_eq!(adapter.operations.len(), 1);
273        assert_eq!(adapter.operations[0].operation_type, OperationType::Query);
274        assert_eq!(adapter.operations[0].name, "user");
275        assert_eq!(adapter.operations[0].handler, "get_user");
276    }
277
278    #[test]
279    fn test_operation_registration_mutation() {
280        let mut adapter = GraphQLAdapter::new();
281        adapter.mutation("createUser", "create_user_handler");
282
283        assert_eq!(adapter.operations.len(), 1);
284        assert_eq!(
285            adapter.operations[0].operation_type,
286            OperationType::Mutation
287        );
288        assert_eq!(adapter.operations[0].name, "createUser");
289        assert_eq!(adapter.operations[0].handler, "create_user_handler");
290    }
291
292    #[test]
293    fn test_operation_registration_multiple() {
294        let mut adapter = GraphQLAdapter::new();
295        adapter.query("user", "get_user");
296        adapter.query("users", "list_users");
297        adapter.mutation("createUser", "create_user");
298
299        assert_eq!(adapter.operations.len(), 3);
300    }
301
302    #[test]
303    fn test_match_operation_query() {
304        let mut adapter = GraphQLAdapter::new();
305        adapter.query("user", "get_user");
306
307        let matched = adapter.match_operation(OperationType::Query, "user");
308        assert!(matched.is_some());
309        assert_eq!(matched.unwrap().handler, "get_user");
310    }
311
312    #[test]
313    fn test_match_operation_mutation() {
314        let mut adapter = GraphQLAdapter::new();
315        adapter.mutation("createUser", "create_user");
316
317        let matched = adapter.match_operation(OperationType::Mutation, "createUser");
318        assert!(matched.is_some());
319        assert_eq!(matched.unwrap().handler, "create_user");
320    }
321
322    #[test]
323    fn test_match_operation_not_found() {
324        let adapter = GraphQLAdapter::new();
325        let matched = adapter.match_operation(OperationType::Query, "nonexistent");
326        assert!(matched.is_none());
327    }
328
329    #[test]
330    fn test_match_operation_wrong_type() {
331        let mut adapter = GraphQLAdapter::new();
332        adapter.query("user", "get_user");
333
334        // Try to find as mutation (should fail)
335        let matched = adapter.match_operation(OperationType::Mutation, "user");
336        assert!(matched.is_none());
337    }
338
339    #[test]
340    fn test_parse_query_named() {
341        let adapter = GraphQLAdapter::new();
342        let result = adapter.parse_query("query GetUser { user }");
343
344        assert!(result.is_ok());
345        let (op_type, name) = result.unwrap();
346        assert_eq!(op_type, OperationType::Query);
347        assert_eq!(name, "user");
348    }
349
350    #[test]
351    fn test_parse_query_shorthand() {
352        let adapter = GraphQLAdapter::new();
353        let result = adapter.parse_query("{ user }");
354
355        assert!(result.is_ok());
356        let (op_type, name) = result.unwrap();
357        assert_eq!(op_type, OperationType::Query);
358        assert_eq!(name, "user");
359    }
360
361    #[test]
362    fn test_parse_query_with_args() {
363        let adapter = GraphQLAdapter::new();
364        let result = adapter.parse_query("query { user(id: 42) }");
365
366        assert!(result.is_ok());
367        let (op_type, name) = result.unwrap();
368        assert_eq!(op_type, OperationType::Query);
369        assert_eq!(name, "user");
370    }
371
372    #[test]
373    fn test_parse_mutation_named() {
374        let adapter = GraphQLAdapter::new();
375        let result = adapter.parse_query("mutation CreateUser { createUser }");
376
377        assert!(result.is_ok());
378        let (op_type, name) = result.unwrap();
379        assert_eq!(op_type, OperationType::Mutation);
380        assert_eq!(name, "createUser");
381    }
382
383    #[test]
384    fn test_parse_mutation_with_args() {
385        let adapter = GraphQLAdapter::new();
386        let result = adapter.parse_query("mutation { createUser(name: \"John\") }");
387
388        assert!(result.is_ok());
389        let (op_type, name) = result.unwrap();
390        assert_eq!(op_type, OperationType::Mutation);
391        assert_eq!(name, "createUser");
392    }
393
394    #[test]
395    fn test_parse_query_empty() {
396        let adapter = GraphQLAdapter::new();
397        let result = adapter.parse_query("");
398
399        assert!(result.is_err());
400        assert!(result.unwrap_err().contains("Empty"));
401    }
402
403    #[test]
404    fn test_parse_query_invalid() {
405        let adapter = GraphQLAdapter::new();
406        let result = adapter.parse_query("invalid query");
407
408        assert!(result.is_err());
409        assert!(result.unwrap_err().contains("Invalid GraphQL query format"));
410    }
411
412    #[test]
413    fn test_schema_generation_empty() {
414        let adapter = GraphQLAdapter::new();
415        let schema = adapter.generate_schema();
416        assert_eq!(schema, "");
417    }
418
419    #[test]
420    fn test_schema_generation_with_queries() {
421        let mut adapter = GraphQLAdapter::new();
422        adapter.query("user", "get_user");
423        adapter.query("users", "list_users");
424
425        let schema = adapter.generate_schema();
426        assert!(schema.contains("type Query {"));
427        assert!(schema.contains("user: String"));
428        assert!(schema.contains("users: String"));
429        assert!(schema.contains("schema {"));
430        assert!(schema.contains("query: Query"));
431    }
432
433    #[test]
434    fn test_schema_generation_with_mutations() {
435        let mut adapter = GraphQLAdapter::new();
436        adapter.mutation("createUser", "create_user");
437        adapter.mutation("deleteUser", "delete_user");
438
439        let schema = adapter.generate_schema();
440        assert!(schema.contains("type Mutation {"));
441        assert!(schema.contains("createUser: String"));
442        assert!(schema.contains("deleteUser: String"));
443        assert!(schema.contains("schema {"));
444        assert!(schema.contains("mutation: Mutation"));
445    }
446
447    #[test]
448    fn test_schema_generation_with_both() {
449        let mut adapter = GraphQLAdapter::new();
450        adapter.query("user", "get_user");
451        adapter.mutation("createUser", "create_user");
452
453        let schema = adapter.generate_schema();
454        assert!(schema.contains("type Query {"));
455        assert!(schema.contains("type Mutation {"));
456        assert!(schema.contains("query: Query"));
457        assert!(schema.contains("mutation: Mutation"));
458    }
459
460    #[tokio::test]
461    async fn test_handle_query_success() {
462        let mut adapter = GraphQLAdapter::new();
463        adapter.query("user", "get_user");
464
465        let result = adapter.handle("query { user }").await;
466        assert!(result.is_ok());
467
468        let response = result.unwrap();
469        assert!(response.contains(r#""data""#));
470        assert!(response.contains("user"));
471        assert!(response.contains("get_user"));
472    }
473
474    #[tokio::test]
475    async fn test_handle_mutation_success() {
476        let mut adapter = GraphQLAdapter::new();
477        adapter.mutation("createUser", "create_user_handler");
478
479        let result = adapter.handle("mutation { createUser }").await;
480        assert!(result.is_ok());
481
482        let response = result.unwrap();
483        assert!(response.contains(r#""data""#));
484        assert!(response.contains("createUser"));
485        assert!(response.contains("create_user_handler"));
486    }
487
488    #[tokio::test]
489    async fn test_handle_operation_not_found() {
490        let adapter = GraphQLAdapter::new();
491        let result = adapter.handle("query { user }").await;
492
493        assert!(result.is_ok());
494        let response = result.unwrap();
495        assert!(response.contains(r#""errors""#));
496        assert!(response.contains("Operation not found"));
497    }
498
499    #[tokio::test]
500    async fn test_handle_invalid_query() {
501        let adapter = GraphQLAdapter::new();
502        let result = adapter.handle("invalid query").await;
503
504        assert!(result.is_ok());
505        let response = result.unwrap();
506        assert!(response.contains(r#""errors""#));
507    }
508
509    #[tokio::test]
510    async fn test_handle_shorthand_query() {
511        let mut adapter = GraphQLAdapter::new();
512        adapter.query("user", "get_user");
513
514        let result = adapter.handle("{ user }").await;
515        assert!(result.is_ok());
516
517        let response = result.unwrap();
518        assert!(response.contains(r#""data""#));
519        assert!(response.contains("user"));
520    }
521
522    #[test]
523    fn test_graphql_operation_new() {
524        let op = GraphQLOperation::new(OperationType::Query, "user", "get_user");
525        assert_eq!(op.operation_type, OperationType::Query);
526        assert_eq!(op.name, "user");
527        assert_eq!(op.handler, "get_user");
528    }
529}