fraiseql_core/federation/
mutation_http_client.rs1use std::time::Duration;
7
8use serde_json::Value;
9
10use crate::{
11 error::{FraiseQLError, Result},
12 federation::{metadata_helpers::find_federation_type, types::FederationMetadata},
13};
14
15#[derive(Debug, Clone)]
17pub struct HttpMutationConfig {
18 pub timeout_ms: u64,
20 pub max_retries: u32,
22 pub retry_delay_ms: u64,
24}
25
26impl Default for HttpMutationConfig {
27 fn default() -> Self {
28 Self {
29 timeout_ms: 5000,
30 max_retries: 3,
31 retry_delay_ms: 100,
32 }
33 }
34}
35
36#[derive(Debug, Clone, serde::Serialize)]
38pub struct GraphQLRequest {
39 pub query: String,
41 pub variables: Value,
43}
44
45#[derive(Debug, Clone, serde::Deserialize)]
47pub struct GraphQLResponse {
48 pub data: Option<Value>,
50 pub errors: Option<Vec<GraphQLError>>,
52}
53
54#[derive(Debug, Clone, serde::Deserialize)]
56pub struct GraphQLError {
57 pub message: String,
59}
60
61pub struct HttpMutationClient {
63 client: Option<reqwest::Client>,
65 config: HttpMutationConfig,
67}
68
69impl HttpMutationClient {
70 pub fn new(config: HttpMutationConfig) -> Self {
72 let client = reqwest::Client::builder()
73 .timeout(Duration::from_millis(config.timeout_ms))
74 .build()
75 .ok();
76
77 Self { client, config }
78 }
79
80 pub async fn execute_mutation(
82 &self,
83 subgraph_url: &str,
84 typename: &str,
85 mutation_name: &str,
86 variables: &Value,
87 metadata: &FederationMetadata,
88 ) -> Result<Value> {
89 let client = self.client.as_ref().ok_or_else(|| FraiseQLError::Internal {
90 message: "HTTP client not initialized".to_string(),
91 source: None,
92 })?;
93
94 let fed_type = find_federation_type(typename, metadata)?;
96
97 let query = self.build_mutation_query(typename, mutation_name, variables, fed_type)?;
99
100 let response = self.execute_with_retry(client, subgraph_url, &query).await?;
102
103 self.parse_response(response, mutation_name)
105 }
106
107 pub fn build_mutation_query(
109 &self,
110 _typename: &str,
111 mutation_name: &str,
112 variables: &Value,
113 fed_type: &crate::federation::types::FederatedType,
114 ) -> Result<GraphQLRequest> {
115 let key_fields = if let Some(key_directive) = fed_type.keys.first() {
117 key_directive.fields.join(" ")
118 } else {
119 "id".to_string()
120 };
121
122 let mut input_fields = Vec::new();
124 if let Some(obj) = variables.as_object() {
125 for key in obj.keys() {
126 if !fed_type.external_fields.contains(key) {
127 input_fields.push(format!("{}: ${}", key, key));
128 }
129 }
130 }
131
132 let var_defs = self.build_variable_definitions(variables)?;
134
135 let response_fields = format!("__typename {}", key_fields);
137 let input_clause = input_fields.join(", ");
138
139 let query = format!(
140 "mutation({}) {{ {}({}) {{ {} }} }}",
141 var_defs, mutation_name, input_clause, response_fields
142 );
143
144 Ok(GraphQLRequest {
145 query,
146 variables: variables.clone(),
147 })
148 }
149
150 pub fn build_variable_definitions(&self, variables: &Value) -> Result<String> {
152 let mut var_defs = Vec::new();
153
154 if let Some(obj) = variables.as_object() {
155 for key in obj.keys() {
156 let var_type = match &obj[key] {
158 Value::String(_) => "String!",
159 Value::Number(_) => "Int!",
160 Value::Bool(_) => "Boolean!",
161 Value::Null => "String",
162 _ => "JSON", };
164 var_defs.push(format!("${}: {}", key, var_type));
165 }
166 }
167
168 Ok(format!("({})", var_defs.join(", ")))
169 }
170
171 async fn execute_with_retry(
173 &self,
174 client: &reqwest::Client,
175 url: &str,
176 request: &GraphQLRequest,
177 ) -> Result<GraphQLResponse> {
178 let mut attempts = 0;
179 let mut last_error = None;
180
181 while attempts < self.config.max_retries {
182 attempts += 1;
183
184 match client.post(url).json(request).send().await {
185 Ok(response) if response.status().is_success() => {
186 return response.json().await.map_err(|e| FraiseQLError::Internal {
187 message: format!("Failed to parse GraphQL response: {}", e),
188 source: None,
189 });
190 },
191 Ok(response) => {
192 last_error = Some(format!("HTTP {}", response.status()));
193 },
194 Err(e) => {
195 last_error = Some(e.to_string());
196 },
197 }
198
199 if attempts < self.config.max_retries {
200 tokio::time::sleep(Duration::from_millis(
201 self.config.retry_delay_ms * u64::from(attempts),
202 ))
203 .await;
204 }
205 }
206
207 Err(FraiseQLError::Internal {
208 message: format!(
209 "Mutation request failed after {} attempts: {}",
210 self.config.max_retries,
211 last_error.unwrap_or_else(|| "Unknown error".to_string())
212 ),
213 source: None,
214 })
215 }
216
217 pub fn parse_response(&self, response: GraphQLResponse, mutation_name: &str) -> Result<Value> {
219 if let Some(errors) = response.errors {
221 let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
222 return Err(FraiseQLError::Internal {
223 message: format!(
224 "GraphQL error in mutation response: {}",
225 error_messages.join("; ")
226 ),
227 source: None,
228 });
229 }
230
231 let data = response.data.ok_or_else(|| FraiseQLError::Internal {
233 message: "No data in mutation response".to_string(),
234 source: None,
235 })?;
236
237 let result = data.get(mutation_name).cloned().ok_or_else(|| FraiseQLError::Internal {
238 message: format!("No {} field in response data", mutation_name),
239 source: None,
240 })?;
241
242 Ok(result)
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use serde_json::json;
249
250 use super::*;
251
252 #[test]
253 fn test_http_mutation_client_creation() {
254 let config = HttpMutationConfig::default();
255 let _client = HttpMutationClient::new(config);
256 }
258
259 #[test]
260 fn test_mutation_config_defaults() {
261 let config = HttpMutationConfig::default();
262 assert_eq!(config.timeout_ms, 5000);
263 assert_eq!(config.max_retries, 3);
264 assert_eq!(config.retry_delay_ms, 100);
265 }
266
267 #[test]
268 fn test_mutation_config_custom() {
269 let config = HttpMutationConfig {
270 timeout_ms: 10000,
271 max_retries: 5,
272 retry_delay_ms: 200,
273 };
274 assert_eq!(config.timeout_ms, 10000);
275 assert_eq!(config.max_retries, 5);
276 assert_eq!(config.retry_delay_ms, 200);
277 }
278
279 #[test]
280 fn test_graphql_request_serialization() {
281 let request = GraphQLRequest {
282 query: "mutation { updateUser(id: $id) { id } }".to_string(),
283 variables: json!({ "id": "123" }),
284 };
285
286 let json = serde_json::to_value(&request).unwrap();
287 assert_eq!(json["query"], "mutation { updateUser(id: $id) { id } }");
288 assert_eq!(json["variables"]["id"], "123");
289 }
290
291 #[test]
292 fn test_graphql_response_parsing_success() {
293 let response_json = json!({
294 "data": {
295 "updateUser": {
296 "__typename": "User",
297 "id": "123",
298 "name": "Alice"
299 }
300 }
301 });
302
303 let response: GraphQLResponse = serde_json::from_value(response_json).unwrap();
304 assert!(response.data.is_some());
305 assert!(response.errors.is_none());
306
307 let data = response.data.unwrap();
308 assert_eq!(data["updateUser"]["id"], "123");
309 }
310
311 #[test]
312 fn test_graphql_response_with_errors() {
313 let response_json = json!({
314 "data": null,
315 "errors": [
316 {
317 "message": "User not found"
318 }
319 ]
320 });
321
322 let response: GraphQLResponse = serde_json::from_value(response_json).unwrap();
323 assert!(response.data.is_none());
324 assert!(response.errors.is_some());
325 assert_eq!(response.errors.unwrap()[0].message, "User not found");
326 }
327
328 #[test]
329 fn test_variable_definition_building() {
330 let config = HttpMutationConfig::default();
331 let client = HttpMutationClient::new(config);
332
333 let variables = json!({
334 "id": "123",
335 "name": "Alice",
336 "active": true
337 });
338
339 let var_defs = client.build_variable_definitions(&variables).unwrap();
340 assert!(var_defs.contains("$id: String!"));
341 assert!(var_defs.contains("$name: String!"));
342 assert!(var_defs.contains("$active: Boolean!"));
343 }
344
345 #[test]
346 fn test_variable_definition_with_numbers() {
347 let config = HttpMutationConfig::default();
348 let client = HttpMutationClient::new(config);
349
350 let variables = json!({
351 "count": 42,
352 "price": 9.99
353 });
354
355 let var_defs = client.build_variable_definitions(&variables).unwrap();
356 assert!(var_defs.contains("$count: Int!"));
357 assert!(var_defs.contains("$price: Int!"));
358 }
359}