Skip to main content

reinhardt_views/viewsets/
batch_operations.rs

1//! Batch operations support for ViewSets
2//!
3//! Provides functionality for performing multiple operations in a single request:
4//! - Batch create (create multiple resources at once)
5//! - Batch update (update multiple resources at once)
6//! - Batch delete (delete multiple resources at once)
7//! - Batch partial update (partial update of multiple resources)
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Batch operation request
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "operation")]
15pub enum BatchOperation<T> {
16	/// Create operation
17	#[serde(rename = "create")]
18	Create {
19		/// Data for the new resource.
20		data: T,
21	},
22	/// Update operation (full update)
23	#[serde(rename = "update")]
24	Update {
25		/// Identifier of the resource to update.
26		id: String,
27		/// Complete replacement data.
28		data: T,
29	},
30	/// Partial update operation
31	#[serde(rename = "partial_update")]
32	PartialUpdate {
33		/// Identifier of the resource to partially update.
34		id: String,
35		/// Partial data to merge into the resource.
36		data: T,
37	},
38	/// Delete operation
39	#[serde(rename = "delete")]
40	Delete {
41		/// Identifier of the resource to delete.
42		id: String,
43	},
44}
45
46/// Batch operation result
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct BatchOperationResult<T> {
49	/// Operation index in the request
50	pub index: usize,
51	/// Whether the operation succeeded
52	pub success: bool,
53	/// Result data (for create/update operations)
54	pub data: Option<T>,
55	/// Error message (if failed)
56	pub error: Option<String>,
57}
58
59impl<T> BatchOperationResult<T> {
60	/// Create a success result
61	///
62	/// # Examples
63	///
64	/// ```
65	/// use reinhardt_views::viewsets::BatchOperationResult;
66	///
67	/// let result = BatchOperationResult::success(0, Some("created".to_string()));
68	/// assert!(result.success);
69	/// assert_eq!(result.index, 0);
70	/// ```
71	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	/// Create a failure result
81	///
82	/// # Examples
83	///
84	/// ```
85	/// use reinhardt_views::viewsets::BatchOperationResult;
86	///
87	/// let result: BatchOperationResult<String> = BatchOperationResult::failure(0, "Not found");
88	/// assert!(!result.success);
89	/// assert_eq!(result.error, Some("Not found".to_string()));
90	/// ```
91	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/// Batch request wrapper
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BatchRequest<T> {
104	/// List of operations to perform
105	pub operations: Vec<BatchOperation<T>>,
106	/// Whether to stop on first error
107	#[serde(default)]
108	pub atomic: bool,
109}
110
111impl<T> BatchRequest<T> {
112	/// Create a new batch request
113	///
114	/// # Examples
115	///
116	/// ```
117	/// use reinhardt_views::viewsets::{BatchRequest, BatchOperation};
118	///
119	/// let request: BatchRequest<String> = BatchRequest::new(vec![
120	///     BatchOperation::Create { data: "item1".to_string() },
121	///     BatchOperation::Create { data: "item2".to_string() },
122	/// ]);
123	/// assert_eq!(request.operations.len(), 2);
124	/// ```
125	pub fn new(operations: Vec<BatchOperation<T>>) -> Self {
126		Self {
127			operations,
128			atomic: false,
129		}
130	}
131
132	/// Set atomic mode
133	pub fn atomic(mut self) -> Self {
134		self.atomic = true;
135		self
136	}
137
138	/// Get the number of operations
139	pub fn len(&self) -> usize {
140		self.operations.len()
141	}
142
143	/// Check if batch is empty
144	pub fn is_empty(&self) -> bool {
145		self.operations.is_empty()
146	}
147}
148
149/// Batch response wrapper
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct BatchResponse<T> {
152	/// Results of operations
153	pub results: Vec<BatchOperationResult<T>>,
154	/// Total number of operations
155	pub total: usize,
156	/// Number of successful operations
157	pub succeeded: usize,
158	/// Number of failed operations
159	pub failed: usize,
160}
161
162impl<T> BatchResponse<T> {
163	/// Create a new batch response
164	///
165	/// # Examples
166	///
167	/// ```
168	/// use reinhardt_views::viewsets::{BatchResponse, BatchOperationResult};
169	///
170	/// let results = vec![
171	///     BatchOperationResult::success(0, Some("created".to_string())),
172	///     BatchOperationResult::failure(1, "Error"),
173	/// ];
174	/// let response = BatchResponse::new(results);
175	/// assert_eq!(response.total, 2);
176	/// assert_eq!(response.succeeded, 1);
177	/// assert_eq!(response.failed, 1);
178	/// ```
179	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	/// Check if all operations succeeded
193	pub fn all_succeeded(&self) -> bool {
194		self.failed == 0
195	}
196
197	/// Check if any operation failed
198	pub fn any_failed(&self) -> bool {
199		self.failed > 0
200	}
201}
202
203/// Batch operation processor
204pub struct BatchProcessor;
205
206impl BatchProcessor {
207	/// Process a batch request
208	///
209	/// # Examples
210	///
211	/// ```
212	/// use reinhardt_views::viewsets::{BatchProcessor, BatchRequest, BatchOperation};
213	///
214	/// let request: BatchRequest<String> = BatchRequest::new(vec![
215	///     BatchOperation::Create { data: "item1".to_string() },
216	/// ]);
217	///
218	/// let response = BatchProcessor::process(request, |op, index| {
219	///     match op {
220	///         BatchOperation::Create { data } => {
221	///             Ok(format!("Created: {}", data))
222	///         }
223	///         _ => Err("Unsupported operation".to_string()),
224	///     }
225	/// });
226	///
227	/// assert!(response.all_succeeded());
228	/// ```
229	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					// Stop on first error in atomic mode
244					if request.atomic {
245						break;
246					}
247				}
248			}
249		}
250
251		BatchResponse::new(results)
252	}
253
254	/// Validate batch request size
255	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/// Batch operation statistics
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct BatchStatistics {
273	/// Number of operations by type
274	pub by_type: HashMap<String, usize>,
275	/// Total processing time (ms)
276	pub processing_time_ms: u64,
277}
278
279impl BatchStatistics {
280	/// Create a new batch statistics
281	pub fn new() -> Self {
282		Self {
283			by_type: HashMap::new(),
284			processing_time_ms: 0,
285		}
286	}
287
288	/// Increment count for an operation type
289	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	/// Set processing time
294	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		// Only 2 operations should be processed (1 success + 1 failure)
444		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}