openzeppelin_relayer/repositories/
redis_base.rs

1//! Base Redis repository functionality shared across all Redis implementations.
2//!
3//! This module provides common utilities and patterns used by all Redis repository
4//! implementations to reduce code duplication and ensure consistency.
5
6use crate::models::RepositoryError;
7use redis::RedisError;
8use serde::{Deserialize, Serialize};
9use tracing::{error, warn};
10
11/// Base trait for Redis repositories providing common functionality
12pub 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    /// Deserialize entity with detailed error context
31    /// Default implementation that works for any Deserialize type
32    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    /// Convert Redis errors to appropriate RepositoryError types
54    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            // Default to Other for connection errors and other issues
80            _ => 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    // Test structs for serialization/deserialization
91    #[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    // Test implementation of RedisRepository trait
104    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        // Use name as ID extractor
142        let result = repo.serialize_entity(&entity, |e| &e.name, "TestEntity");
143
144        assert!(result.is_ok());
145        let json = result.unwrap();
146
147        // Should still serialize the entire entity
148        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":}"#; // Missing value
186
187        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                // Context should be used in logging but not necessarily in the error message
357            }
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        // Serialize
372        let json = repo
373            .serialize_entity(&original, |e| &e.id, "TestEntity")
374            .unwrap();
375
376        // Deserialize
377        let deserialized: TestEntity = repo
378            .deserialize_entity(&json, "roundtrip-id", "TestEntity")
379            .unwrap();
380
381        // Should be identical
382        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        // Serialize
395        let json = repo
396            .serialize_entity(&original, |e| &e.id, "TestEntity")
397            .unwrap();
398
399        // Deserialize
400        let deserialized: TestEntity = repo
401            .deserialize_entity(&json, "unicode-id", "TestEntity")
402            .unwrap();
403
404        // Should handle unicode correctly
405        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 specifically for u128 serialization/deserialization with large values
445    #[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        // Test with very large u128 values that would overflow JSON numbers
469        let original = TestU128Entity {
470            id: "u128-test".to_string(),
471            gas_price: Some(u128::MAX), // 340282366920938463463374607431768211455
472            max_fee_per_gas: Some(999999999999999999999999999999999u128),
473        };
474
475        // Serialize
476        let json = repo
477            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
478            .unwrap();
479
480        // Verify it contains string representations, not numbers
481        assert!(json.contains("\"340282366920938463463374607431768211455\""));
482        assert!(json.contains("\"999999999999999999999999999999999\""));
483        // Make sure they're not stored as numbers (which would cause overflow)
484        assert!(!json.contains("3.4028236692093846e+38"));
485
486        // Deserialize
487        let deserialized: TestU128Entity = repo
488            .deserialize_entity(&json, "u128-test", "TestU128Entity")
489            .unwrap();
490
491        // Should be identical
492        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        // Test with None values
518        let original = TestU128Entity {
519            id: "u128-none-test".to_string(),
520            gas_price: None,
521        };
522
523        // Serialize
524        let json = repo
525            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
526            .unwrap();
527
528        // Should contain null
529        assert!(json.contains("null"));
530
531        // Deserialize
532        let deserialized: TestU128Entity = repo
533            .deserialize_entity(&json, "u128-none-test", "TestU128Entity")
534            .unwrap();
535
536        // Should be identical
537        assert_eq!(original, deserialized);
538        assert_eq!(deserialized.gas_price, None);
539    }
540}