Skip to content

Commit 4f116f8

Browse files
committed
feat: add optional Tower service integration to API clients
Add tower-integration feature to redis-cloud and redis-enterprise crates, enabling Tower middleware composition for resilience patterns. Changes: - Add tower as optional dependency with feature flag - Implement tower::Service for CloudClient and EnterpriseClient - Add tower_support module with ApiRequest/ApiResponse types - Add comprehensive documentation and examples - Update READMEs with Tower integration usage This enables composition with tower-resilience middleware like circuit breakers, retry, and rate limiting without forcing dependencies on library consumers. Closes #445
1 parent f6990d6 commit 4f116f8

File tree

7 files changed

+515
-0
lines changed

7 files changed

+515
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/redis-cloud/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ base64 = { workspace = true }
2828
chrono = { workspace = true }
2929
url = { workspace = true }
3030
typed-builder = "0.20"
31+
tower = { version = "0.5", optional = true }
32+
33+
[features]
34+
tower-integration = ["tower"]
3135

3236
[dev-dependencies]
3337
wiremock = { workspace = true }

crates/redis-cloud/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A comprehensive Rust client library for the Redis Cloud REST API.
88
- Async/await support with tokio
99
- Strong typing for API requests and responses
1010
- Comprehensive error handling
11+
- Optional Tower service integration for middleware composition
1112
- Support for all Redis Cloud features including:
1213
- Subscriptions and databases
1314
- User and ACL management
@@ -21,6 +22,9 @@ A comprehensive Rust client library for the Redis Cloud REST API.
2122
```toml
2223
[dependencies]
2324
redis-cloud = "0.1.0"
25+
26+
# Optional: Enable Tower service integration
27+
redis-cloud = { version = "0.1.0", features = ["tower-integration"] }
2428
```
2529

2630
## Quick Start
@@ -69,6 +73,37 @@ export REDIS_CLOUD_API_SECRET="your-api-secret"
6973
cargo run --example basic
7074
```
7175

76+
## Tower Integration
77+
78+
Enable the `tower-integration` feature to use the client with Tower middleware:
79+
80+
```rust
81+
use redis_cloud::CloudClient;
82+
use redis_cloud::tower_support::ApiRequest;
83+
use tower::ServiceExt;
84+
85+
#[tokio::main]
86+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
87+
let client = CloudClient::builder()
88+
.api_key("your-api-key")
89+
.api_secret("your-api-secret")
90+
.build()?;
91+
92+
// Convert to a Tower service
93+
let mut service = client.into_service();
94+
95+
// Use the service
96+
let response = service
97+
.oneshot(ApiRequest::get("/subscriptions"))
98+
.await?;
99+
100+
println!("Response: {:?}", response.body);
101+
Ok(())
102+
}
103+
```
104+
105+
This enables composition with Tower middleware like circuit breakers, retry, rate limiting, and more.
106+
72107
## API Coverage
73108

74109
This library provides comprehensive coverage of the Redis Cloud REST API, including:

crates/redis-cloud/src/client.rs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,216 @@ impl CloudClient {
364364
}
365365
}
366366
}
367+
368+
/// Tower Service integration for CloudClient
369+
///
370+
/// This module provides Tower Service implementations for CloudClient, enabling
371+
/// middleware composition with patterns like circuit breakers, retry, and rate limiting.
372+
///
373+
/// # Feature Flag
374+
///
375+
/// This module is only available when the `tower-integration` feature is enabled.
376+
///
377+
/// # Examples
378+
///
379+
/// ```rust,no_run
380+
/// use redis_cloud::CloudClient;
381+
/// # #[cfg(feature = "tower-integration")]
382+
/// use redis_cloud::tower_support::ApiRequest;
383+
/// # #[cfg(feature = "tower-integration")]
384+
/// use tower::ServiceExt;
385+
///
386+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
387+
/// # #[cfg(feature = "tower-integration")]
388+
/// # {
389+
/// let client = CloudClient::builder()
390+
/// .api_key("your-key")
391+
/// .api_secret("your-secret")
392+
/// .build()?;
393+
///
394+
/// // Convert to a Tower service
395+
/// let mut service = client.into_service();
396+
///
397+
/// // Use the service
398+
/// let response = service.oneshot(ApiRequest::get("/subscriptions")).await?;
399+
/// println!("Status: {}", response.status);
400+
/// # }
401+
/// # Ok(())
402+
/// # }
403+
/// ```
404+
#[cfg(feature = "tower-integration")]
405+
pub mod tower_support {
406+
use super::*;
407+
use std::future::Future;
408+
use std::pin::Pin;
409+
use std::task::{Context, Poll};
410+
use tower::Service;
411+
412+
/// HTTP method for API requests
413+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414+
pub enum Method {
415+
/// GET request
416+
Get,
417+
/// POST request
418+
Post,
419+
/// PUT request
420+
Put,
421+
/// PATCH request
422+
Patch,
423+
/// DELETE request
424+
Delete,
425+
}
426+
427+
/// Tower-compatible request type for Redis Cloud API
428+
///
429+
/// This wraps the essential components of an API request in a format
430+
/// suitable for Tower middleware composition.
431+
#[derive(Debug, Clone)]
432+
pub struct ApiRequest {
433+
/// HTTP method
434+
pub method: Method,
435+
/// API endpoint path (e.g., "/subscriptions")
436+
pub path: String,
437+
/// Optional JSON body for POST/PUT/PATCH requests
438+
pub body: Option<serde_json::Value>,
439+
}
440+
441+
impl ApiRequest {
442+
/// Create a GET request
443+
pub fn get(path: impl Into<String>) -> Self {
444+
Self {
445+
method: Method::Get,
446+
path: path.into(),
447+
body: None,
448+
}
449+
}
450+
451+
/// Create a POST request with a JSON body
452+
pub fn post(path: impl Into<String>, body: serde_json::Value) -> Self {
453+
Self {
454+
method: Method::Post,
455+
path: path.into(),
456+
body: Some(body),
457+
}
458+
}
459+
460+
/// Create a PUT request with a JSON body
461+
pub fn put(path: impl Into<String>, body: serde_json::Value) -> Self {
462+
Self {
463+
method: Method::Put,
464+
path: path.into(),
465+
body: Some(body),
466+
}
467+
}
468+
469+
/// Create a PATCH request with a JSON body
470+
pub fn patch(path: impl Into<String>, body: serde_json::Value) -> Self {
471+
Self {
472+
method: Method::Patch,
473+
path: path.into(),
474+
body: Some(body),
475+
}
476+
}
477+
478+
/// Create a DELETE request
479+
pub fn delete(path: impl Into<String>) -> Self {
480+
Self {
481+
method: Method::Delete,
482+
path: path.into(),
483+
body: None,
484+
}
485+
}
486+
}
487+
488+
/// Tower-compatible response type
489+
///
490+
/// Contains the HTTP status code and response body as JSON.
491+
#[derive(Debug, Clone)]
492+
pub struct ApiResponse {
493+
/// HTTP status code
494+
pub status: u16,
495+
/// Response body as JSON
496+
pub body: serde_json::Value,
497+
}
498+
499+
impl CloudClient {
500+
/// Convert this client into a Tower service
501+
///
502+
/// This consumes the client and returns it wrapped in a Tower service
503+
/// implementation, enabling middleware composition.
504+
///
505+
/// # Examples
506+
///
507+
/// ```rust,no_run
508+
/// use redis_cloud::CloudClient;
509+
/// # #[cfg(feature = "tower-integration")]
510+
/// use tower::ServiceExt;
511+
/// # #[cfg(feature = "tower-integration")]
512+
/// use redis_cloud::tower_support::ApiRequest;
513+
///
514+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
515+
/// # #[cfg(feature = "tower-integration")]
516+
/// # {
517+
/// let client = CloudClient::builder()
518+
/// .api_key("key")
519+
/// .api_secret("secret")
520+
/// .build()?;
521+
///
522+
/// let mut service = client.into_service();
523+
/// let response = service.oneshot(ApiRequest::get("/subscriptions")).await?;
524+
/// # }
525+
/// # Ok(())
526+
/// # }
527+
/// ```
528+
pub fn into_service(self) -> Self {
529+
self
530+
}
531+
}
532+
533+
impl Service<ApiRequest> for CloudClient {
534+
type Response = ApiResponse;
535+
type Error = RestError;
536+
type Future = Pin<Box<dyn Future<Output = Result<Self::Response>> + Send>>;
537+
538+
fn poll_ready(
539+
&mut self,
540+
_cx: &mut Context<'_>,
541+
) -> Poll<std::result::Result<(), Self::Error>> {
542+
// CloudClient is always ready since it uses an internal connection pool
543+
Poll::Ready(Ok(()))
544+
}
545+
546+
fn call(&mut self, req: ApiRequest) -> Self::Future {
547+
let client = self.clone();
548+
Box::pin(async move {
549+
let response: serde_json::Value = match req.method {
550+
Method::Get => client.get_raw(&req.path).await?,
551+
Method::Post => {
552+
let body = req.body.ok_or_else(|| RestError::BadRequest {
553+
message: "POST request requires a body".to_string(),
554+
})?;
555+
client.post_raw(&req.path, body).await?
556+
}
557+
Method::Put => {
558+
let body = req.body.ok_or_else(|| RestError::BadRequest {
559+
message: "PUT request requires a body".to_string(),
560+
})?;
561+
client.put_raw(&req.path, body).await?
562+
}
563+
Method::Patch => {
564+
let body = req.body.ok_or_else(|| RestError::BadRequest {
565+
message: "PATCH request requires a body".to_string(),
566+
})?;
567+
client.patch_raw(&req.path, body).await?
568+
}
569+
Method::Delete => client.delete_raw(&req.path).await?,
570+
};
571+
572+
Ok(ApiResponse {
573+
status: 200,
574+
body: response,
575+
})
576+
})
577+
}
578+
}
579+
}

crates/redis-enterprise/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ base64 = { workspace = true }
3232
chrono = { workspace = true }
3333
url = { workspace = true }
3434
typed-builder = "0.20"
35+
tower = { version = "0.5", optional = true }
36+
37+
[features]
38+
tower-integration = ["tower"]
3539

3640
[dev-dependencies]
3741
wiremock = { workspace = true }

crates/redis-enterprise/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A comprehensive Rust client library for the Redis Enterprise REST API.
88
- Async/await support with tokio
99
- Strong typing for API requests and responses
1010
- Comprehensive error handling
11+
- Optional Tower service integration for middleware composition
1112
- Support for all Redis Enterprise features including:
1213
- Cluster management and bootstrap
1314
- Database (BDB) operations
@@ -22,6 +23,9 @@ A comprehensive Rust client library for the Redis Enterprise REST API.
2223
```toml
2324
[dependencies]
2425
redis-enterprise = "0.1.0"
26+
27+
# Optional: Enable Tower service integration
28+
redis-enterprise = { version = "0.1.0", features = ["tower-integration"] }
2529
```
2630

2731
## Quick Start
@@ -74,6 +78,39 @@ export REDIS_ENTERPRISE_INSECURE="true" # For self-signed certificates
7478
cargo run --example basic
7579
```
7680

81+
## Tower Integration
82+
83+
Enable the `tower-integration` feature to use the client with Tower middleware:
84+
85+
```rust
86+
use redis_enterprise::EnterpriseClient;
87+
use redis_enterprise::tower_support::ApiRequest;
88+
use tower::ServiceExt;
89+
90+
#[tokio::main]
91+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
92+
let client = EnterpriseClient::builder()
93+
.base_url("https://localhost:9443")
94+
.username("admin")
95+
.password("password")
96+
.insecure(true)
97+
.build()?;
98+
99+
// Convert to a Tower service
100+
let mut service = client.into_service();
101+
102+
// Use the service
103+
let response = service
104+
.oneshot(ApiRequest::get("/v1/cluster"))
105+
.await?;
106+
107+
println!("Response: {:?}", response.body);
108+
Ok(())
109+
}
110+
```
111+
112+
This enables composition with Tower middleware like circuit breakers, retry, rate limiting, and more.
113+
77114
## API Coverage
78115

79116
This library provides 100% coverage of the Redis Enterprise REST API, including:

0 commit comments

Comments
 (0)