#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PlanLimits {
pub cpu_millicores: u32,
pub memory_mb: u32,
pub storage_gb: Option<u32>,
pub max_connections: Option<u32>,
pub read_replica: bool,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
strum::Display,
strum::EnumString,
strum::EnumIter,
)]
#[strum(serialize_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum ComputePlan {
Starter,
Small,
Medium,
Large,
Xl,
}
impl ComputePlan {
#[must_use]
pub const fn limits(&self) -> PlanLimits {
match self {
Self::Starter => PlanLimits {
cpu_millicores: 250,
memory_mb: 256,
storage_gb: None,
max_connections: None,
read_replica: false,
},
Self::Small => PlanLimits {
cpu_millicores: 500,
memory_mb: 512,
storage_gb: None,
max_connections: None,
read_replica: false,
},
Self::Medium => PlanLimits {
cpu_millicores: 1000,
memory_mb: 2048,
storage_gb: None,
max_connections: None,
read_replica: false,
},
Self::Large => PlanLimits {
cpu_millicores: 2000,
memory_mb: 4096,
storage_gb: None,
max_connections: None,
read_replica: false,
},
Self::Xl => PlanLimits {
cpu_millicores: 4000,
memory_mb: 8192,
storage_gb: None,
max_connections: None,
read_replica: false,
},
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde::Serialize,
serde::Deserialize,
strum::Display,
strum::EnumString,
strum::EnumIter,
)]
#[strum(serialize_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum DatabasePlan {
Starter,
Small,
Medium,
Large,
Xl,
}
impl DatabasePlan {
#[must_use]
pub const fn limits(&self) -> PlanLimits {
match self {
Self::Starter => PlanLimits {
cpu_millicores: 250,
memory_mb: 512,
storage_gb: Some(5),
max_connections: Some(20),
read_replica: false,
},
Self::Small => PlanLimits {
cpu_millicores: 500,
memory_mb: 1024,
storage_gb: Some(10),
max_connections: Some(50),
read_replica: false,
},
Self::Medium => PlanLimits {
cpu_millicores: 1000,
memory_mb: 2048,
storage_gb: Some(25),
max_connections: Some(100),
read_replica: true,
},
Self::Large => PlanLimits {
cpu_millicores: 2000,
memory_mb: 4096,
storage_gb: Some(50),
max_connections: Some(200),
read_replica: true,
},
Self::Xl => PlanLimits {
cpu_millicores: 4000,
memory_mb: 8192,
storage_gb: Some(100),
max_connections: Some(500),
read_replica: true,
},
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::str::FromStr;
use strum::IntoEnumIterator;
use super::*;
#[test]
fn compute_starter_has_shared_cpu() {
let limits = ComputePlan::Starter.limits();
assert_eq!(limits.cpu_millicores, 250);
assert_eq!(limits.memory_mb, 256);
}
#[test]
fn compute_xl_has_4_vcpu() {
let limits = ComputePlan::Xl.limits();
assert_eq!(limits.cpu_millicores, 4000);
assert_eq!(limits.memory_mb, 8192);
}
#[test]
fn compute_plans_have_no_storage_or_connections() {
for plan in ComputePlan::iter() {
let limits = plan.limits();
assert_eq!(limits.storage_gb, None, "{plan} should have no storage");
assert_eq!(
limits.max_connections, None,
"{plan} should have no connections"
);
}
}
#[test]
fn compute_plans_scale_monotonically() {
let plans: Vec<ComputePlan> = ComputePlan::iter().collect();
for window in plans.windows(2) {
let (prev, next) = (window[0].limits(), window[1].limits());
assert!(
next.cpu_millicores >= prev.cpu_millicores,
"CPU should scale monotonically"
);
assert!(
next.memory_mb >= prev.memory_mb,
"RAM should scale monotonically"
);
}
}
#[test]
fn database_starter_has_5gb_storage() {
let limits = DatabasePlan::Starter.limits();
assert_eq!(limits.storage_gb, Some(5));
assert_eq!(limits.max_connections, Some(20));
}
#[test]
fn database_medium_and_above_have_read_replica() {
assert!(!DatabasePlan::Starter.limits().read_replica);
assert!(!DatabasePlan::Small.limits().read_replica);
assert!(DatabasePlan::Medium.limits().read_replica);
assert!(DatabasePlan::Large.limits().read_replica);
assert!(DatabasePlan::Xl.limits().read_replica);
}
#[test]
fn database_plans_scale_monotonically() {
let plans: Vec<DatabasePlan> = DatabasePlan::iter().collect();
for window in plans.windows(2) {
let (prev, next) = (window[0].limits(), window[1].limits());
assert!(
next.storage_gb >= prev.storage_gb,
"storage should scale monotonically"
);
assert!(
next.max_connections >= prev.max_connections,
"connections should scale monotonically"
);
}
}
#[test]
fn compute_plan_parses_from_string() {
let plan = ComputePlan::from_str("starter").expect("should parse 'starter'");
assert_eq!(plan, ComputePlan::Starter);
}
#[test]
fn plan_rejects_invalid_string() {
assert!(ComputePlan::from_str("huge").is_err());
assert!(DatabasePlan::from_str("huge").is_err());
}
#[test]
fn compute_plan_displays_as_lowercase() {
assert_eq!(ComputePlan::Starter.to_string(), "starter");
}
#[test]
fn database_plan_roundtrips_through_display_and_parse() {
for plan in DatabasePlan::iter() {
let s = plan.to_string();
let roundtrip = DatabasePlan::from_str(&s).expect("should roundtrip");
assert_eq!(plan, roundtrip);
}
}
}