diff --git a/crates/redis-cloud/src/client.rs b/crates/redis-cloud/src/client.rs index fbb096f9..38a178de 100644 --- a/crates/redis-cloud/src/client.rs +++ b/crates/redis-cloud/src/client.rs @@ -1,61 +1,141 @@ //! Redis Cloud API client core implementation +//! +//! This module contains the core HTTP client for interacting with the Redis Cloud REST API. +//! It provides authentication handling, request/response processing, and error management. +//! +//! The client is designed around a builder pattern for flexible configuration and supports +//! both typed and untyped API interactions. use crate::{CloudError as RestError, Result}; use reqwest::Client; use serde::Serialize; use std::sync::Arc; -/// Redis Cloud API configuration +/// Builder for constructing a CloudClient with custom configuration +/// +/// Provides a fluent interface for configuring API credentials, base URL, timeouts, +/// and other client settings before creating the final CloudClient instance. +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::CloudClient; +/// +/// // Basic configuration +/// let client = CloudClient::builder() +/// .api_key("your-api-key") +/// .api_secret("your-api-secret") +/// .build()?; +/// +/// // Advanced configuration +/// let client = CloudClient::builder() +/// .api_key("your-api-key") +/// .api_secret("your-api-secret") +/// .base_url("https://api.redislabs.com/v1".to_string()) +/// .timeout(std::time::Duration::from_secs(120)) +/// .build()?; +/// # Ok::<(), Box>(()) +/// ``` #[derive(Debug, Clone)] -pub struct CloudConfig { - pub api_key: String, - pub api_secret: String, - pub base_url: String, - pub timeout: std::time::Duration, +pub struct CloudClientBuilder { + api_key: Option, + api_secret: Option, + base_url: String, + timeout: std::time::Duration, } -impl Default for CloudConfig { +impl Default for CloudClientBuilder { fn default() -> Self { - CloudConfig { - api_key: String::new(), - api_secret: String::new(), + Self { + api_key: None, + api_secret: None, base_url: "https://api.redislabs.com/v1".to_string(), timeout: std::time::Duration::from_secs(30), } } } -/// Redis Cloud API client -#[derive(Clone)] -pub struct CloudClient { - pub(crate) config: CloudConfig, - pub(crate) client: Arc, -} +impl CloudClientBuilder { + /// Create a new builder + pub fn new() -> Self { + Self::default() + } + + /// Set the API key + pub fn api_key(mut self, key: impl Into) -> Self { + self.api_key = Some(key.into()); + self + } + + /// Set the API secret + pub fn api_secret(mut self, secret: impl Into) -> Self { + self.api_secret = Some(secret.into()); + self + } + + /// Set the base URL + pub fn base_url(mut self, url: impl Into) -> Self { + self.base_url = url.into(); + self + } + + /// Set the timeout + pub fn timeout(mut self, timeout: std::time::Duration) -> Self { + self.timeout = timeout; + self + } + + /// Build the client + pub fn build(self) -> Result { + let api_key = self + .api_key + .ok_or_else(|| RestError::ConnectionError("API key is required".to_string()))?; + let api_secret = self + .api_secret + .ok_or_else(|| RestError::ConnectionError("API secret is required".to_string()))?; -impl CloudClient { - /// Create a new Cloud API client - pub fn new(config: CloudConfig) -> Result { let client = Client::builder() - .timeout(config.timeout) + .timeout(self.timeout) .build() .map_err(|e| RestError::ConnectionError(e.to_string()))?; Ok(CloudClient { - config, + api_key, + api_secret, + base_url: self.base_url, + timeout: self.timeout, client: Arc::new(client), }) } +} + +/// Redis Cloud API client +#[derive(Clone)] +pub struct CloudClient { + pub(crate) api_key: String, + pub(crate) api_secret: String, + pub(crate) base_url: String, + #[allow(dead_code)] + pub(crate) timeout: std::time::Duration, + pub(crate) client: Arc, +} + +impl CloudClient { + /// Create a new builder for the client + pub fn builder() -> CloudClientBuilder { + CloudClientBuilder::new() + } /// Make a GET request with API key authentication pub async fn get(&self, path: &str) -> Result { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Redis Cloud API uses these headers for authentication let response = self .client .get(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .send() .await?; @@ -68,14 +148,14 @@ impl CloudClient { path: &str, body: &B, ) -> Result { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Same backwards header naming as GET let response = self .client .post(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .json(body) .send() .await?; @@ -89,14 +169,14 @@ impl CloudClient { path: &str, body: &B, ) -> Result { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Same backwards header naming as GET let response = self .client .put(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .json(body) .send() .await?; @@ -106,14 +186,14 @@ impl CloudClient { /// Make a DELETE request pub async fn delete(&self, path: &str) -> Result<()> { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Same backwards header naming as GET let response = self .client .delete(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .send() .await?; @@ -150,14 +230,14 @@ impl CloudClient { path: &str, body: serde_json::Value, ) -> Result { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Use backwards header names for compatibility let response = self .client .patch(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .json(&body) .send() .await?; @@ -167,14 +247,14 @@ impl CloudClient { /// Execute raw DELETE request returning any response body pub async fn delete_raw(&self, path: &str) -> Result { - let url = format!("{}{}", self.config.base_url, path); + let url = format!("{}{}", self.base_url, path); // Use backwards header names for compatibility let response = self .client .delete(&url) - .header("x-api-key", &self.config.api_key) - .header("x-api-secret-key", &self.config.api_secret) + .header("x-api-key", &self.api_key) + .header("x-api-secret-key", &self.api_secret) .send() .await?; diff --git a/crates/redis-cloud/src/handlers/billing.rs b/crates/redis-cloud/src/handlers/billing.rs index 701244a4..a2c9616a 100644 --- a/crates/redis-cloud/src/handlers/billing.rs +++ b/crates/redis-cloud/src/handlers/billing.rs @@ -1,9 +1,46 @@ //! Billing and payment operations handler +//! +//! This module provides comprehensive billing and payment management for Redis Cloud, +//! including invoice management, payment method handling, cost analysis, and usage reporting. +//! +//! # Examples +//! +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudBillingHandler}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .build()?; +//! +//! let billing_handler = CloudBillingHandler::new(client); +//! +//! // Get current billing information +//! let billing_info = billing_handler.get_info().await?; +//! +//! // List all invoices +//! let invoices = billing_handler.list_invoices().await?; +//! +//! // Get usage report for date range +//! let usage = billing_handler.get_usage("2024-01-01", "2024-01-31").await?; +//! +//! // List payment methods +//! let payment_methods = billing_handler.list_payment_methods().await?; +//! # Ok(()) +//! # } +//! ``` use crate::{Result, client::CloudClient}; use serde_json::Value; /// Handler for Cloud billing and payment operations +/// +/// Provides access to billing information, invoice management, payment methods, +/// cost analysis, and usage reporting. Essential for monitoring and managing +/// Redis Cloud costs and payment configuration. pub struct CloudBillingHandler { client: CloudClient, } diff --git a/crates/redis-cloud/src/handlers/database.rs b/crates/redis-cloud/src/handlers/database.rs index 57d091cb..52f18281 100644 --- a/crates/redis-cloud/src/handlers/database.rs +++ b/crates/redis-cloud/src/handlers/database.rs @@ -1,4 +1,38 @@ //! Database operations handler +//! +//! This module provides comprehensive database management capabilities for Redis Cloud, +//! including CRUD operations, backups, imports, metrics, and scaling operations. +//! +//! # Examples +//! +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudDatabaseHandler}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .build()?; +//! +//! let db_handler = CloudDatabaseHandler::new(client); +//! +//! // Get database information +//! let db_info = db_handler.get(123, 456).await?; +//! println!("Database: {}", db_info.name); +//! +//! // Create database using raw client API +//! let database_config = json!({ +//! "name": "my-redis-db", +//! "memory_limit_in_gb": 2.5, +//! "support_oss_cluster_api": false, +//! "replication": true, +//! "data_persistence": "aof-every-1-sec" +//! }); +//! # Ok(()) +//! # } +//! ``` use crate::{ Result, @@ -8,16 +42,47 @@ use crate::{ use serde_json::Value; /// Handler for Cloud database operations +/// +/// Provides methods for managing Redis Cloud databases including creation, updates, +/// backups, imports, metrics collection, and scaling operations. +/// +/// All database operations require both a subscription ID and database ID, as databases +/// are scoped within subscriptions in Redis Cloud. pub struct CloudDatabaseHandler { client: CloudClient, } impl CloudDatabaseHandler { + /// Create a new database handler instance + /// + /// # Arguments + /// * `client` - The configured CloudClient instance pub fn new(client: CloudClient) -> Self { CloudDatabaseHandler { client } } - /// Get database by ID + /// Retrieve a specific database by ID + /// + /// Returns detailed information about a database including its configuration, + /// status, endpoints, and current metrics. + /// + /// # Arguments + /// * `subscription_id` - The ID of the subscription containing the database + /// * `database_id` - The unique database identifier + /// + /// # Examples + /// ```rust,no_run + /// # use redis_cloud::{CloudClient, CloudDatabaseHandler}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let client = CloudClient::builder().api_key("key").api_secret("secret").build()?; + /// let db_handler = CloudDatabaseHandler::new(client); + /// let database = db_handler.get(123, 456).await?; + /// println!("Database name: {}", database.name); + /// println!("Memory limit: {} GB", database.memory_limit_in_gb); + /// # Ok(()) + /// # } + /// ``` pub async fn get(&self, subscription_id: u32, database_id: u32) -> Result { self.client .get(&format!( @@ -27,7 +92,35 @@ impl CloudDatabaseHandler { .await } - /// Create a new database + /// Create a new database in a subscription + /// + /// Creates a new Redis database with the specified configuration. The database + /// will be deployed across the subscription's defined regions and cloud providers. + /// + /// # Arguments + /// * `subscription_id` - The ID of the subscription to create the database in + /// * `request` - Database configuration including name, memory, replication settings + /// + /// # Examples + /// ```rust,no_run + /// # use redis_cloud::{CloudClient, CloudDatabaseHandler}; + /// # use serde_json::json; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let client = CloudClient::builder().api_key("key").api_secret("secret").build()?; + /// let db_handler = CloudDatabaseHandler::new(client); + /// let config = json!({ + /// "name": "production-cache", + /// "memory_limit_in_gb": 5.0, + /// "replication": true, + /// "data_persistence": "aof-every-1-sec", + /// "password": "secure-password" + /// }); + /// // Note: create() takes a typed request struct, not JSON + /// // let result = db_handler.create(123, create_request).await?; + /// # Ok(()) + /// # } + /// ``` pub async fn create( &self, subscription_id: u32, @@ -41,7 +134,33 @@ impl CloudDatabaseHandler { .await } - /// Update database + /// Update an existing database configuration + /// + /// Modifies database settings such as memory limits, replication, persistence, + /// and other configuration options. Some changes may require a database restart. + /// + /// # Arguments + /// * `subscription_id` - The ID of the subscription containing the database + /// * `database_id` - The unique database identifier + /// * `request` - Updated configuration settings + /// + /// # Examples + /// ```rust,no_run + /// # use redis_cloud::{CloudClient, CloudDatabaseHandler}; + /// # use serde_json::json; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let client = CloudClient::builder().api_key("key").api_secret("secret").build()?; + /// let db_handler = CloudDatabaseHandler::new(client); + /// let updates = json!({ + /// "memory_limit_in_gb": 10.0, + /// "replication": false + /// }); + /// // Note: update() takes a typed request struct, not JSON + /// // let updated_db = db_handler.update(123, 456, update_request).await?; + /// # Ok(()) + /// # } + /// ``` pub async fn update( &self, subscription_id: u32, @@ -59,7 +178,27 @@ impl CloudDatabaseHandler { .await } - /// Delete database + /// Delete a database permanently + /// + /// **Warning**: This operation is irreversible and will permanently delete + /// all data in the database. Consider creating a backup before deletion. + /// + /// # Arguments + /// * `subscription_id` - The ID of the subscription containing the database + /// * `database_id` - The unique database identifier + /// + /// # Examples + /// ```rust,no_run + /// # use redis_cloud::{CloudClient, CloudDatabaseHandler}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let client = CloudClient::builder().api_key("key").api_secret("secret").build()?; + /// let db_handler = CloudDatabaseHandler::new(client); + /// let result = db_handler.delete(123, 456).await?; + /// println!("Database deleted successfully"); + /// # Ok(()) + /// # } + /// ``` pub async fn delete(&self, subscription_id: u32, database_id: u32) -> Result { self.client .delete(&format!( diff --git a/crates/redis-cloud/src/handlers/subscription.rs b/crates/redis-cloud/src/handlers/subscription.rs index 838f745c..77634c89 100644 --- a/crates/redis-cloud/src/handlers/subscription.rs +++ b/crates/redis-cloud/src/handlers/subscription.rs @@ -1,4 +1,39 @@ //! Subscription operations handler +//! +//! This module provides comprehensive subscription management for Redis Cloud, +//! including creating, updating, and managing subscriptions across multiple cloud providers. +//! +//! # Examples +//! +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudSubscriptionHandler}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .build()?; +//! +//! let sub_handler = CloudSubscriptionHandler::new(client.clone()); +//! +//! // List all subscriptions +//! let subscriptions = sub_handler.list().await?; +//! +//! // Create a new AWS subscription +//! let aws_subscription = json!({ +//! "name": "production-cluster", +//! "provider": "AWS", +//! "regions": [{"region": "us-east-1", "networking": {}}], +//! "plan": "flexible", +//! "payment_method_id": 12345 +//! }); +//! // Use raw client for JSON requests +//! let result = client.post_raw("/subscriptions", aws_subscription).await?; +//! # Ok(()) +//! # } +//! ``` use crate::{ Result, @@ -10,6 +45,10 @@ use crate::{ use serde_json::Value; /// Handler for Cloud subscription operations +/// +/// Manages Redis Cloud subscriptions which define the cloud provider, regions, +/// and infrastructure configuration for hosting databases. Subscriptions serve +/// as containers for databases and define billing, networking, and scaling policies. pub struct CloudSubscriptionHandler { client: CloudClient, } diff --git a/crates/redis-cloud/src/lib.rs b/crates/redis-cloud/src/lib.rs index 6b723878..cd130492 100644 --- a/crates/redis-cloud/src/lib.rs +++ b/crates/redis-cloud/src/lib.rs @@ -1,204 +1,233 @@ -//! Redis Cloud REST API client +//! Redis Cloud REST API Client //! -//! This module provides a client for interacting with Redis Cloud's REST API, -//! enabling subscription management, database operations, and monitoring. +//! A comprehensive Rust client for the Redis Cloud REST API, providing full access to +//! subscription management, database operations, billing, monitoring, and advanced features +//! like VPC peering, SSO/SAML, and Private Service Connect. //! -//! # Examples +//! ## Features //! -//! ## Creating a Client +//! - **Subscription Management**: Create, update, delete subscriptions across AWS, GCP, Azure +//! - **Database Operations**: Full CRUD operations, backups, imports, metrics +//! - **Advanced Networking**: VPC peering, Transit Gateway, Private Service Connect +//! - **Security & Access**: ACLs, SSO/SAML integration, API key management +//! - **Monitoring & Billing**: Comprehensive metrics, logs, billing and payment management +//! - **Enterprise Features**: Active-Active databases (CRDB), fixed/essentials plans //! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudConfig}; +//! ## Quick Start //! -//! # async fn example() -> Result<(), Box> { -//! let config = CloudConfig { -//! api_key: "your-api-key".to_string(), -//! api_secret_key: "your-secret-key".to_string(), -//! api_url: "https://api.redislabs.com/v1".to_string(), -//! }; +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudDatabaseHandler}; //! -//! let client = CloudClient::new(config)?; -//! # Ok(()) -//! # } +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Create client with API credentials +//! let client = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .build()?; +//! +//! // List all databases +//! let db_handler = CloudDatabaseHandler::new(client.clone()); +//! let databases = db_handler.list(123).await?; +//! println!("Found {} databases", databases.as_array().unwrap_or(&vec![]).len()); +//! +//! Ok(()) +//! } //! ``` //! -//! ## Managing Subscriptions +//! ## Core Usage Patterns //! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudSubscriptionHandler, CreateSubscriptionRequest}; +//! ### Client Creation //! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudSubscriptionHandler::new(client); +//! The client uses a builder pattern for flexible configuration: //! -//! // List all subscriptions -//! let subscriptions = handler.list().await?; -//! for sub in subscriptions { -//! println!("Subscription: {} ({})", sub.name, sub.id); -//! } +//! ```rust,no_run +//! use redis_cloud::CloudClient; //! -//! // Create a new subscription -//! let request = CreateSubscriptionRequest { -//! name: "production".to_string(), -//! payment_method_id: 123, -//! memory_storage: "ram".to_string(), -//! cloud_provider: vec![/* provider config */], -//! // ... other fields -//! }; -//! -//! let new_sub = handler.create(request).await?; -//! println!("Created subscription: {}", new_sub.id); +//! # fn main() -> Result<(), Box> { +//! // Basic client with default settings +//! let client = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .build()?; +//! +//! // Custom configuration +//! let client2 = CloudClient::builder() +//! .api_key("your-api-key") +//! .api_secret("your-api-secret") +//! .base_url("https://api.redislabs.com/v1".to_string()) +//! .timeout(std::time::Duration::from_secs(60)) +//! .build()?; //! # Ok(()) //! # } //! ``` //! -//! ## Database Operations +//! ### Working with Subscriptions //! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudDatabaseHandler, CreateDatabaseRequest}; +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudSubscriptionHandler}; +//! use serde_json::json; //! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudDatabaseHandler::new(client); -//! let subscription_id = 12345; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("key") +//! .api_secret("secret") +//! .build()?; //! -//! // List databases in a subscription -//! let databases = handler.list(subscription_id).await?; -//! for db in databases { -//! println!("Database: {} at {}:{}", db.name, db.public_endpoint, db.port); -//! } +//! let sub_handler = CloudSubscriptionHandler::new(client.clone()); //! -//! // Create a new database -//! let request = CreateDatabaseRequest { -//! name: "cache-db".to_string(), -//! memory_limit_in_gb: 1.0, -//! modules: vec!["RedisJSON".to_string()], -//! data_persistence: "aof-every-1-second".to_string(), -//! replication: true, -//! // ... other fields -//! }; -//! -//! let new_db = handler.create(subscription_id, request).await?; -//! println!("Created database: {}", new_db.database_id); -//! -//! // Get database metrics -//! let metrics = handler.get_metrics(subscription_id, new_db.database_id, None, None).await?; -//! println!("Ops/sec: {:?}", metrics); +//! // List subscriptions +//! let subscriptions = sub_handler.list().await?; +//! +//! // Create a new subscription using raw API +//! let new_subscription = json!({ +//! "name": "my-redis-subscription", +//! "provider": "AWS", +//! "region": "us-east-1", +//! "plan": "cache.m5.large" +//! }); +//! let created = client.post_raw("/subscriptions", new_subscription).await?; //! # Ok(()) //! # } //! ``` //! -//! ## Backup Management -//! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudBackupHandler, CreateBackupRequest}; +//! ### Database Management //! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudBackupHandler::new(client); -//! let subscription_id = 12345; -//! let database_id = 67890; +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudDatabaseHandler}; +//! use serde_json::json; //! -//! // List backups -//! let backups = handler.list(subscription_id, database_id).await?; -//! for backup in backups { -//! println!("Backup: {} ({})", backup.backup_id, backup.status); -//! } +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("key") +//! .api_secret("secret") +//! .build()?; //! -//! // Create a backup -//! let request = CreateBackupRequest { -//! description: Some("Pre-deployment backup".to_string()), -//! }; +//! let db_handler = CloudDatabaseHandler::new(client.clone()); //! -//! let new_backup = handler.create(subscription_id, database_id, request).await?; -//! println!("Created backup: {}", new_backup.backup_id); +//! // Create database using raw API +//! let database_config = json!({ +//! "name": "my-database", +//! "memory_limit_in_gb": 1.0, +//! "support_oss_cluster_api": false, +//! "replication": true +//! }); +//! let database = client.post_raw("/subscriptions/123/databases", database_config).await?; //! -//! // Restore from backup -//! handler.restore(subscription_id, database_id, new_backup.backup_id).await?; -//! println!("Restore initiated"); +//! // Get database info +//! let db_info = db_handler.get(123, 456).await?; //! # Ok(()) //! # } //! ``` //! -//! ## ACL Management -//! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudAclHandler}; -//! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudAclHandler::new(client); -//! let subscription_id = 12345; -//! let database_id = 67890; -//! -//! // Get database ACLs -//! let acls = handler.get_database_acls(subscription_id, database_id).await?; -//! for acl in acls { -//! println!("ACL User: {}", acl.username); -//! } -//! -//! // List ACL users -//! let users = handler.list_acl_users().await?; -//! for user in users { -//! println!("User: {} - Rules: {:?}", user.name, user.rules); -//! } +//! ### Advanced Features +//! +//! #### VPC Peering +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudPeeringHandler}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("key") +//! .api_secret("secret") +//! .build()?; +//! +//! let peering_handler = CloudPeeringHandler::new(client.clone()); +//! +//! let peering_request = json!({ +//! "aws_account_id": "123456789012", +//! "vpc_id": "vpc-12345678", +//! "vpc_cidr": "10.0.0.0/16", +//! "region": "us-east-1" +//! }); +//! let peering = client.post_raw("/subscriptions/123/peerings", peering_request).await?; +//! # Ok(()) +//! # } +//! ``` //! -//! // List ACL roles -//! let roles = handler.list_acl_roles().await?; -//! for role in roles { -//! println!("Role: {} - Permissions: {:?}", role.name, role.permissions); -//! } +//! #### SSO/SAML Management +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudSsoHandler}; +//! use serde_json::json; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let client = CloudClient::builder() +//! .api_key("key") +//! .api_secret("secret") +//! .build()?; +//! +//! let sso_handler = CloudSsoHandler::new(client.clone()); +//! +//! // Configure SSO using raw API +//! let sso_config = json!({ +//! "enabled": true, +//! "auto_provision": true +//! }); +//! let config = client.put_raw("/sso", sso_config).await?; //! # Ok(()) //! # } //! ``` //! -//! ## VPC Peering +//! ## Error Handling //! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudPeeringHandler, CreatePeeringRequest}; +//! The client provides comprehensive error handling for different failure scenarios: //! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudPeeringHandler::new(client); -//! let subscription_id = 12345; +//! ```rust,no_run +//! use redis_cloud::{CloudClient, CloudError, CloudDatabaseHandler}; //! -//! // List peerings -//! let peerings = handler.list(subscription_id).await?; -//! for peering in peerings { -//! println!("Peering: {} ({})", peering.peering_id, peering.status); -//! } +//! # #[tokio::main] +//! # async fn main() { +//! let client = CloudClient::builder() +//! .api_key("key") +//! .api_secret("secret") +//! .build().unwrap(); //! -//! // Create VPC peering -//! let request = CreatePeeringRequest { -//! aws_account_id: "123456789012".to_string(), -//! vpc_id: "vpc-12345".to_string(), -//! vpc_cidr: "10.0.0.0/16".to_string(), -//! region: "us-east-1".to_string(), -//! }; +//! let db_handler = CloudDatabaseHandler::new(client.clone()); //! -//! let new_peering = handler.create(subscription_id, request).await?; -//! println!("Created peering: {}", new_peering.peering_id); -//! # Ok(()) +//! match db_handler.get(123, 456).await { +//! Ok(database) => println!("Database: {:?}", database), +//! Err(CloudError::ApiError { code: 404, .. }) => { +//! println!("Database not found"); +//! }, +//! Err(CloudError::AuthenticationFailed) => { +//! println!("Invalid API credentials"); +//! }, +//! Err(e) => println!("Other error: {}", e), +//! } //! # } //! ``` //! -//! ## Cloud Provider Regions +//! ## Handler Overview //! -//! ```ignore -//! use redis_cloud::{CloudClient, CloudRegionHandler}; +//! The client provides specialized handlers for different API domains: //! -//! # async fn example(client: CloudClient) -> Result<(), Box> { -//! let handler = CloudRegionHandler::new(client); +//! | Handler | Purpose | Key Operations | +//! |---------|---------|----------------| +//! | [`CloudSubscriptionHandler`] | Subscription management | create, list, update, delete, pricing | +//! | [`CloudDatabaseHandler`] | Database operations | create, backup, import, metrics, resize | +//! | [`CloudAccountHandler`] | Account information | info, users, payment methods | +//! | [`CloudUsersHandler`] | User management | create, update, delete, invite | +//! | [`CloudBillingHandler`] | Billing & payments | invoices, payment methods, usage reports | +//! | [`CloudBackupHandler`] | Database backups | create, restore, list, delete | +//! | [`CloudAclHandler`] | Access control | users, roles, Redis rules | +//! | [`CloudPeeringHandler`] | VPC peering | create, delete, list peering connections | +//! | [`CloudSsoHandler`] | SSO/SAML | configure, test, user/group mappings | +//! | [`CloudMetricsHandler`] | Monitoring | database and subscription metrics | +//! | [`CloudLogsHandler`] | Audit trails | system, database, and session logs | +//! | [`CloudTasksHandler`] | Async operations | track long-running operations | //! -//! // List AWS regions -//! let aws_regions = handler.list("AWS").await?; -//! for region in aws_regions { -//! println!("AWS Region: {} - {}", region.name, region.display_name); -//! } +//! ## Authentication //! -//! // List GCP regions -//! let gcp_regions = handler.list("GCP").await?; -//! for region in gcp_regions { -//! println!("GCP Region: {} - {}", region.name, region.display_name); -//! } -//! # Ok(()) -//! # } -//! ``` +//! Redis Cloud uses API key authentication with two required headers: +//! - `x-api-key`: Your API key +//! - `x-api-secret-key`: Your API secret +//! +//! These credentials can be obtained from the Redis Cloud console under Account Settings > API Keys. pub mod client; pub mod handlers; @@ -208,7 +237,7 @@ pub mod models; mod lib_tests; // Re-export from the new structure -pub use client::{CloudClient, CloudConfig}; +pub use client::{CloudClient, CloudClientBuilder}; // Re-export handlers explicitly pub use handlers::{ diff --git a/crates/redis-cloud/src/lib_tests.rs b/crates/redis-cloud/src/lib_tests.rs index 95e90b0d..1c552215 100644 --- a/crates/redis-cloud/src/lib_tests.rs +++ b/crates/redis-cloud/src/lib_tests.rs @@ -2,40 +2,14 @@ #[cfg(test)] mod tests { - use crate::{CloudClient, CloudConfig, CloudError, Result}; + use crate::{CloudClient, CloudError, Result}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; - #[tokio::test] - async fn test_cloud_config_default() { - let config = CloudConfig::default(); - assert_eq!(config.base_url, "https://api.redislabs.com/v1"); - assert_eq!(config.timeout, std::time::Duration::from_secs(30)); - assert!(config.api_key.is_empty()); - assert!(config.api_secret.is_empty()); - } - #[tokio::test] async fn test_cloud_client_creation() { - let config = CloudConfig { - api_key: "test_key".to_string(), - api_secret: "test_secret".to_string(), - base_url: "https://example.com".to_string(), - timeout: std::time::Duration::from_secs(10), - }; - - let result = CloudClient::new(config.clone()); - - // Client should be created successfully - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_cloud_client_get_request() { - // Start a background HTTP server on a random local port let mock_server = MockServer::start().await; - // Arrange the behaviour of the MockServer adding a Mock Mock::given(method("GET")) .and(path("/test")) .respond_with( @@ -44,14 +18,12 @@ mod tests { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "test_key".to_string(), - api_secret: "test_secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(10), - }; - - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("test_key") + .api_secret("test_secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let result: Result = client.get("/test").await; assert!(result.is_ok()); @@ -71,14 +43,12 @@ mod tests { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "test_key".to_string(), - api_secret: "test_secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(10), - }; - - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("test_key") + .api_secret("test_secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let test_data = serde_json::json!({"name": "test"}); let result: Result = client.post("/test", &test_data).await; @@ -99,14 +69,12 @@ mod tests { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "test_key".to_string(), - api_secret: "test_secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(10), - }; - - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("test_key") + .api_secret("test_secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let result: Result = client.get("/error").await; assert!(result.is_err()); diff --git a/crates/redis-cloud/src/models/database.rs b/crates/redis-cloud/src/models/database.rs index c9d8182e..838997ca 100644 --- a/crates/redis-cloud/src/models/database.rs +++ b/crates/redis-cloud/src/models/database.rs @@ -1,40 +1,108 @@ //! Database-related data models +//! +//! Contains data structures for Redis Cloud database operations including database +//! configuration, status information, and request/response models for database management. use serde::{Deserialize, Serialize}; use serde_json::Value; -/// Cloud database +/// Represents a Redis Cloud database instance +/// +/// Contains all the configuration, status, and operational information for a database +/// deployed in Redis Cloud. This includes memory settings, persistence configuration, +/// replication status, and connection endpoints. +/// +/// # Examples +/// +/// ```rust,no_run +/// # use redis_cloud::{CloudClient, CloudDatabaseHandler}; +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// # let client = CloudClient::builder().api_key("key").api_secret("secret").build()?; +/// let db_handler = CloudDatabaseHandler::new(client); +/// let database = db_handler.get(123, 456).await?; +/// +/// println!("Database: {}", database.name); +/// println!("Status: {}", database.status); +/// println!("Memory: {:.1} GB", database.memory_limit_in_gb); +/// +/// if let Some(endpoint) = &database.public_endpoint { +/// println!("Connect to: {}", endpoint); +/// } +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CloudDatabase { + /// Unique database identifier within the subscription pub db_id: u32, + /// Human-readable database name pub name: String, + /// Redis protocol version (e.g., "redis") pub protocol: String, + /// Cloud provider hosting the database (AWS, GCP, Azure) pub provider: String, + /// Cloud region where the database is deployed pub region: String, + /// Current database status (active, pending, error, etc.) pub status: String, + /// Maximum memory allocation in gigabytes pub memory_limit_in_gb: f64, + /// Current memory usage in megabytes pub memory_used_in_mb: Option, + /// Memory usage as a percentage (0-100) pub memory_usage: Option, + /// Data persistence configuration (none, aof-every-1-sec, etc.) pub data_persistence: String, + /// Whether replication is enabled for high availability pub replication: bool, + /// Data eviction policy when memory limit is reached pub data_eviction: Option, + /// Throughput measurement configuration pub throughput_measurement: Option, + /// ISO 8601 timestamp when database was activated pub activated_on: Option, + /// ISO 8601 timestamp of last modification pub last_modified: Option, + /// Public internet connection endpoint pub public_endpoint: Option, + /// VPC-private connection endpoint pub private_endpoint: Option, + /// Additional fields not explicitly modeled #[serde(flatten)] pub extra: Value, } +/// Throughput measurement configuration for database performance monitoring #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThroughputMeasurement { + /// Measurement method (e.g., "operations-per-second") pub by: String, + /// Throughput limit value pub value: u32, } -/// Create database request +/// Request payload for creating a new database +/// +/// Defines the configuration for a new Redis database including memory limits, +/// persistence settings, and optional features like cluster API support. +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::CreateDatabaseRequest; +/// use serde_json::json; +/// +/// let request = json!({ +/// "name": "production-cache", +/// "memory_limit_in_gb": 5.0, +/// "data_persistence": "aof-every-1-sec", +/// "replication": true, +/// "password": "secure-password-123", +/// "support_oss_cluster_api": false +/// }); +/// ``` #[derive(Debug, Serialize)] pub struct CreateDatabaseRequest { pub name: String, diff --git a/crates/redis-cloud/src/models/mod.rs b/crates/redis-cloud/src/models/mod.rs index 364a52a4..ab4d1e35 100644 --- a/crates/redis-cloud/src/models/mod.rs +++ b/crates/redis-cloud/src/models/mod.rs @@ -1,4 +1,27 @@ //! Cloud API data models +//! +//! This module contains all data structures used for Redis Cloud API requests and responses. +//! Models are organized by functional area and include both request/response types and +//! configuration structures. +//! +//! # Module Organization +//! +//! - [`account`] - Account information, users, and payment method models +//! - [`backup`] - Database backup and restore operation models +//! - [`database`] - Database configuration, status, and operational models +//! - [`metrics`] - Performance metrics, measurements, and monitoring models +//! - [`peering`] - VPC peering connection and networking models +//! - [`subscription`] - Subscription management and cloud provider models +//! +//! # Common Patterns +//! +//! Most models follow consistent patterns: +//! - Request models: `Create*Request`, `Update*Request` for API inputs +//! - Response models: Plain struct names like `CloudDatabase`, `CloudSubscription` +//! - Configuration models: `*Config` for nested configuration objects +//! +//! All models implement `Serialize` and `Deserialize` for JSON handling and many +//! include `Debug`, `Clone`, and other useful derives. pub mod account; pub mod backup; diff --git a/crates/redis-cloud/tests/account_tests.rs b/crates/redis-cloud/tests/account_tests.rs index ca0e3c1b..d29a70bb 100644 --- a/crates/redis-cloud/tests/account_tests.rs +++ b/crates/redis-cloud/tests/account_tests.rs @@ -1,6 +1,6 @@ //! Account endpoint tests for Redis Cloud -use redis_cloud::{CloudAccountHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudAccountHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/acl_tests.rs b/crates/redis-cloud/tests/acl_tests.rs index d97d4810..eb72698b 100644 --- a/crates/redis-cloud/tests/acl_tests.rs +++ b/crates/redis-cloud/tests/acl_tests.rs @@ -1,6 +1,6 @@ //! ACL endpoint tests for Redis Cloud -use redis_cloud::{CloudAclHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudAclHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{body_json, header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -23,13 +23,12 @@ fn no_content_response() -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Database ACL tests diff --git a/crates/redis-cloud/tests/api_keys_tests.rs b/crates/redis-cloud/tests/api_keys_tests.rs index 667d7abd..7d5d3134 100644 --- a/crates/redis-cloud/tests/api_keys_tests.rs +++ b/crates/redis-cloud/tests/api_keys_tests.rs @@ -1,6 +1,6 @@ //! API keys endpoint tests for Redis Cloud -use redis_cloud::{CloudApiKeysHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudApiKeysHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/backup_tests.rs b/crates/redis-cloud/tests/backup_tests.rs index 436e5453..36d9f8a7 100644 --- a/crates/redis-cloud/tests/backup_tests.rs +++ b/crates/redis-cloud/tests/backup_tests.rs @@ -1,6 +1,6 @@ //! Backup endpoint tests for Redis Cloud -use redis_cloud::{CloudBackupHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudBackupHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{body_json, header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -23,13 +23,12 @@ fn no_content_response() -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/billing_tests.rs b/crates/redis-cloud/tests/billing_tests.rs index 24f2f34f..9b63ea51 100644 --- a/crates/redis-cloud/tests/billing_tests.rs +++ b/crates/redis-cloud/tests/billing_tests.rs @@ -1,6 +1,6 @@ //! Billing endpoint tests for Redis Cloud -use redis_cloud::{CloudBillingHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudBillingHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/cloud_accounts_tests.rs b/crates/redis-cloud/tests/cloud_accounts_tests.rs index f4640277..658f446d 100644 --- a/crates/redis-cloud/tests/cloud_accounts_tests.rs +++ b/crates/redis-cloud/tests/cloud_accounts_tests.rs @@ -1,6 +1,6 @@ //! Cloud accounts endpoint tests for Redis Cloud -use redis_cloud::{CloudAccountsHandler, CloudClient, CloudConfig}; +use redis_cloud::{CloudAccountsHandler, CloudClient}; use serde_json::json; use wiremock::matchers::{body_json, header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -23,13 +23,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Helper function to create mock cloud accounts list response diff --git a/crates/redis-cloud/tests/crdb_tests.rs b/crates/redis-cloud/tests/crdb_tests.rs index 98017e2f..cae1a64a 100644 --- a/crates/redis-cloud/tests/crdb_tests.rs +++ b/crates/redis-cloud/tests/crdb_tests.rs @@ -1,6 +1,6 @@ //! Active-Active (CRDB) database endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudCrdbHandler}; +use redis_cloud::{CloudClient, CloudCrdbHandler}; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/database_tests.rs b/crates/redis-cloud/tests/database_tests.rs index fb431be9..4da767c7 100644 --- a/crates/redis-cloud/tests/database_tests.rs +++ b/crates/redis-cloud/tests/database_tests.rs @@ -1,6 +1,6 @@ //! Database endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudDatabaseHandler}; +use redis_cloud::{CloudClient, CloudDatabaseHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -11,13 +11,12 @@ fn success_response(body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/fixed_tests.rs b/crates/redis-cloud/tests/fixed_tests.rs index 05ba26c7..169b3969 100644 --- a/crates/redis-cloud/tests/fixed_tests.rs +++ b/crates/redis-cloud/tests/fixed_tests.rs @@ -1,6 +1,6 @@ //! Fixed (Essentials) subscription endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudFixedHandler}; +use redis_cloud::{CloudClient, CloudFixedHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/logs_tests.rs b/crates/redis-cloud/tests/logs_tests.rs index 93ce70c0..86b0aacb 100644 --- a/crates/redis-cloud/tests/logs_tests.rs +++ b/crates/redis-cloud/tests/logs_tests.rs @@ -1,6 +1,6 @@ //! Logs endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudLogsHandler}; +use redis_cloud::{CloudClient, CloudLogsHandler}; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Helper function to create mock database logs response @@ -331,13 +330,12 @@ async fn test_system_logs_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "invalid-secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("invalid-secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudLogsHandler::new(client); let result = handler.system(None, None).await; diff --git a/crates/redis-cloud/tests/metrics_tests.rs b/crates/redis-cloud/tests/metrics_tests.rs index 2b64059f..df4a36fc 100644 --- a/crates/redis-cloud/tests/metrics_tests.rs +++ b/crates/redis-cloud/tests/metrics_tests.rs @@ -1,6 +1,6 @@ //! Metrics endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudMetricsHandler}; +use redis_cloud::{CloudClient, CloudMetricsHandler}; use serde_json::json; use wiremock::matchers::{header, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Helper function to create mock database metrics response @@ -382,13 +381,12 @@ async fn test_subscription_metrics_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "invalid-secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("invalid-secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudMetricsHandler::new(client); let result = handler.subscription(12345, None, None).await; diff --git a/crates/redis-cloud/tests/peering_tests.rs b/crates/redis-cloud/tests/peering_tests.rs index 79137597..96cd4b57 100644 --- a/crates/redis-cloud/tests/peering_tests.rs +++ b/crates/redis-cloud/tests/peering_tests.rs @@ -1,6 +1,6 @@ //! Peering endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudPeeringHandler, CreatePeeringRequest}; +use redis_cloud::{CloudClient, CloudPeeringHandler, CreatePeeringRequest}; use serde_json::json; use wiremock::matchers::{body_json, header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -23,13 +23,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Helper function to create mock peerings list response @@ -550,13 +549,12 @@ async fn test_list_peerings_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "invalid-secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("invalid-secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudPeeringHandler::new(client); let result = handler.list(67890).await; diff --git a/crates/redis-cloud/tests/private_service_connect_tests.rs b/crates/redis-cloud/tests/private_service_connect_tests.rs index 904719dc..aa68b64b 100644 --- a/crates/redis-cloud/tests/private_service_connect_tests.rs +++ b/crates/redis-cloud/tests/private_service_connect_tests.rs @@ -1,6 +1,6 @@ //! Private Service Connect endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudPrivateServiceConnectHandler}; +use redis_cloud::{CloudClient, CloudPrivateServiceConnectHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/region_tests.rs b/crates/redis-cloud/tests/region_tests.rs index 1f2d0bd6..57c11c0b 100644 --- a/crates/redis-cloud/tests/region_tests.rs +++ b/crates/redis-cloud/tests/region_tests.rs @@ -1,6 +1,6 @@ //! Region endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudRegionHandler}; +use redis_cloud::{CloudClient, CloudRegionHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] @@ -436,13 +435,12 @@ async fn test_region_list_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("test-secret-key") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudRegionHandler::new(client); let result = handler.list("AWS").await; diff --git a/crates/redis-cloud/tests/sso_tests.rs b/crates/redis-cloud/tests/sso_tests.rs index f14f015b..df111c0a 100644 --- a/crates/redis-cloud/tests/sso_tests.rs +++ b/crates/redis-cloud/tests/sso_tests.rs @@ -1,6 +1,6 @@ //! SSO/SAML configuration endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudSsoHandler}; +use redis_cloud::{CloudClient, CloudSsoHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/subscription_tests.rs b/crates/redis-cloud/tests/subscription_tests.rs index 62b371f1..ea74b3e9 100644 --- a/crates/redis-cloud/tests/subscription_tests.rs +++ b/crates/redis-cloud/tests/subscription_tests.rs @@ -1,6 +1,6 @@ //! Subscription endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudSubscriptionHandler}; +use redis_cloud::{CloudClient, CloudSubscriptionHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -11,13 +11,12 @@ fn success_response(body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/tasks_tests.rs b/crates/redis-cloud/tests/tasks_tests.rs index be8c36fc..06b9745c 100644 --- a/crates/redis-cloud/tests/tasks_tests.rs +++ b/crates/redis-cloud/tests/tasks_tests.rs @@ -1,6 +1,6 @@ //! Tasks endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudTasksHandler}; +use redis_cloud::{CloudClient, CloudTasksHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -15,13 +15,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } // Helper function to create mock tasks list response @@ -269,13 +268,12 @@ async fn test_list_tasks_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "invalid-secret".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("invalid-secret") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudTasksHandler::new(client); let result = handler.list().await; diff --git a/crates/redis-cloud/tests/transit_gateway_tests.rs b/crates/redis-cloud/tests/transit_gateway_tests.rs index b35952f0..7e9903d5 100644 --- a/crates/redis-cloud/tests/transit_gateway_tests.rs +++ b/crates/redis-cloud/tests/transit_gateway_tests.rs @@ -1,6 +1,6 @@ //! Transit Gateway endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudTransitGatewayHandler}; +use redis_cloud::{CloudClient, CloudTransitGatewayHandler}; use serde_json::json; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -19,13 +19,12 @@ fn error_response(status: u16, body: serde_json::Value) -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] diff --git a/crates/redis-cloud/tests/users_tests.rs b/crates/redis-cloud/tests/users_tests.rs index 894649bc..4b67a11b 100644 --- a/crates/redis-cloud/tests/users_tests.rs +++ b/crates/redis-cloud/tests/users_tests.rs @@ -1,6 +1,6 @@ //! Users endpoint tests for Redis Cloud -use redis_cloud::{CloudClient, CloudConfig, CloudUsersHandler}; +use redis_cloud::{CloudClient, CloudUsersHandler}; use serde_json::json; use wiremock::matchers::{body_json, header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -23,13 +23,12 @@ fn no_content_response() -> ResponseTemplate { } fn create_test_client(base_url: String) -> CloudClient { - let config = CloudConfig { - api_key: "test-api-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url, - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).unwrap() + CloudClient::builder() + .api_key("test-api-key") + .api_secret("test-secret-key") + .base_url(base_url) + .build() + .unwrap() } #[tokio::test] @@ -333,13 +332,12 @@ async fn test_users_list_unauthorized() { .mount(&mock_server) .await; - let config = CloudConfig { - api_key: "invalid-key".to_string(), - api_secret: "test-secret-key".to_string(), - base_url: mock_server.uri(), - timeout: std::time::Duration::from_secs(30), - }; - let client = CloudClient::new(config).unwrap(); + let client = CloudClient::builder() + .api_key("invalid-key") + .api_secret("test-secret-key") + .base_url(mock_server.uri()) + .build() + .unwrap(); let handler = CloudUsersHandler::new(client); let result = handler.list().await; diff --git a/crates/redisctl/Cargo.toml b/crates/redisctl/Cargo.toml index b26334c2..b53bd3b9 100644 --- a/crates/redisctl/Cargo.toml +++ b/crates/redisctl/Cargo.toml @@ -38,6 +38,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +chrono = "0.4" rpassword = { workspace = true } urlencoding = "2.1" diff --git a/crates/redisctl/src/cli.rs b/crates/redisctl/src/cli.rs index 36cb99a5..34ed3e66 100644 --- a/crates/redisctl/src/cli.rs +++ b/crates/redisctl/src/cli.rs @@ -172,6 +172,11 @@ pub enum CloudCommands { #[command(subcommand)] command: SsoCommands, }, + /// Billing and payment management + Billing { + #[command(subcommand)] + command: BillingCommands, + }, } #[derive(Subcommand)] @@ -1479,3 +1484,103 @@ pub enum SsoCommands { force: bool, }, } + +#[derive(Subcommand)] +pub enum BillingCommands { + /// Get billing information + Info, + /// List invoices + InvoiceList { + /// Number of invoices to return + #[arg(long)] + limit: Option, + /// Filter by status + #[arg(long)] + status: Option, + }, + /// Get invoice details + InvoiceGet { + /// Invoice ID + id: String, + }, + /// Download invoice PDF + InvoiceDownload { + /// Invoice ID + id: String, + /// Output file path + #[arg(long)] + output: Option, + }, + /// Get current month invoice + InvoiceCurrent, + /// List payment methods + PaymentMethodList, + /// Get payment method details + PaymentMethodGet { + /// Payment method ID + id: String, + }, + /// Add payment method + PaymentMethodAdd { + /// Payment method JSON data + #[arg(long)] + data: String, + }, + /// Update payment method + PaymentMethodUpdate { + /// Payment method ID + id: String, + /// Payment method JSON data + #[arg(long)] + data: String, + }, + /// Delete payment method + PaymentMethodDelete { + /// Payment method ID + id: String, + /// Skip confirmation + #[arg(long)] + force: bool, + }, + /// Set default payment method + PaymentMethodDefault { + /// Payment method ID + id: String, + }, + /// Get cost breakdown + CostBreakdown { + /// Subscription ID (optional) + #[arg(long)] + subscription: Option, + }, + /// Get usage report + Usage { + /// Start date (YYYY-MM-DD) + #[arg(long)] + from: Option, + /// End date (YYYY-MM-DD) + #[arg(long)] + to: Option, + }, + /// Get billing history + History { + /// Number of months + #[arg(long)] + months: Option, + }, + /// Get available credits + Credits, + /// Apply promo code + PromoApply { + /// Promo code + code: String, + }, + /// Get billing alerts + AlertList, + /// Update billing alerts + AlertUpdate { + /// Alert settings JSON + #[arg(long)] + data: String, + }, +} diff --git a/crates/redisctl/src/commands/cloud.rs b/crates/redisctl/src/commands/cloud.rs index 8939fef7..bbf65b76 100644 --- a/crates/redisctl/src/commands/cloud.rs +++ b/crates/redisctl/src/commands/cloud.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use redis_cloud::{CloudClient, CloudConfig}; +use redis_cloud::CloudClient; use redis_common::{OutputFormat, Profile, ProfileCredentials, print_output}; use crate::cli::{ @@ -79,6 +79,16 @@ pub async fn handle_cloud_command( CloudCommands::Sso { command } => { handle_sso_command(command, profile, output_format, query).await } + CloudCommands::Billing { command } => { + let client = create_cloud_client(profile)?; + crate::commands::cloud_billing::handle_billing_command( + command, + &client, + output_format, + query, + ) + .await + } } } @@ -1115,13 +1125,13 @@ pub fn create_cloud_client(profile: &Profile) -> Result { api_url, } = &profile.credentials { - let config = CloudConfig { - api_key: api_key.clone(), - api_secret: api_secret.clone(), - base_url: api_url.clone(), - timeout: std::time::Duration::from_secs(30), - }; - CloudClient::new(config).map_err(Into::into) + CloudClient::builder() + .api_key(api_key.clone()) + .api_secret(api_secret.clone()) + .base_url(api_url.clone()) + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(Into::into) } else { anyhow::bail!("Invalid profile type for Cloud commands") } diff --git a/crates/redisctl/src/commands/cloud_billing.rs b/crates/redisctl/src/commands/cloud_billing.rs new file mode 100644 index 00000000..a365a23c --- /dev/null +++ b/crates/redisctl/src/commands/cloud_billing.rs @@ -0,0 +1,110 @@ +use anyhow::Result; +use redis_cloud::{CloudBillingHandler, CloudClient}; +use redis_common::{OutputFormat, print_output}; + +use crate::cli::BillingCommands; + +pub async fn handle_billing_command( + command: BillingCommands, + client: &CloudClient, + output_format: OutputFormat, + query: Option<&str>, +) -> Result<()> { + let billing_handler = CloudBillingHandler::new(client.clone()); + + let result = match command { + BillingCommands::Info => billing_handler.get_info().await, + BillingCommands::InvoiceList { .. } => { + // The API doesn't support filtering, so we ignore these parameters for now + billing_handler.list_invoices().await + } + BillingCommands::InvoiceGet { id } => billing_handler.get_invoice(&id).await, + BillingCommands::InvoiceDownload { id, output } => { + // Note: The API returns JSON, not actual PDF data. + // This would need to be handled differently in production + let data = billing_handler.download_invoice(&id).await?; + let filename = output.unwrap_or_else(|| format!("invoice_{}.json", id)); + let json_str = serde_json::to_string_pretty(&data)?; + std::fs::write(&filename, json_str)?; + println!("Invoice data saved to {}", filename); + return Ok(()); + } + BillingCommands::InvoiceCurrent => billing_handler.get_current_invoice().await, + BillingCommands::PaymentMethodList => billing_handler.list_payment_methods().await, + BillingCommands::PaymentMethodGet { id } => { + let method_id: u32 = id.parse()?; + billing_handler.get_payment_method(method_id).await + } + BillingCommands::PaymentMethodAdd { data } => { + let payment_method: serde_json::Value = serde_json::from_str(&data)?; + billing_handler.add_payment_method(payment_method).await + } + BillingCommands::PaymentMethodUpdate { id, data } => { + let method_id: u32 = id.parse()?; + let payment_method: serde_json::Value = serde_json::from_str(&data)?; + billing_handler + .update_payment_method(method_id, payment_method) + .await + } + BillingCommands::PaymentMethodDelete { id, force } => { + if !force { + println!( + "Are you sure you want to delete payment method {}? Use --force to confirm", + id + ); + return Ok(()); + } + let method_id: u32 = id.parse()?; + billing_handler.delete_payment_method(method_id).await + } + BillingCommands::PaymentMethodDefault { id } => { + let method_id: u32 = id.parse()?; + billing_handler.set_default_payment_method(method_id).await + } + BillingCommands::CostBreakdown { subscription } => { + // The API expects a period string, not a subscription ID + // Using "current" as a default period + let period = subscription + .map(|s| s.to_string()) + .unwrap_or_else(|| "current".to_string()); + billing_handler.get_cost_breakdown(&period).await + } + BillingCommands::Usage { from, to } => { + // Both dates are required for the usage API + let start = from.as_deref().unwrap_or("2024-01-01"); + let end = to.as_deref().unwrap_or("2024-12-31"); + billing_handler.get_usage(start, end).await + } + BillingCommands::History { months } => { + // The API takes start_date and end_date, not months + // We'll calculate the date range based on months + let end_date = chrono::Local::now().format("%Y-%m-%d").to_string(); + let start_date = if let Some(m) = months { + chrono::Local::now() + .checked_sub_months(chrono::Months::new(m)) + .unwrap_or_else(chrono::Local::now) + .format("%Y-%m-%d") + .to_string() + } else { + // Default to 6 months + chrono::Local::now() + .checked_sub_months(chrono::Months::new(6)) + .unwrap_or_else(chrono::Local::now) + .format("%Y-%m-%d") + .to_string() + }; + billing_handler + .get_history(Some(&start_date), Some(&end_date)) + .await + } + BillingCommands::Credits => billing_handler.get_credits().await, + BillingCommands::PromoApply { code } => billing_handler.apply_promo_code(&code).await, + BillingCommands::AlertList => billing_handler.get_alerts().await, + BillingCommands::AlertUpdate { data } => { + let alerts: serde_json::Value = serde_json::from_str(&data)?; + billing_handler.update_alerts(alerts).await + } + }; + + print_output(result?, output_format, query) +} diff --git a/crates/redisctl/src/commands/mod.rs b/crates/redisctl/src/commands/mod.rs index 7dcdebf6..c304383d 100644 --- a/crates/redisctl/src/commands/mod.rs +++ b/crates/redisctl/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod api; pub mod cloud; +pub mod cloud_billing; pub mod enterprise; pub mod profile;