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