reinhardt_views/viewsets/
batch_operations.rs1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "operation")]
15pub enum BatchOperation<T> {
16 #[serde(rename = "create")]
18 Create {
19 data: T,
21 },
22 #[serde(rename = "update")]
24 Update {
25 id: String,
27 data: T,
29 },
30 #[serde(rename = "partial_update")]
32 PartialUpdate {
33 id: String,
35 data: T,
37 },
38 #[serde(rename = "delete")]
40 Delete {
41 id: String,
43 },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct BatchOperationResult<T> {
49 pub index: usize,
51 pub success: bool,
53 pub data: Option<T>,
55 pub error: Option<String>,
57}
58
59impl<T> BatchOperationResult<T> {
60 pub fn success(index: usize, data: Option<T>) -> Self {
72 Self {
73 index,
74 success: true,
75 data,
76 error: None,
77 }
78 }
79
80 pub fn failure(index: usize, error: impl Into<String>) -> Self {
92 Self {
93 index,
94 success: false,
95 data: None,
96 error: Some(error.into()),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BatchRequest<T> {
104 pub operations: Vec<BatchOperation<T>>,
106 #[serde(default)]
108 pub atomic: bool,
109}
110
111impl<T> BatchRequest<T> {
112 pub fn new(operations: Vec<BatchOperation<T>>) -> Self {
126 Self {
127 operations,
128 atomic: false,
129 }
130 }
131
132 pub fn atomic(mut self) -> Self {
134 self.atomic = true;
135 self
136 }
137
138 pub fn len(&self) -> usize {
140 self.operations.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.operations.is_empty()
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct BatchResponse<T> {
152 pub results: Vec<BatchOperationResult<T>>,
154 pub total: usize,
156 pub succeeded: usize,
158 pub failed: usize,
160}
161
162impl<T> BatchResponse<T> {
163 pub fn new(results: Vec<BatchOperationResult<T>>) -> Self {
180 let total = results.len();
181 let succeeded = results.iter().filter(|r| r.success).count();
182 let failed = total - succeeded;
183
184 Self {
185 results,
186 total,
187 succeeded,
188 failed,
189 }
190 }
191
192 pub fn all_succeeded(&self) -> bool {
194 self.failed == 0
195 }
196
197 pub fn any_failed(&self) -> bool {
199 self.failed > 0
200 }
201}
202
203pub struct BatchProcessor;
205
206impl BatchProcessor {
207 pub fn process<T, F>(request: BatchRequest<T>, mut handler: F) -> BatchResponse<T>
230 where
231 F: FnMut(&BatchOperation<T>, usize) -> std::result::Result<T, String>,
232 {
233 let mut results = Vec::new();
234
235 for (index, operation) in request.operations.iter().enumerate() {
236 match handler(operation, index) {
237 Ok(data) => {
238 results.push(BatchOperationResult::success(index, Some(data)));
239 }
240 Err(error) => {
241 results.push(BatchOperationResult::failure(index, error));
242
243 if request.atomic {
245 break;
246 }
247 }
248 }
249 }
250
251 BatchResponse::new(results)
252 }
253
254 pub fn validate_size<T>(
256 request: &BatchRequest<T>,
257 max_size: usize,
258 ) -> std::result::Result<(), String> {
259 if request.operations.len() > max_size {
260 return Err(format!(
261 "Batch size {} exceeds maximum {}",
262 request.operations.len(),
263 max_size
264 ));
265 }
266 Ok(())
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct BatchStatistics {
273 pub by_type: HashMap<String, usize>,
275 pub processing_time_ms: u64,
277}
278
279impl BatchStatistics {
280 pub fn new() -> Self {
282 Self {
283 by_type: HashMap::new(),
284 processing_time_ms: 0,
285 }
286 }
287
288 pub fn increment(&mut self, operation_type: impl Into<String>) {
290 *self.by_type.entry(operation_type.into()).or_insert(0) += 1;
291 }
292
293 pub fn set_processing_time(&mut self, ms: u64) {
295 self.processing_time_ms = ms;
296 }
297}
298
299impl Default for BatchStatistics {
300 fn default() -> Self {
301 Self::new()
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_batch_operation_result_success() {
311 let result = BatchOperationResult::success(0, Some("data".to_string()));
312 assert!(result.success);
313 assert_eq!(result.index, 0);
314 assert_eq!(result.data, Some("data".to_string()));
315 assert_eq!(result.error, None);
316 }
317
318 #[test]
319 fn test_batch_operation_result_failure() {
320 let result: BatchOperationResult<String> =
321 BatchOperationResult::failure(1, "Error message");
322 assert!(!result.success);
323 assert_eq!(result.index, 1);
324 assert_eq!(result.data, None);
325 assert_eq!(result.error, Some("Error message".to_string()));
326 }
327
328 #[test]
329 fn test_batch_request_new() {
330 let operations = vec![
331 BatchOperation::Create {
332 data: "item1".to_string(),
333 },
334 BatchOperation::Create {
335 data: "item2".to_string(),
336 },
337 ];
338
339 let request = BatchRequest::new(operations);
340 assert_eq!(request.len(), 2);
341 assert!(!request.atomic);
342 }
343
344 #[test]
345 fn test_batch_request_atomic() {
346 let request: BatchRequest<String> = BatchRequest::new(vec![]).atomic();
347 assert!(request.atomic);
348 }
349
350 #[test]
351 fn test_batch_response_statistics() {
352 let results = vec![
353 BatchOperationResult::success(0, Some("data1".to_string())),
354 BatchOperationResult::success(1, Some("data2".to_string())),
355 BatchOperationResult::failure(2, "Error"),
356 ];
357
358 let response = BatchResponse::new(results);
359 assert_eq!(response.total, 3);
360 assert_eq!(response.succeeded, 2);
361 assert_eq!(response.failed, 1);
362 assert!(!response.all_succeeded());
363 assert!(response.any_failed());
364 }
365
366 #[test]
367 fn test_batch_processor_all_success() {
368 let request = BatchRequest::new(vec![
369 BatchOperation::Create {
370 data: "item1".to_string(),
371 },
372 BatchOperation::Create {
373 data: "item2".to_string(),
374 },
375 ]);
376
377 let response = BatchProcessor::process(request, |op, _index| match op {
378 BatchOperation::Create { data } => Ok(format!("Created: {}", data)),
379 _ => Err("Unsupported".to_string()),
380 });
381
382 assert_eq!(response.total, 2);
383 assert_eq!(response.succeeded, 2);
384 assert!(response.all_succeeded());
385 }
386
387 #[test]
388 fn test_batch_processor_with_errors() {
389 let request = BatchRequest::new(vec![
390 BatchOperation::Create {
391 data: "item1".to_string(),
392 },
393 BatchOperation::Create {
394 data: "fail".to_string(),
395 },
396 BatchOperation::Create {
397 data: "item3".to_string(),
398 },
399 ]);
400
401 let response = BatchProcessor::process(request, |op, _index| match op {
402 BatchOperation::Create { data } => {
403 if data == "fail" {
404 Err("Failed".to_string())
405 } else {
406 Ok(format!("Created: {}", data))
407 }
408 }
409 _ => Err("Unsupported".to_string()),
410 });
411
412 assert_eq!(response.total, 3);
413 assert_eq!(response.succeeded, 2);
414 assert_eq!(response.failed, 1);
415 }
416
417 #[test]
418 fn test_batch_processor_atomic_mode() {
419 let request = BatchRequest::new(vec![
420 BatchOperation::Create {
421 data: "item1".to_string(),
422 },
423 BatchOperation::Create {
424 data: "fail".to_string(),
425 },
426 BatchOperation::Create {
427 data: "item3".to_string(),
428 },
429 ])
430 .atomic();
431
432 let response = BatchProcessor::process(request, |op, _index| match op {
433 BatchOperation::Create { data } => {
434 if data == "fail" {
435 Err("Failed".to_string())
436 } else {
437 Ok(format!("Created: {}", data))
438 }
439 }
440 _ => Err("Unsupported".to_string()),
441 });
442
443 assert_eq!(response.results.len(), 2);
445 assert_eq!(response.succeeded, 1);
446 assert_eq!(response.failed, 1);
447 }
448
449 #[test]
450 fn test_batch_processor_validate_size() {
451 let request: BatchRequest<String> = BatchRequest::new(vec![
452 BatchOperation::Create {
453 data: "item1".to_string(),
454 },
455 BatchOperation::Create {
456 data: "item2".to_string(),
457 },
458 ]);
459
460 assert!(BatchProcessor::validate_size(&request, 5).is_ok());
461 assert!(BatchProcessor::validate_size(&request, 1).is_err());
462 }
463
464 #[test]
465 fn test_batch_statistics() {
466 let mut stats = BatchStatistics::new();
467 stats.increment("create");
468 stats.increment("create");
469 stats.increment("update");
470 stats.set_processing_time(1000);
471
472 assert_eq!(stats.by_type.get("create"), Some(&2));
473 assert_eq!(stats.by_type.get("update"), Some(&1));
474 assert_eq!(stats.processing_time_ms, 1000);
475 }
476
477 #[test]
478 fn test_batch_operation_serialization() {
479 let op = BatchOperation::Create {
480 data: "test".to_string(),
481 };
482 let json = serde_json::to_string(&op).unwrap();
483 assert!(json.contains("\"operation\":\"create\""));
484 assert!(json.contains("\"data\":\"test\""));
485 }
486
487 #[test]
488 fn test_batch_request_is_empty() {
489 let empty_request: BatchRequest<String> = BatchRequest::new(vec![]);
490 assert!(empty_request.is_empty());
491
492 let non_empty = BatchRequest::new(vec![BatchOperation::Create {
493 data: "item".to_string(),
494 }]);
495 assert!(!non_empty.is_empty());
496 }
497}