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();
37 let mut mutation_operations = Vec::new();
38
39 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 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 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 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 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 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 fn generate_mock_response_data(&self, operation: &SpecOperation) -> serde_json::Value {
102 let field_name = operation.name.as_str();
104
105 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 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 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 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 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 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 let data = self.generate_mock_response_data(&operation);
175
176 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); 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 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 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}