1use 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
16pub struct GraphQLSchemaRegistry {
18 _schema_sdl: String,
20 query_operations: Vec<SpecOperation>,
22 mutation_operations: Vec<SpecOperation>,
24}
25
26impl GraphQLSchemaRegistry {
27 pub fn from_sdl(sdl: &str) -> Result<Self> {
29 let _schema_doc = parse_schema(sdl).map_err(|e| {
31 mockforge_core::Error::validation(format!("Invalid GraphQL schema: {}", e))
32 })?;
33
34 let mut query_operations = Vec::new();
40 let mut mutation_operations = Vec::new();
41
42 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 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 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 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 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 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 fn generate_mock_response_data(&self, operation: &SpecOperation) -> serde_json::Value {
105 let field_name = operation.name.as_str();
107
108 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 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 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 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 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 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 let data = self.generate_mock_response_data(&operation);
178
179 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); 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 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 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}