redis_derive/
lib.rs

1/*!
2# redis-derive
3
4This crate implements the [`FromRedisValue`](redis::FromRedisValue) and [`ToRedisArgs`](redis::ToRedisArgs) traits 
5from [`redis-rs`](https://github.com/redis-rs/redis-rs) for any struct or enum.
6
7This allows seamless type conversion between Rust structs and Redis hash sets, which is more beneficial than JSON encoding the struct and storing the result in a Redis key because when saving as a Redis hash set, sorting algorithms can be performed without having to move data out of the database.
8
9There is also the benefit of being able to retrieve just one value of the struct in the database.
10
11Initial development was done by @Michaelvanstraten 🙏🏽.
12
13## Features
14
15- **RESP3 Support**: Native support for Redis 7+ protocol features including VerbatimString
16- **Hash Field Expiration**: Per-field TTL support using Redis 7.4+ HEXPIRE commands  
17- **Client-Side Caching**: Automatic cache management with Redis 6+ client caching
18- **Cluster Awareness**: Hash tag generation for Redis Cluster deployments
19- **Flexible Naming**: Support for various case conversion rules (snake_case, kebab-case, etc.)
20- **Comprehensive Error Handling**: Clear error messages for debugging
21- **Performance Optimized**: Efficient serialization with minimal allocations
22
23## Usage and Examples
24
25Add this to your `Cargo.toml`:
26
27```toml
28[dependencies]
29redis-derive = "0.2.0"
30redis = "0.32"
31```
32
33Import the procedural macros:
34
35```rust
36use redis_derive::{FromRedisValue, ToRedisArgs};
37```
38
39### Basic Struct Example
40
41```rust
42use redis::Commands;
43use redis_derive::{FromRedisValue, ToRedisArgs};
44
45#[derive(ToRedisArgs, FromRedisValue, Debug)]
46struct User {
47    id: u64,
48    username: String,
49    email: Option<String>,
50    active: bool,
51}
52
53fn main() -> redis::RedisResult<()> {
54    let client = redis::Client::open("redis://127.0.0.1/")?;
55    let mut con = client.get_connection()?;
56
57    let user = User {
58        id: 12345,
59        username: "john_doe".to_string(),
60        email: Some("john@example.com".to_string()),
61        active: true,
62    };
63
64    // Store individual fields
65    con.hset("user:12345", "id", user.id)?;
66    con.hset("user:12345", "username", &user.username)?;
67    con.hset("user:12345", "email", &user.email)?;
68    con.hset("user:12345", "active", user.active)?;
69
70    // Retrieve the complete struct
71    let retrieved_user: User = con.hgetall("user:12345")?;
72    println!("Retrieved: {:?}", retrieved_user);
73
74    Ok(())
75}
76```
77
78### Enum with Case Conversion
79
80```rust,ignore
81use redis_derive::{FromRedisValue, ToRedisArgs};
82
83#[derive(ToRedisArgs, FromRedisValue, Debug, PartialEq)]
84#[redis(rename_all = "snake_case")]
85enum UserRole {
86    Administrator,      // stored as "administrator" 
87    PowerUser,          // stored as "power_user"
88    RegularUser,        // stored as "regular_user"
89    GuestUser,          // stored as "guest_user"
90}
91
92// Works seamlessly with Redis
93let role = UserRole::PowerUser;
94con.set("user:role", &role)?;
95let retrieved: UserRole = con.get("user:role")?;
96assert_eq!(role, retrieved);
97```
98
99## Naming Conventions and Attributes
100
101### Case Conversion Rules
102
103The `rename_all` attribute supports multiple case conversion rules:
104
105```rust
106use redis_derive::{FromRedisValue, ToRedisArgs};
107
108#[derive(ToRedisArgs, FromRedisValue)]
109#[redis(rename_all = "snake_case")]
110enum Status {
111    InProgress,        // → "in_progress"
112    WaitingForReview,  // → "waiting_for_review"  
113    Completed,         // → "completed"
114}
115
116#[derive(ToRedisArgs, FromRedisValue)]
117#[redis(rename_all = "kebab-case")]
118enum Priority {
119    HighPriority,      // → "high-priority"
120    MediumPriority,    // → "medium-priority"
121    LowPriority,       // → "low-priority"
122}
123```
124
125Supported case conversion rules:
126- `"lowercase"`: `MyField` → `myfield`
127- `"UPPERCASE"`: `MyField` → `MYFIELD`  
128- `"PascalCase"`: `my_field` → `MyField`
129- `"camelCase"`: `my_field` → `myField`
130- `"snake_case"`: `MyField` → `my_field`
131- `"kebab-case"`: `MyField` → `my-field`
132
133### Important Naming Behavior
134
135**Key insight**: The case conversion applies to **both** serialization and deserialization:
136
137```rust,ignore
138// With rename_all = "snake_case"
139let role = UserRole::PowerUser;
140
141// Serialization: PowerUser → "power_user" 
142con.set("key", &role)?;
143
144// Deserialization: "power_user" → PowerUser
145let retrieved: UserRole = con.get("key")?;
146
147// Error messages also use converted names:
148// "Unknown variant 'admin' for UserRole. Valid variants: [administrator, power_user, regular_user, guest_user]"
149```
150
151### Redis Protocol Support
152
153This crate handles multiple Redis value types automatically:
154
155- **BulkString**: Most common for stored hash fields and string values
156- **SimpleString**: Direct Redis command responses  
157- **VerbatimString**: Redis 6+ RESP3 protocol feature (automatically supported)
158- **Proper error handling**: Clear messages for nil values and type mismatches
159
160### Advanced Features
161
162#### Hash Field Expiration (Redis 7.4+)
163```rust
164use redis_derive::{FromRedisValue, ToRedisArgs};
165
166#[derive(ToRedisArgs, FromRedisValue)]
167struct SessionData {
168    user_id: u64,
169    #[redis(expire = "1800")] // 30 minutes
170    access_token: String,
171    #[redis(expire = "7200")] // 2 hours  
172    refresh_token: String,
173}
174```
175
176#### Cluster-Aware Keys
177```rust
178use redis_derive::{FromRedisValue, ToRedisArgs};
179
180#[derive(ToRedisArgs, FromRedisValue)]
181#[redis(cluster_key = "user_id")]
182struct UserProfile {
183    user_id: u64,
184    profile_data: String,
185}
186```
187
188#### Client-Side Caching
189```rust
190use redis_derive::{FromRedisValue, ToRedisArgs};
191
192#[derive(ToRedisArgs, FromRedisValue)]
193#[redis(cache = true, ttl = "600")]
194struct CachedData {
195    id: u64,
196    data: String,
197}
198```
199
200## Development and Testing
201
202The crate includes comprehensive examples in the `examples/` directory:
203
204```bash
205# Start Redis with Docker
206cd examples && docker-compose up -d
207
208# Run basic example
209cargo run --example main
210
211# Test all enum deserialization branches  
212cargo run --example enum_branches
213
214# Debug attribute parsing behavior
215cargo run --example debug_attributes
216```
217
218## Limitations
219
220- Only unit enums (variants without fields) are currently supported
221- Requires redis-rs 0.32.4 or later for full compatibility
222
223## Compatibility
224
225- **Redis**: Compatible with Redis 6+ (RESP2) and Redis 7+ (RESP3)
226- **Rust**: MSRV 1.70+ (follows redis-rs requirements)
227- **redis-rs**: 0.32.4+ (uses `num_of_args()` instead of deprecated `num_args()`)
228
229License: MIT OR Apache-2.0
230*/
231
232use proc_macro::TokenStream;
233use syn::{parse_macro_input, Data::*, DeriveInput};
234
235mod data_enum;
236mod data_struct;
237mod util;
238
239#[proc_macro_derive(ToRedisArgs, attributes(redis))]
240/**
241This macro implements the [`ToRedisArgs`](redis::ToRedisArgs) trait for a given struct or enum.
242It generates efficient serialization code that converts Rust types to Redis arguments.
243
244# Attributes
245
246- `redis(rename_all = "...")`: Transform field/variant names using case conversion rules
247- `redis(expire = "seconds")`: Set TTL for hash fields (requires Redis 7.4+)
248- `redis(expire_at = "field_name")`: Expire field at timestamp specified by another field
249- `redis(cluster_key = "field_name")`: Use specified field for Redis Cluster hash tag generation
250- `redis(cache = true)`: Enable client-side caching support
251- `redis(ttl = "seconds")`: Default TTL for cached objects
252
253## Case Conversion Rules
254
255- `"lowercase"`: `MyField` → `myfield`
256- `"UPPERCASE"`: `MyField` → `MYFIELD`
257- `"PascalCase"`: `my_field` → `MyField`
258- `"camelCase"`: `my_field` → `myField`
259- `"snake_case"`: `MyField` → `my_field`
260- `"kebab-case"`: `MyField` → `my-field`
261*/
262pub fn to_redis_args(tokenstream: TokenStream) -> TokenStream {
263    let ast = parse_macro_input!(tokenstream as DeriveInput);
264    let type_ident = ast.ident;
265    let attr_map = util::parse_attributes(&ast.attrs);
266
267    match ast.data {
268        Struct(data_struct) => data_struct::derive_to_redis_struct(data_struct, type_ident, attr_map),
269        Enum(data_enum) => data_enum::derive_to_redis_enum(data_enum, type_ident, attr_map),
270        Union(_) => panic!("ToRedisArgs cannot be derived for union types"),
271    }
272}
273
274#[proc_macro_derive(FromRedisValue, attributes(redis))]
275/**
276This macro implements the [`FromRedisValue`](redis::FromRedisValue) trait for a given struct or enum.
277It generates efficient deserialization code with full RESP3 support and enhanced error handling.
278
279# Attributes
280
281Same attributes as `ToRedisArgs`. The deserialization respects the same naming conventions
282and provides helpful error messages for debugging.
283
284# Error Handling
285
286The generated code provides detailed error messages including:
287- Expected vs actual Redis value types
288- Missing field information
289- Type conversion failures with context
290- RESP2/RESP3 compatibility notes
291*/
292pub fn from_redis_value(tokenstream: TokenStream) -> TokenStream {
293    let ast = parse_macro_input!(tokenstream as DeriveInput);
294    let type_ident = ast.ident;
295    let attr_map = util::parse_attributes(&ast.attrs);
296
297    match ast.data {
298        Struct(data_struct) => data_struct::derive_from_redis_struct(data_struct, type_ident, attr_map),
299        Enum(data_enum) => data_enum::derive_from_redis_enum(data_enum, type_ident, attr_map),
300        Union(_) => panic!("FromRedisValue cannot be derived for union types"),
301    }
302}