1use crate::models::RepositoryError;
7use redis::RedisError;
8use serde::{Deserialize, Serialize};
9use tracing::{error, warn};
10
11pub trait RedisRepository {
13 fn serialize_entity<T, F>(
14 &self,
15 entity: &T,
16 id_extractor: F,
17 entity_type: &str,
18 ) -> Result<String, RepositoryError>
19 where
20 T: Serialize,
21 F: Fn(&T) -> &str,
22 {
23 serde_json::to_string(entity).map_err(|e| {
24 let id = id_extractor(entity);
25 error!(entity_type = %entity_type, id = %id, error = %e, "serialization failed");
26 RepositoryError::InvalidData(format!("Failed to serialize {entity_type} {id}: {e}"))
27 })
28 }
29
30 fn deserialize_entity<T>(
33 &self,
34 json: &str,
35 entity_id: &str,
36 entity_type: &str,
37 ) -> Result<T, RepositoryError>
38 where
39 T: for<'de> Deserialize<'de>,
40 {
41 serde_json::from_str(json).map_err(|e| {
42 error!(entity_type = %entity_type, entity_id = %entity_id, error = %e, "deserialization failed");
43 RepositoryError::InvalidData(format!(
44 "Failed to deserialize {} {}: {} (JSON length: {})",
45 entity_type,
46 entity_id,
47 e,
48 json.len()
49 ))
50 })
51 }
52
53 fn map_redis_error(&self, error: RedisError, context: &str) -> RepositoryError {
55 warn!(context = %context, error = %error, "redis operation failed");
56
57 match error.kind() {
58 redis::ErrorKind::TypeError => RepositoryError::InvalidData(format!(
59 "Redis data type error in operation '{context}': {error}"
60 )),
61 redis::ErrorKind::AuthenticationFailed => {
62 RepositoryError::InvalidData("Redis authentication failed".to_string())
63 }
64 redis::ErrorKind::NoScriptError => RepositoryError::InvalidData(format!(
65 "Redis script error in operation '{context}': {error}"
66 )),
67 redis::ErrorKind::ReadOnly => RepositoryError::InvalidData(format!(
68 "Redis is read-only in operation '{context}': {error}"
69 )),
70 redis::ErrorKind::ExecAbortError => RepositoryError::InvalidData(format!(
71 "Redis transaction aborted in operation '{context}': {error}"
72 )),
73 redis::ErrorKind::BusyLoadingError => RepositoryError::InvalidData(format!(
74 "Redis is busy in operation '{context}': {error}"
75 )),
76 redis::ErrorKind::ExtensionError => RepositoryError::InvalidData(format!(
77 "Redis extension error in operation '{context}': {error}"
78 )),
79 _ => RepositoryError::Other(format!("Redis operation '{context}' failed: {error}")),
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use serde::{Deserialize, Serialize};
89
90 #[derive(Debug, Serialize, Deserialize, PartialEq)]
92 struct TestEntity {
93 id: String,
94 name: String,
95 value: i32,
96 }
97
98 #[derive(Debug, Serialize, Deserialize, PartialEq)]
99 struct SimpleEntity {
100 id: String,
101 }
102
103 struct TestRedisRepository;
105
106 impl RedisRepository for TestRedisRepository {}
107
108 impl TestRedisRepository {
109 fn new() -> Self {
110 TestRedisRepository
111 }
112 }
113
114 #[test]
115 fn test_serialize_entity_success() {
116 let repo = TestRedisRepository::new();
117 let entity = TestEntity {
118 id: "test-id".to_string(),
119 name: "test-name".to_string(),
120 value: 42,
121 };
122
123 let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
124
125 assert!(result.is_ok());
126 let json = result.unwrap();
127 assert!(json.contains("test-id"));
128 assert!(json.contains("test-name"));
129 assert!(json.contains("42"));
130 }
131
132 #[test]
133 fn test_serialize_entity_with_different_id_extractor() {
134 let repo = TestRedisRepository::new();
135 let entity = TestEntity {
136 id: "test-id".to_string(),
137 name: "test-name".to_string(),
138 value: 42,
139 };
140
141 let result = repo.serialize_entity(&entity, |e| &e.name, "TestEntity");
143
144 assert!(result.is_ok());
145 let json = result.unwrap();
146
147 assert!(json.contains("test-id"));
149 assert!(json.contains("test-name"));
150 assert!(json.contains("42"));
151 }
152
153 #[test]
154 fn test_serialize_entity_simple_struct() {
155 let repo = TestRedisRepository::new();
156 let entity = SimpleEntity {
157 id: "simple-id".to_string(),
158 };
159
160 let result = repo.serialize_entity(&entity, |e| &e.id, "SimpleEntity");
161
162 assert!(result.is_ok());
163 let json = result.unwrap();
164 assert!(json.contains("simple-id"));
165 }
166
167 #[test]
168 fn test_deserialize_entity_success() {
169 let repo = TestRedisRepository::new();
170 let json = r#"{"id":"test-id","name":"test-name","value":42}"#;
171
172 let result: Result<TestEntity, RepositoryError> =
173 repo.deserialize_entity(json, "test-id", "TestEntity");
174
175 assert!(result.is_ok());
176 let entity = result.unwrap();
177 assert_eq!(entity.id, "test-id");
178 assert_eq!(entity.name, "test-name");
179 assert_eq!(entity.value, 42);
180 }
181
182 #[test]
183 fn test_deserialize_entity_invalid_json() {
184 let repo = TestRedisRepository::new();
185 let invalid_json = r#"{"id":"test-id","name":"test-name","value":}"#; let result: Result<TestEntity, RepositoryError> =
188 repo.deserialize_entity(invalid_json, "test-id", "TestEntity");
189
190 assert!(result.is_err());
191 match result.unwrap_err() {
192 RepositoryError::InvalidData(msg) => {
193 assert!(msg.contains("Failed to deserialize TestEntity test-id"));
194 assert!(msg.contains("JSON length:"));
195 }
196 _ => panic!("Expected InvalidData error"),
197 }
198 }
199
200 #[test]
201 fn test_deserialize_entity_invalid_structure() {
202 let repo = TestRedisRepository::new();
203 let json = r#"{"wrongfield":"test-id"}"#;
204
205 let result: Result<TestEntity, RepositoryError> =
206 repo.deserialize_entity(json, "test-id", "TestEntity");
207
208 assert!(result.is_err());
209 match result.unwrap_err() {
210 RepositoryError::InvalidData(msg) => {
211 assert!(msg.contains("Failed to deserialize TestEntity test-id"));
212 }
213 _ => panic!("Expected InvalidData error"),
214 }
215 }
216
217 #[test]
218 fn test_map_redis_error_type_error() {
219 let repo = TestRedisRepository::new();
220 let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
221
222 let result = repo.map_redis_error(redis_error, "test_operation");
223
224 match result {
225 RepositoryError::InvalidData(msg) => {
226 assert!(msg.contains("Redis data type error"));
227 assert!(msg.contains("test_operation"));
228 }
229 _ => panic!("Expected InvalidData error"),
230 }
231 }
232
233 #[test]
234 fn test_map_redis_error_authentication_failed() {
235 let repo = TestRedisRepository::new();
236 let redis_error = RedisError::from((redis::ErrorKind::AuthenticationFailed, "Auth failed"));
237
238 let result = repo.map_redis_error(redis_error, "auth_operation");
239
240 match result {
241 RepositoryError::InvalidData(msg) => {
242 assert!(msg.contains("Redis authentication failed"));
243 }
244 _ => panic!("Expected InvalidData error"),
245 }
246 }
247
248 #[test]
249 fn test_map_redis_error_connection_error() {
250 let repo = TestRedisRepository::new();
251 let redis_error = RedisError::from((redis::ErrorKind::IoError, "Connection failed"));
252
253 let result = repo.map_redis_error(redis_error, "connection_operation");
254
255 match result {
256 RepositoryError::Other(msg) => {
257 assert!(msg.contains("Redis operation"));
258 assert!(msg.contains("connection_operation"));
259 }
260 _ => panic!("Expected Other error"),
261 }
262 }
263
264 #[test]
265 fn test_map_redis_error_no_script_error() {
266 let repo = TestRedisRepository::new();
267 let redis_error = RedisError::from((redis::ErrorKind::NoScriptError, "Script not found"));
268
269 let result = repo.map_redis_error(redis_error, "script_operation");
270
271 match result {
272 RepositoryError::InvalidData(msg) => {
273 assert!(msg.contains("Redis script error"));
274 assert!(msg.contains("script_operation"));
275 }
276 _ => panic!("Expected InvalidData error"),
277 }
278 }
279
280 #[test]
281 fn test_map_redis_error_read_only() {
282 let repo = TestRedisRepository::new();
283 let redis_error = RedisError::from((redis::ErrorKind::ReadOnly, "Read only"));
284
285 let result = repo.map_redis_error(redis_error, "write_operation");
286
287 match result {
288 RepositoryError::InvalidData(msg) => {
289 assert!(msg.contains("Redis is read-only"));
290 assert!(msg.contains("write_operation"));
291 }
292 _ => panic!("Expected InvalidData error"),
293 }
294 }
295
296 #[test]
297 fn test_map_redis_error_exec_abort_error() {
298 let repo = TestRedisRepository::new();
299 let redis_error =
300 RedisError::from((redis::ErrorKind::ExecAbortError, "Transaction aborted"));
301
302 let result = repo.map_redis_error(redis_error, "transaction_operation");
303
304 match result {
305 RepositoryError::InvalidData(msg) => {
306 assert!(msg.contains("Redis transaction aborted"));
307 assert!(msg.contains("transaction_operation"));
308 }
309 _ => panic!("Expected InvalidData error"),
310 }
311 }
312
313 #[test]
314 fn test_map_redis_error_busy_error() {
315 let repo = TestRedisRepository::new();
316 let redis_error = RedisError::from((redis::ErrorKind::BusyLoadingError, "Server busy"));
317
318 let result = repo.map_redis_error(redis_error, "busy_operation");
319
320 match result {
321 RepositoryError::InvalidData(msg) => {
322 assert!(msg.contains("Redis is busy"));
323 assert!(msg.contains("busy_operation"));
324 }
325 _ => panic!("Expected InvalidData error"),
326 }
327 }
328
329 #[test]
330 fn test_map_redis_error_extension_error() {
331 let repo = TestRedisRepository::new();
332 let redis_error = RedisError::from((redis::ErrorKind::ExtensionError, "Extension error"));
333
334 let result = repo.map_redis_error(redis_error, "extension_operation");
335
336 match result {
337 RepositoryError::InvalidData(msg) => {
338 assert!(msg.contains("Redis extension error"));
339 assert!(msg.contains("extension_operation"));
340 }
341 _ => panic!("Expected InvalidData error"),
342 }
343 }
344
345 #[test]
346 fn test_map_redis_error_context_propagation() {
347 let repo = TestRedisRepository::new();
348 let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
349 let context = "user_repository_get_operation";
350
351 let result = repo.map_redis_error(redis_error, context);
352
353 match result {
354 RepositoryError::InvalidData(msg) => {
355 assert!(msg.contains("Redis data type error"));
356 }
358 _ => panic!("Expected InvalidData error"),
359 }
360 }
361
362 #[test]
363 fn test_serialize_deserialize_roundtrip() {
364 let repo = TestRedisRepository::new();
365 let original = TestEntity {
366 id: "roundtrip-id".to_string(),
367 name: "roundtrip-name".to_string(),
368 value: 123,
369 };
370
371 let json = repo
373 .serialize_entity(&original, |e| &e.id, "TestEntity")
374 .unwrap();
375
376 let deserialized: TestEntity = repo
378 .deserialize_entity(&json, "roundtrip-id", "TestEntity")
379 .unwrap();
380
381 assert_eq!(original, deserialized);
383 }
384
385 #[test]
386 fn test_serialize_deserialize_unicode_content() {
387 let repo = TestRedisRepository::new();
388 let original = TestEntity {
389 id: "unicode-id".to_string(),
390 name: "测试名称 🚀".to_string(),
391 value: 456,
392 };
393
394 let json = repo
396 .serialize_entity(&original, |e| &e.id, "TestEntity")
397 .unwrap();
398
399 let deserialized: TestEntity = repo
401 .deserialize_entity(&json, "unicode-id", "TestEntity")
402 .unwrap();
403
404 assert_eq!(original, deserialized);
406 }
407
408 #[test]
409 fn test_serialize_entity_with_complex_data() {
410 let repo = TestRedisRepository::new();
411
412 #[derive(Serialize)]
413 struct ComplexEntity {
414 id: String,
415 nested: NestedData,
416 list: Vec<i32>,
417 }
418
419 #[derive(Serialize)]
420 struct NestedData {
421 field1: String,
422 field2: bool,
423 }
424
425 let complex_entity = ComplexEntity {
426 id: "complex-id".to_string(),
427 nested: NestedData {
428 field1: "nested-value".to_string(),
429 field2: true,
430 },
431 list: vec![1, 2, 3],
432 };
433
434 let result = repo.serialize_entity(&complex_entity, |e| &e.id, "ComplexEntity");
435
436 assert!(result.is_ok());
437 let json = result.unwrap();
438 assert!(json.contains("complex-id"));
439 assert!(json.contains("nested-value"));
440 assert!(json.contains("true"));
441 assert!(json.contains("[1,2,3]"));
442 }
443
444 #[test]
446 fn test_serialize_deserialize_u128_large_values() {
447 use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
448
449 #[derive(Serialize, Deserialize, PartialEq, Debug)]
450 struct TestU128Entity {
451 id: String,
452 #[serde(
453 serialize_with = "serialize_optional_u128",
454 deserialize_with = "deserialize_optional_u128",
455 default
456 )]
457 gas_price: Option<u128>,
458 #[serde(
459 serialize_with = "serialize_optional_u128",
460 deserialize_with = "deserialize_optional_u128",
461 default
462 )]
463 max_fee_per_gas: Option<u128>,
464 }
465
466 let repo = TestRedisRepository::new();
467
468 let original = TestU128Entity {
470 id: "u128-test".to_string(),
471 gas_price: Some(u128::MAX), max_fee_per_gas: Some(999999999999999999999999999999999u128),
473 };
474
475 let json = repo
477 .serialize_entity(&original, |e| &e.id, "TestU128Entity")
478 .unwrap();
479
480 assert!(json.contains("\"340282366920938463463374607431768211455\""));
482 assert!(json.contains("\"999999999999999999999999999999999\""));
483 assert!(!json.contains("3.4028236692093846e+38"));
485
486 let deserialized: TestU128Entity = repo
488 .deserialize_entity(&json, "u128-test", "TestU128Entity")
489 .unwrap();
490
491 assert_eq!(original, deserialized);
493 assert_eq!(deserialized.gas_price, Some(u128::MAX));
494 assert_eq!(
495 deserialized.max_fee_per_gas,
496 Some(999999999999999999999999999999999u128)
497 );
498 }
499
500 #[test]
501 fn test_serialize_deserialize_u128_none_values() {
502 use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
503
504 #[derive(Serialize, Deserialize, PartialEq, Debug)]
505 struct TestU128Entity {
506 id: String,
507 #[serde(
508 serialize_with = "serialize_optional_u128",
509 deserialize_with = "deserialize_optional_u128",
510 default
511 )]
512 gas_price: Option<u128>,
513 }
514
515 let repo = TestRedisRepository::new();
516
517 let original = TestU128Entity {
519 id: "u128-none-test".to_string(),
520 gas_price: None,
521 };
522
523 let json = repo
525 .serialize_entity(&original, |e| &e.id, "TestU128Entity")
526 .unwrap();
527
528 assert!(json.contains("null"));
530
531 let deserialized: TestU128Entity = repo
533 .deserialize_entity(&json, "u128-none-test", "TestU128Entity")
534 .unwrap();
535
536 assert_eq!(original, deserialized);
538 assert_eq!(deserialized.gas_price, None);
539 }
540}