From 0a518d862755abb32f1bb9258dd8421ae2b3c249 Mon Sep 17 00:00:00 2001 From: Josh Rotenberg Date: Thu, 28 Aug 2025 12:40:51 -0700 Subject: [PATCH 1/2] feat: add builder patterns to request structs - Add typed-builder v0.20 dependency to both redis-cloud and redis-enterprise - Implement builders for high-priority request structs: redis-cloud: - CreateDatabaseRequest, UpdateDatabaseRequest - CreateSubscriptionRequest, UpdateSubscriptionRequest - CloudProviderConfig, CloudRegionConfig - CreateBackupRequest, CreatePeeringRequest redis-enterprise: - CreateUserRequest, UpdateUserRequest - CreateRoleRequest with BdbRole - CreateCrdbRequest with CreateCrdbInstance - Builder benefits: - No more manual Some() wrapping for optional fields - No more .to_string() calls with setter(into) - strip_option allows natural API (no Option in setter) - Default values for optional fields - Compile-time validation - Update example to showcase cleaner builder API - Add comprehensive examples in doc comments Resolves #38 --- crates/redis-cloud/Cargo.toml | 1 + .../examples/database_management.rs | 19 +++---- crates/redis-cloud/src/models/backup.rs | 4 +- crates/redis-cloud/src/models/database.rs | 48 ++++++++++++---- crates/redis-cloud/src/models/peering.rs | 23 +++++++- crates/redis-cloud/src/models/subscription.rs | 55 +++++++++++++++++-- crates/redis-enterprise/Cargo.toml | 1 + crates/redis-enterprise/src/crdb.rs | 40 +++++++++++++- crates/redis-enterprise/src/roles.rs | 29 +++++++++- crates/redis-enterprise/src/users.rs | 39 ++++++++++++- 10 files changed, 226 insertions(+), 33 deletions(-) diff --git a/crates/redis-cloud/Cargo.toml b/crates/redis-cloud/Cargo.toml index 2b9f978a..6f7722f8 100644 --- a/crates/redis-cloud/Cargo.toml +++ b/crates/redis-cloud/Cargo.toml @@ -24,6 +24,7 @@ anyhow = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } url = { workspace = true } +typed-builder = "0.20" [dev-dependencies] wiremock = { workspace = true } diff --git a/crates/redis-cloud/examples/database_management.rs b/crates/redis-cloud/examples/database_management.rs index 580d90bf..8a82167b 100644 --- a/crates/redis-cloud/examples/database_management.rs +++ b/crates/redis-cloud/examples/database_management.rs @@ -71,16 +71,15 @@ async fn main() -> Result<(), Box> { // Uncomment and modify as needed /* println!("\nCreating a new database..."); - let new_database = CreateDatabaseRequest { - name: "example-db".to_string(), - memory_limit_in_gb: 0.1, // 100 MB - data_persistence: "none".to_string(), - replication: false, - data_eviction: Some("volatile-lru".to_string()), - password: None, - support_oss_cluster_api: Some(false), - use_external_endpoint_for_oss_cluster_api: None, - }; + // Using the new builder pattern for cleaner API + let new_database = CreateDatabaseRequest::builder() + .name("example-db") + .memory_limit_in_gb(0.1) // 100 MB + .data_persistence("none") + .replication(false) + .data_eviction("volatile-lru") + .support_oss_cluster_api(false) + .build(); let created_db = db_handler .create(subscription_id, new_database) diff --git a/crates/redis-cloud/src/models/backup.rs b/crates/redis-cloud/src/models/backup.rs index 62d2a86e..9e977272 100644 --- a/crates/redis-cloud/src/models/backup.rs +++ b/crates/redis-cloud/src/models/backup.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Backup information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,9 +18,10 @@ pub struct CloudBackup { } /// Create backup request -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateBackupRequest { pub database_id: u32, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub description: Option, } diff --git a/crates/redis-cloud/src/models/database.rs b/crates/redis-cloud/src/models/database.rs index 838997ca..d7491b4f 100644 --- a/crates/redis-cloud/src/models/database.rs +++ b/crates/redis-cloud/src/models/database.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Represents a Redis Cloud database instance /// @@ -92,46 +93,71 @@ pub struct ThroughputMeasurement { /// /// ```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 -/// }); +/// let request = CreateDatabaseRequest::builder() +/// .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) +/// .build(); /// ``` -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateDatabaseRequest { + #[builder(setter(into))] pub name: String, pub memory_limit_in_gb: f64, + #[builder(setter(into))] pub data_persistence: String, + #[builder(default)] pub replication: bool, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub data_eviction: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub password: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub support_oss_cluster_api: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub use_external_endpoint_for_oss_cluster_api: Option, } /// Update database request -#[derive(Debug, Serialize)] +/// +/// All fields are optional - only provide the fields you want to update. +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::UpdateDatabaseRequest; +/// +/// let request = UpdateDatabaseRequest::builder() +/// .memory_limit_in_gb(10.0) +/// .replication(true) +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct UpdateDatabaseRequest { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub memory_limit_in_gb: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub data_persistence: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub replication: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub data_eviction: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub password: Option, } diff --git a/crates/redis-cloud/src/models/peering.rs b/crates/redis-cloud/src/models/peering.rs index 5cab3688..e6120e0c 100644 --- a/crates/redis-cloud/src/models/peering.rs +++ b/crates/redis-cloud/src/models/peering.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// VPC Peering #[derive(Debug, Clone, Serialize, Deserialize)] @@ -18,12 +19,32 @@ pub struct CloudPeering { } /// Create peering request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::CreatePeeringRequest; +/// +/// let request = CreatePeeringRequest::builder() +/// .subscription_id(123) +/// .provider("AWS") +/// .aws_account_id("123456789012") +/// .vpc_id("vpc-12345678") +/// .vpc_cidr("10.0.0.0/16") +/// .region("us-east-1") +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreatePeeringRequest { pub subscription_id: u32, + #[builder(setter(into))] pub provider: String, + #[builder(default, setter(into, strip_option))] pub aws_account_id: Option, + #[builder(setter(into))] pub vpc_id: String, + #[builder(setter(into))] pub vpc_cidr: String, + #[builder(setter(into))] pub region: String, } diff --git a/crates/redis-cloud/src/models/subscription.rs b/crates/redis-cloud/src/models/subscription.rs index c8a8ad5c..c87f7715 100644 --- a/crates/redis-cloud/src/models/subscription.rs +++ b/crates/redis-cloud/src/models/subscription.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Cloud subscription #[derive(Debug, Clone, Serialize, Deserialize)] @@ -35,42 +36,88 @@ pub struct CloudRegion { } /// Create subscription request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::{CreateSubscriptionRequest, CloudProviderConfig, CloudRegionConfig}; +/// +/// let request = CreateSubscriptionRequest::builder() +/// .name("production") +/// .payment_method_id(12345) +/// .memory_storage("ram") +/// .cloud_provider( +/// CloudProviderConfig::builder() +/// .provider("AWS") +/// .regions(vec![ +/// CloudRegionConfig::builder() +/// .region("us-east-1") +/// .multiple_availability_zones(true) +/// .build() +/// ]) +/// .build() +/// ) +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateSubscriptionRequest { + #[builder(setter(into))] pub name: String, pub payment_method_id: u32, + #[builder(setter(into))] pub memory_storage: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub persistent_storage_encryption: Option, pub cloud_provider: CloudProviderConfig, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct CloudProviderConfig { + #[builder(setter(into))] pub provider: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub cloud_account_id: Option, pub regions: Vec, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct CloudRegionConfig { + #[builder(setter(into))] pub region: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub networking_deployment_cidr: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub preferred_availability_zones: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub multiple_availability_zones: Option, } /// Update subscription request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_cloud::UpdateSubscriptionRequest; +/// +/// let request = UpdateSubscriptionRequest::builder() +/// .name("production-updated") +/// .payment_method_id(54321) +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct UpdateSubscriptionRequest { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub payment_method_id: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub memory_storage: Option, } diff --git a/crates/redis-enterprise/Cargo.toml b/crates/redis-enterprise/Cargo.toml index 954f23ab..b041f506 100644 --- a/crates/redis-enterprise/Cargo.toml +++ b/crates/redis-enterprise/Cargo.toml @@ -25,6 +25,7 @@ anyhow = { workspace = true } base64 = { workspace = true } chrono = { workspace = true } url = { workspace = true } +typed-builder = "0.20" [dev-dependencies] wiremock = { workspace = true } diff --git a/crates/redis-enterprise/src/crdb.rs b/crates/redis-enterprise/src/crdb.rs index 26d6a9fc..9460097e 100644 --- a/crates/redis-enterprise/src/crdb.rs +++ b/crates/redis-enterprise/src/crdb.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// CRDB (Active-Active Database) information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -36,28 +37,63 @@ pub struct CrdbInstance { } /// Create CRDB request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_enterprise::{CreateCrdbRequest, CreateCrdbInstance}; +/// +/// let request = CreateCrdbRequest::builder() +/// .name("global-cache") +/// .memory_size(1024 * 1024 * 1024) // 1GB +/// .instances(vec![ +/// CreateCrdbInstance::builder() +/// .cluster("cluster1.example.com") +/// .cluster_url("https://cluster1.example.com:9443") +/// .username("admin") +/// .password("password") +/// .build(), +/// CreateCrdbInstance::builder() +/// .cluster("cluster2.example.com") +/// .cluster_url("https://cluster2.example.com:9443") +/// .username("admin") +/// .password("password") +/// .build() +/// ]) +/// .encryption(true) +/// .data_persistence("aof") +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateCrdbRequest { + #[builder(setter(into))] pub name: String, pub memory_size: u64, pub instances: Vec, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub encryption: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub data_persistence: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub eviction_policy: Option, } /// Create CRDB instance -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateCrdbInstance { + #[builder(setter(into))] pub cluster: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub cluster_url: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub username: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub password: Option, } diff --git a/crates/redis-enterprise/src/roles.rs b/crates/redis-enterprise/src/roles.rs index 803ff6a4..644344b3 100644 --- a/crates/redis-enterprise/src/roles.rs +++ b/crates/redis-enterprise/src/roles.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Role information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -24,25 +25,49 @@ pub struct RoleInfo { } /// Database-specific role permissions -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct BdbRole { pub bdb_uid: u32, + #[builder(setter(into))] pub role: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub redis_acl_uid: Option, } /// Create role request -#[derive(Debug, Clone, Serialize, Deserialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_enterprise::{CreateRoleRequest, BdbRole}; +/// +/// let request = CreateRoleRequest::builder() +/// .name("database-admin") +/// .management("admin") +/// .bdb_roles(vec![ +/// BdbRole::builder() +/// .bdb_uid(1) +/// .role("admin") +/// .build() +/// ]) +/// .build(); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct CreateRoleRequest { + #[builder(setter(into))] pub name: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub management: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub data_access: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub bdb_roles: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub cluster_roles: Option>, } diff --git a/crates/redis-enterprise/src/users.rs b/crates/redis-enterprise/src/users.rs index 6b11f340..260daffa 100644 --- a/crates/redis-enterprise/src/users.rs +++ b/crates/redis-enterprise/src/users.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// User information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -22,27 +23,61 @@ pub struct User { } /// Create user request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_enterprise::CreateUserRequest; +/// +/// let request = CreateUserRequest::builder() +/// .username("john.doe") +/// .password("secure-password-123") +/// .role("db_admin") +/// .email("john.doe@example.com") +/// .email_alerts(true) +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct CreateUserRequest { + #[builder(setter(into))] pub username: String, + #[builder(setter(into))] pub password: String, + #[builder(setter(into))] pub role: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub email_alerts: Option, } /// Update user request -#[derive(Debug, Serialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_enterprise::UpdateUserRequest; +/// +/// let request = UpdateUserRequest::builder() +/// .password("new-secure-password") +/// .email_alerts(false) +/// .build(); +/// ``` +#[derive(Debug, Serialize, TypedBuilder)] pub struct UpdateUserRequest { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub password: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub role: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub email_alerts: Option, } From 944d31f63e0bedc51ab5f8e8994aa79a9ff4a74d Mon Sep 17 00:00:00 2001 From: Josh Rotenberg Date: Thu, 28 Aug 2025 13:15:39 -0700 Subject: [PATCH 2/2] feat: add builder patterns to ALL remaining request structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete 100% builder pattern coverage across both libraries! redis-cloud (already complete): - All 6 request structs have TypedBuilder redis-enterprise (now complete): - All 18 request structs now have TypedBuilder - Migrated CreateDatabaseRequest from custom builder to TypedBuilder - Added builders to all HIGH, MEDIUM, and LOW priority structs Structs updated in this commit: - CreateDatabaseRequest (migrated from custom builder) - BootstrapRequest with nested ClusterBootstrapInfo - CreateMigrationRequest with MigrationEndpoint - DebugInfoRequest (all optional fields) - CreateScheduledJobRequest - CreateLdapMappingRequest - CreateSuffixRequest - CreateRedisAclRequest - CreateCrdbTaskRequest - DiagnosticRequest - ServiceConfigRequest - NodeActionRequest - LicenseUpdateRequest - ModuleConfig (helper struct) Achievement: 100% builder pattern coverage! - redis-cloud: 6/6 structs ✅ - redis-enterprise: 18/18 structs ✅ Resolves #38 Closes #41 --- crates/redis-enterprise/src/bdb.rs | 143 ++++-------------- crates/redis-enterprise/src/cluster.rs | 11 +- crates/redis-enterprise/src/crdb_tasks.rs | 6 +- crates/redis-enterprise/src/debuginfo.rs | 9 +- crates/redis-enterprise/src/diagnostics.rs | 6 +- crates/redis-enterprise/src/job_scheduler.rs | 8 +- crates/redis-enterprise/src/ldap_mappings.rs | 8 +- crates/redis-enterprise/src/lib.rs | 6 +- crates/redis-enterprise/src/license.rs | 4 +- crates/redis-enterprise/src/migrations.rs | 6 +- crates/redis-enterprise/src/nodes.rs | 5 +- crates/redis-enterprise/src/redis_acls.rs | 6 +- crates/redis-enterprise/src/services.rs | 5 +- crates/redis-enterprise/src/suffixes.rs | 7 +- .../redis-enterprise/tests/database_tests.rs | 10 +- 15 files changed, 107 insertions(+), 133 deletions(-) diff --git a/crates/redis-enterprise/src/bdb.rs b/crates/redis-enterprise/src/bdb.rs index a0131ba5..8f88f166 100644 --- a/crates/redis-enterprise/src/bdb.rs +++ b/crates/redis-enterprise/src/bdb.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; // Aliases for easier use pub type Database = DatabaseInfo; @@ -243,136 +244,62 @@ pub struct EndpointInfo { pub dns_name: Option, } -/// Builder for CreateDatabaseRequest -#[derive(Debug, Default)] -pub struct CreateDatabaseRequestBuilder { - name: Option, - memory_size: Option, - port: Option, - replication: Option, - persistence: Option, - eviction_policy: Option, - shards_count: Option, - module_list: Option>, - authentication_redis_pass: Option, -} - -impl CreateDatabaseRequestBuilder { - /// Create a new builder - pub fn new() -> Self { - Self::default() - } - - /// Set the database name (required) - pub fn name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - /// Set the memory size in bytes (required) - pub fn memory_size(mut self, size: u64) -> Self { - self.memory_size = Some(size); - self - } - - /// Set the port number - pub fn port(mut self, port: u16) -> Self { - self.port = Some(port); - self - } - - /// Enable or disable replication - pub fn replication(mut self, enabled: bool) -> Self { - self.replication = Some(enabled); - self - } - - /// Set persistence type ("aof", "snapshot", "disabled") - pub fn persistence(mut self, persistence: impl Into) -> Self { - self.persistence = Some(persistence.into()); - self - } - - /// Set eviction policy - pub fn eviction_policy(mut self, policy: impl Into) -> Self { - self.eviction_policy = Some(policy.into()); - self - } - - /// Set number of shards - pub fn shards(mut self, count: u32) -> Self { - self.shards_count = Some(count); - self - } - - /// Add Redis modules - pub fn modules(mut self, modules: Vec) -> Self { - self.module_list = Some(modules); - self - } - - /// Set database password - pub fn password(mut self, password: impl Into) -> Self { - self.authentication_redis_pass = Some(password.into()); - self - } - - /// Build the request - pub fn build(self) -> Result { - Ok(CreateDatabaseRequest { - name: self.name.ok_or_else(|| { - crate::error::RestError::ValidationError("Database name is required".to_string()) - })?, - memory_size: self.memory_size.ok_or_else(|| { - crate::error::RestError::ValidationError("Memory size is required".to_string()) - })?, - port: self.port, - replication: self.replication, - persistence: self.persistence, - eviction_policy: self.eviction_policy, - shards_count: self.shards_count, - module_list: self.module_list, - authentication_redis_pass: self.authentication_redis_pass, - }) - } -} - /// Module configuration for database creation -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct ModuleConfig { + #[builder(setter(into))] pub module_name: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub module_args: Option, } /// Create database request -#[derive(Debug, Serialize, Deserialize)] +/// +/// # Examples +/// +/// ```rust,no_run +/// use redis_enterprise::{CreateDatabaseRequest, ModuleConfig}; +/// +/// let request = CreateDatabaseRequest::builder() +/// .name("my-database") +/// .memory_size(1024 * 1024 * 1024) // 1GB +/// .port(12000) +/// .replication(true) +/// .persistence("aof") +/// .eviction_policy("volatile-lru") +/// .shards_count(2) +/// .authentication_redis_pass("secure-password") +/// .build(); +/// ``` +#[derive(Debug, Serialize, Deserialize, TypedBuilder)] pub struct CreateDatabaseRequest { + #[builder(setter(into))] pub name: String, pub memory_size: u64, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub port: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub replication: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub persistence: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub eviction_policy: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub shards_count: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub module_list: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub authentication_redis_pass: Option, } -impl CreateDatabaseRequest { - /// Create a new builder for the request - pub fn builder() -> CreateDatabaseRequestBuilder { - CreateDatabaseRequestBuilder::new() - } -} - /// Database handler for executing database commands pub struct DatabaseHandler { client: RestClient, @@ -398,16 +325,6 @@ impl DatabaseHandler { self.client.post("/v1/bdbs", &request).await } - /// Create a new database using builder pattern - pub async fn create_with_builder(&self, f: F) -> Result - where - F: FnOnce(CreateDatabaseRequestBuilder) -> CreateDatabaseRequestBuilder, - { - let builder = CreateDatabaseRequestBuilder::new(); - let request = f(builder).build()?; - self.create(request).await - } - /// Update database configuration (BDB.UPDATE) pub async fn update(&self, uid: u32, updates: Value) -> Result { self.client diff --git a/crates/redis-enterprise/src/cluster.rs b/crates/redis-enterprise/src/cluster.rs index 11a48f06..55614130 100644 --- a/crates/redis-enterprise/src/cluster.rs +++ b/crates/redis-enterprise/src/cluster.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Node information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -119,23 +120,27 @@ pub struct ClusterSettings { } /// Bootstrap request for creating a new cluster -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct BootstrapRequest { + #[builder(setter(into))] pub action: String, pub cluster: ClusterBootstrapInfo, pub credentials: BootstrapCredentials, } /// Cluster information for bootstrap -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct ClusterBootstrapInfo { + #[builder(setter(into))] pub name: String, } /// Credentials for bootstrap -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct BootstrapCredentials { + #[builder(setter(into))] pub username: String, + #[builder(setter(into))] pub password: String, } diff --git a/crates/redis-enterprise/src/crdb_tasks.rs b/crates/redis-enterprise/src/crdb_tasks.rs index f0f8470b..094e11d3 100644 --- a/crates/redis-enterprise/src/crdb_tasks.rs +++ b/crates/redis-enterprise/src/crdb_tasks.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// CRDB task information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -26,11 +27,14 @@ pub struct CrdbTask { } /// CRDB task creation request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct CreateCrdbTaskRequest { + #[builder(setter(into))] pub crdb_guid: String, + #[builder(setter(into))] pub task_type: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub params: Option, } diff --git a/crates/redis-enterprise/src/debuginfo.rs b/crates/redis-enterprise/src/debuginfo.rs index f2ca9372..d5ae2674 100644 --- a/crates/redis-enterprise/src/debuginfo.rs +++ b/crates/redis-enterprise/src/debuginfo.rs @@ -4,21 +4,28 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Debug info collection request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct DebugInfoRequest { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub node_uids: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub bdb_uids: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub include_logs: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub include_metrics: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub include_configs: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub time_range: Option, } diff --git a/crates/redis-enterprise/src/diagnostics.rs b/crates/redis-enterprise/src/diagnostics.rs index 0689d989..11f7aaed 100644 --- a/crates/redis-enterprise/src/diagnostics.rs +++ b/crates/redis-enterprise/src/diagnostics.rs @@ -4,15 +4,19 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Diagnostic check request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct DiagnosticRequest { #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub checks: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub node_uids: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub bdb_uids: Option>, } diff --git a/crates/redis-enterprise/src/job_scheduler.rs b/crates/redis-enterprise/src/job_scheduler.rs index 5dbc0cda..612c2fc8 100644 --- a/crates/redis-enterprise/src/job_scheduler.rs +++ b/crates/redis-enterprise/src/job_scheduler.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Scheduled job information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -26,14 +27,19 @@ pub struct ScheduledJob { } /// Create scheduled job request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct CreateScheduledJobRequest { + #[builder(setter(into))] pub name: String, + #[builder(setter(into))] pub job_type: String, + #[builder(setter(into))] pub schedule: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub params: Option, } diff --git a/crates/redis-enterprise/src/ldap_mappings.rs b/crates/redis-enterprise/src/ldap_mappings.rs index 5592a2d6..8117c9dc 100644 --- a/crates/redis-enterprise/src/ldap_mappings.rs +++ b/crates/redis-enterprise/src/ldap_mappings.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// LDAP mapping information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -22,14 +23,19 @@ pub struct LdapMapping { } /// Create or update LDAP mapping request -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TypedBuilder)] pub struct CreateLdapMappingRequest { + #[builder(setter(into))] pub name: String, + #[builder(setter(into))] pub dn: String, + #[builder(setter(into))] pub role: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub role_uids: Option>, } diff --git a/crates/redis-enterprise/src/lib.rs b/crates/redis-enterprise/src/lib.rs index 0385e22a..d7e95253 100644 --- a/crates/redis-enterprise/src/lib.rs +++ b/crates/redis-enterprise/src/lib.rs @@ -24,7 +24,7 @@ //! ## Working with Databases //! //! ```no_run -//! use redis_enterprise::{EnterpriseClient, BdbHandler, CreateDatabaseRequestBuilder}; +//! use redis_enterprise::{EnterpriseClient, BdbHandler, CreateDatabaseRequest}; //! //! # async fn example(client: EnterpriseClient) -> Result<(), Box> { //! // List all databases @@ -35,13 +35,13 @@ //! } //! //! // Create a new database -//! let request = CreateDatabaseRequestBuilder::new() +//! let request = CreateDatabaseRequest::builder() //! .name("my-database") //! .memory_size(1024 * 1024 * 1024) // 1GB //! .port(12000) //! .replication(false) //! .persistence("aof") -//! .build()?; +//! .build(); //! //! let new_db = handler.create(request).await?; //! println!("Created database: {}", new_db.uid); diff --git a/crates/redis-enterprise/src/license.rs b/crates/redis-enterprise/src/license.rs index 44327de5..e36ec53b 100644 --- a/crates/redis-enterprise/src/license.rs +++ b/crates/redis-enterprise/src/license.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// License information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,8 +28,9 @@ pub struct License { } /// License update request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct LicenseUpdateRequest { + #[builder(setter(into))] pub license: String, } diff --git a/crates/redis-enterprise/src/migrations.rs b/crates/redis-enterprise/src/migrations.rs index 9dfb7712..80b7100a 100644 --- a/crates/redis-enterprise/src/migrations.rs +++ b/crates/redis-enterprise/src/migrations.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Migration task #[derive(Debug, Clone, Serialize, Deserialize)] @@ -42,15 +43,18 @@ pub struct MigrationEndpoint { } /// Create migration request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct CreateMigrationRequest { pub source: MigrationEndpoint, pub target: MigrationEndpoint, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub migration_type: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub key_pattern: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub flush_target: Option, } diff --git a/crates/redis-enterprise/src/nodes.rs b/crates/redis-enterprise/src/nodes.rs index 1263d3a7..95ec26e2 100644 --- a/crates/redis-enterprise/src/nodes.rs +++ b/crates/redis-enterprise/src/nodes.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Node information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -98,10 +99,12 @@ pub struct NodeStats { } /// Node action request -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TypedBuilder)] pub struct NodeActionRequest { + #[builder(setter(into))] pub action: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub node_uid: Option, } diff --git a/crates/redis-enterprise/src/redis_acls.rs b/crates/redis-enterprise/src/redis_acls.rs index 2dddb50b..a8c35aea 100644 --- a/crates/redis-enterprise/src/redis_acls.rs +++ b/crates/redis-enterprise/src/redis_acls.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Redis ACL information #[derive(Debug, Clone, Serialize, Deserialize)] @@ -19,11 +20,14 @@ pub struct RedisAcl { } /// Create or update Redis ACL request -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TypedBuilder)] pub struct CreateRedisAclRequest { + #[builder(setter(into))] pub name: String, + #[builder(setter(into))] pub acl: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into, strip_option))] pub description: Option, } diff --git a/crates/redis-enterprise/src/services.rs b/crates/redis-enterprise/src/services.rs index 76f3dd61..dc0dc598 100644 --- a/crates/redis-enterprise/src/services.rs +++ b/crates/redis-enterprise/src/services.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// Service configuration #[derive(Debug, Clone, Serialize, Deserialize)] @@ -24,12 +25,14 @@ pub struct Service { } /// Service configuration request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct ServiceConfigRequest { pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub config: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub node_uids: Option>, } diff --git a/crates/redis-enterprise/src/suffixes.rs b/crates/redis-enterprise/src/suffixes.rs index 61c983ce..f5006871 100644 --- a/crates/redis-enterprise/src/suffixes.rs +++ b/crates/redis-enterprise/src/suffixes.rs @@ -4,6 +4,7 @@ use crate::client::RestClient; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; +use typed_builder::TypedBuilder; /// DNS suffix configuration #[derive(Debug, Clone, Serialize, Deserialize)] @@ -21,13 +22,17 @@ pub struct Suffix { } /// Create suffix request -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)] pub struct CreateSuffixRequest { + #[builder(setter(into))] pub name: String, + #[builder(setter(into))] pub dns_suffix: String, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub use_internal_addr: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(strip_option))] pub use_external_addr: Option, } diff --git a/crates/redis-enterprise/tests/database_tests.rs b/crates/redis-enterprise/tests/database_tests.rs index 09fe8fee..d2fa180c 100644 --- a/crates/redis-enterprise/tests/database_tests.rs +++ b/crates/redis-enterprise/tests/database_tests.rs @@ -1,5 +1,6 @@ //! Database (BDB) endpoint tests for Redis Enterprise +use redis_enterprise::bdb::CreateDatabaseRequest; use redis_enterprise::{BdbHandler, EnterpriseClient}; use serde_json::json; use wiremock::matchers::{basic_auth, method, path}; @@ -111,9 +112,12 @@ async fn test_database_create() { .unwrap(); let handler = BdbHandler::new(client); - let request = handler - .create_with_builder(|b| b.name("test-db").memory_size(1073741824).port(12000)) - .await; + let request_data = CreateDatabaseRequest::builder() + .name("test-db") + .memory_size(1073741824) + .port(12000) + .build(); + let request = handler.create(request_data).await; assert!(request.is_ok()); let db = request.unwrap();