mockforge_graphql/
registry.rs

1//! GraphQL Schema Registry - SpecRegistry implementation for GraphQL
2//!
3//! This module provides a SpecRegistry implementation that can load GraphQL schemas
4//! from files and generate mock responses.
5
6use async_graphql::parser::parse_schema;
7use mockforge_core::protocol_abstraction::{
8    Protocol, ProtocolRequest, ProtocolResponse, ResponseStatus, SpecOperation, SpecRegistry,
9};
10use mockforge_core::{
11    ProtocolValidationError as ValidationError, ProtocolValidationResult as ValidationResult,
12    Result,
13};
14use std::collections::HashMap;
15
16/// GraphQL Schema Registry implementing SpecRegistry
17pub struct GraphQLSchemaRegistry {
18    /// Parsed schema SDL
19    _schema_sdl: String,
20    /// Query operations
21    query_operations: Vec<SpecOperation>,
22    /// Mutation operations
23    mutation_operations: Vec<SpecOperation>,
24}
25
26impl GraphQLSchemaRegistry {
27    /// Create a new GraphQL schema registry from SDL string
28    pub fn from_sdl(sdl: &str) -> Result<Self> {
29        // Validate SDL by parsing it with async-graphql's parser
30        let _schema_doc = parse_schema(sdl).map_err(|e| {
31            mockforge_core::Error::validation(format!("Invalid GraphQL schema: {}", e))
32        })?;
33
34        // Extract operation names from the SDL using string parsing
35        // Note: This is a pragmatic approach for operation matching. While the async-graphql
36        // parser validates the schema, extracting the operation names via string parsing is
37        // simpler and sufficient for our handler matching needs. The schema is already
38        // validated above, so we know it's well-formed.
39        let mut query_operations = Vec::new();
40        let mut mutation_operations = Vec::new();
41
42        // Extract Query type fields
43        if let Some(query_start) = sdl.find("type Query") {
44            if let Some(query_block) = Self::extract_type_block(sdl, query_start) {
45                query_operations = Self::extract_fields_as_operations(&query_block, "Query");
46            }
47        }
48
49        // Extract Mutation type fields
50        if let Some(mutation_start) = sdl.find("type Mutation") {
51            if let Some(mutation_block) = Self::extract_type_block(sdl, mutation_start) {
52                mutation_operations =
53                    Self::extract_fields_as_operations(&mutation_block, "Mutation");
54            }
55        }
56
57        Ok(Self {
58            _schema_sdl: sdl.to_string(),
59            query_operations,
60            mutation_operations,
61        })
62    }
63
64    /// Extract a type block from SDL (everything between { and })
65    fn extract_type_block(sdl: &str, start_pos: usize) -> Option<String> {
66        let remaining = &sdl[start_pos..];
67        let open_brace = remaining.find('{')?;
68        let close_brace = remaining.find('}')?;
69        Some(remaining[open_brace + 1..close_brace].to_string())
70    }
71
72    /// Extract field names from a type block and convert to operations
73    fn extract_fields_as_operations(block: &str, operation_type: &str) -> Vec<SpecOperation> {
74        block
75            .lines()
76            .filter_map(|line| {
77                let trimmed = line.trim();
78                if trimmed.is_empty() || trimmed.starts_with('#') {
79                    return None;
80                }
81
82                // Extract field name (before '(' or ':')
83                let field_name = trimmed.split(['(', ':']).next()?.trim().to_string();
84
85                Some(SpecOperation {
86                    name: field_name.clone(),
87                    path: format!("{}.{}", operation_type, field_name),
88                    operation_type: operation_type.to_string(),
89                    input_schema: None,
90                    output_schema: None,
91                    metadata: HashMap::new(),
92                })
93            })
94            .collect()
95    }
96
97    /// Load schema from file
98    pub async fn from_file(path: &str) -> Result<Self> {
99        let sdl = tokio::fs::read_to_string(path).await?;
100        Self::from_sdl(&sdl)
101    }
102
103    /// Generate mock response for a query/mutation
104    fn generate_mock_response_data(&self, operation: &SpecOperation) -> serde_json::Value {
105        // Extract the field name from the operation path (e.g., "Query.users" -> "users")
106        let field_name = operation.name.as_str();
107
108        // Check if it returns a list (common pattern: plural names or explicit list type)
109        let is_list = field_name.ends_with('s')
110            || operation.output_schema.as_ref().map(|s| s.starts_with('[')).unwrap_or(false);
111
112        if is_list {
113            // Generate a list of mock objects
114            let items: Vec<serde_json::Value> = (0..3)
115                .map(|i| {
116                    serde_json::json!({
117                        "id": format!("{}-{}", field_name, i),
118                        "name": format!("Mock {} {}", field_name, i),
119                        "description": format!("This is mock {} number {}", field_name, i),
120                    })
121                })
122                .collect();
123            serde_json::json!(items)
124        } else {
125            // Generate a single mock object
126            serde_json::json!({
127                "id": format!("{}-1", field_name),
128                "name": format!("Mock {}", field_name),
129                "description": format!("This is a mock {}", field_name),
130            })
131        }
132    }
133}
134
135impl SpecRegistry for GraphQLSchemaRegistry {
136    fn protocol(&self) -> Protocol {
137        Protocol::GraphQL
138    }
139
140    fn operations(&self) -> Vec<SpecOperation> {
141        let mut ops = self.query_operations.clone();
142        ops.extend(self.mutation_operations.clone());
143        ops
144    }
145
146    fn find_operation(&self, operation: &str, _path: &str) -> Option<SpecOperation> {
147        // Operation format: "Query.fieldName" or "Mutation.fieldName"
148        self.operations()
149            .into_iter()
150            .find(|op| op.path == operation || op.name == operation)
151    }
152
153    fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult> {
154        // For now, basic validation - just check if the operation exists
155        if let Some(_op) = self.find_operation(&request.operation, &request.path) {
156            Ok(ValidationResult::success())
157        } else {
158            Ok(ValidationResult::failure(vec![ValidationError {
159                message: format!("Unknown GraphQL operation: {}", request.operation),
160                path: Some(request.path.clone()),
161                code: Some("UNKNOWN_OPERATION".to_string()),
162            }]))
163        }
164    }
165
166    fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse> {
167        // Find the operation
168        let operation =
169            self.find_operation(&request.operation, &request.path).ok_or_else(|| {
170                mockforge_core::Error::validation(format!(
171                    "Unknown operation: {}",
172                    request.operation
173                ))
174            })?;
175
176        // Generate mock data
177        let data = self.generate_mock_response_data(&operation);
178
179        // Create GraphQL response format
180        let graphql_response = serde_json::json!({
181            "data": {
182                &operation.name: data
183            }
184        });
185
186        let body = serde_json::to_vec(&graphql_response)?;
187
188        Ok(ProtocolResponse {
189            status: ResponseStatus::GraphQLStatus(true),
190            metadata: {
191                let mut m = HashMap::new();
192                m.insert("content-type".to_string(), "application/json".to_string());
193                m
194            },
195            body,
196            content_type: "application/json".to_string(),
197        })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    const SAMPLE_SCHEMA: &str = r#"
206        type Query {
207            users(limit: Int): [User!]!
208            user(id: ID!): User
209            posts(limit: Int): [Post!]!
210        }
211
212        type Mutation {
213            createUser(input: CreateUserInput!): User!
214            updateUser(id: ID!, input: UpdateUserInput!): User
215            deleteUser(id: ID!): Boolean!
216        }
217
218        type User {
219            id: ID!
220            name: String!
221            email: String!
222            posts: [Post!]!
223        }
224
225        type Post {
226            id: ID!
227            title: String!
228            content: String!
229            author: User!
230        }
231
232        input CreateUserInput {
233            name: String!
234            email: String!
235        }
236
237        input UpdateUserInput {
238            name: String
239            email: String
240        }
241    "#;
242
243    #[test]
244    fn test_from_sdl() {
245        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA);
246        assert!(registry.is_ok());
247
248        let registry = registry.unwrap();
249        assert_eq!(registry.query_operations.len(), 3);
250        assert_eq!(registry.mutation_operations.len(), 3);
251    }
252
253    #[test]
254    fn test_protocol() {
255        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
256        assert_eq!(registry.protocol(), Protocol::GraphQL);
257    }
258
259    #[test]
260    fn test_operations() {
261        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
262        let ops = registry.operations();
263        assert_eq!(ops.len(), 6); // 3 queries + 3 mutations
264
265        // Check query operations
266        assert!(ops.iter().any(|op| op.name == "users"));
267        assert!(ops.iter().any(|op| op.name == "user"));
268        assert!(ops.iter().any(|op| op.name == "posts"));
269
270        // Check mutation operations
271        assert!(ops.iter().any(|op| op.name == "createUser"));
272        assert!(ops.iter().any(|op| op.name == "updateUser"));
273        assert!(ops.iter().any(|op| op.name == "deleteUser"));
274    }
275
276    #[test]
277    fn test_find_operation() {
278        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
279
280        let op = registry.find_operation("Query.users", "/graphql");
281        assert!(op.is_some());
282        assert_eq!(op.unwrap().name, "users");
283
284        let op = registry.find_operation("Mutation.createUser", "/graphql");
285        assert!(op.is_some());
286        assert_eq!(op.unwrap().name, "createUser");
287
288        let op = registry.find_operation("nonexistent", "/graphql");
289        assert!(op.is_none());
290    }
291
292    #[test]
293    fn test_validate_request() {
294        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
295
296        let request = ProtocolRequest {
297            protocol: Protocol::GraphQL,
298            pattern: mockforge_core::protocol_abstraction::MessagePattern::RequestResponse,
299            topic: None,
300            routing_key: None,
301            partition: None,
302            qos: None,
303            operation: "Query.users".to_string(),
304            path: "/graphql".to_string(),
305            metadata: HashMap::new(),
306            body: None,
307            client_ip: None,
308        };
309
310        let result = registry.validate_request(&request);
311        assert!(result.is_ok());
312        assert!(result.unwrap().valid);
313    }
314
315    #[test]
316    fn test_generate_mock_response() {
317        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
318
319        let request = ProtocolRequest {
320            protocol: Protocol::GraphQL,
321            pattern: mockforge_core::protocol_abstraction::MessagePattern::RequestResponse,
322            topic: None,
323            routing_key: None,
324            partition: None,
325            qos: None,
326            operation: "Query.users".to_string(),
327            path: "/graphql".to_string(),
328            metadata: HashMap::new(),
329            body: Some(b"{\"query\": \"{ users { id name email } }\"}".to_vec()),
330            client_ip: None,
331        };
332
333        let response = registry.generate_mock_response(&request);
334        assert!(response.is_ok());
335
336        let response = response.unwrap();
337        assert_eq!(response.status, ResponseStatus::GraphQLStatus(true));
338        assert_eq!(response.content_type, "application/json");
339
340        // Parse response body
341        let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
342        assert!(body.get("data").is_some());
343        assert!(body["data"].get("users").is_some());
344    }
345
346    #[test]
347    fn test_generate_mock_response_mutation() {
348        let registry = GraphQLSchemaRegistry::from_sdl(SAMPLE_SCHEMA).unwrap();
349
350        let request = ProtocolRequest {
351            protocol: Protocol::GraphQL,
352            pattern: mockforge_core::protocol_abstraction::MessagePattern::RequestResponse,
353            topic: None,
354            routing_key: None,
355            partition: None,
356            qos: None,
357            operation: "Mutation.createUser".to_string(),
358            path: "/graphql".to_string(),
359            metadata: HashMap::new(),
360            body: Some(b"{\"query\": \"mutation { createUser(input: {name: \\\"Test\\\", email: \\\"test@example.com\\\"}) { id name email } }\"}".to_vec()),
361            client_ip: None,
362        };
363
364        let response = registry.generate_mock_response(&request);
365        assert!(response.is_ok());
366
367        let response = response.unwrap();
368        let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
369        assert!(body.get("data").is_some());
370        assert!(body["data"].get("createUser").is_some());
371    }
372
373    #[tokio::test]
374    async fn test_from_file_nonexistent() {
375        let result = GraphQLSchemaRegistry::from_file("/nonexistent/schema.graphql").await;
376        assert!(result.is_err());
377    }
378}