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}