use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SlaMetricType {
ResponseTimeMs,
UptimePercent,
QualityMinScore,
ThroughputRps,
}
impl std::fmt::Display for SlaMetricType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ResponseTimeMs => write!(f, "response_time_ms"),
Self::UptimePercent => write!(f, "uptime_percent"),
Self::QualityMinScore => write!(f, "quality_min_score"),
Self::ThroughputRps => write!(f, "throughput_rps"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PenaltyType {
Credit,
Refund,
Suspension,
}
impl Default for PenaltyType {
fn default() -> Self {
Self::Credit
}
}
impl std::fmt::Display for PenaltyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Credit => write!(f, "credit"),
Self::Refund => write!(f, "refund"),
Self::Suspension => write!(f, "suspension"),
}
}
}
pub const DEFAULT_PENALTY_PERCENT: Decimal = dec!(5);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlaDefinition {
pub service_id: Uuid,
pub response_time_ms: Option<Decimal>,
pub uptime_percent: Option<Decimal>,
pub quality_min_score: Option<Decimal>,
pub throughput_rps: Option<Decimal>,
pub penalty_percent: Decimal,
pub penalty_type: PenaltyType,
pub active: bool,
}
impl SlaDefinition {
#[must_use]
pub fn new(service_id: Uuid) -> Self {
Self {
service_id,
response_time_ms: None,
uptime_percent: None,
quality_min_score: None,
throughput_rps: None,
penalty_percent: DEFAULT_PENALTY_PERCENT,
penalty_type: PenaltyType::default(),
active: true,
}
}
#[must_use]
pub const fn with_response_time(mut self, ms: Decimal) -> Self {
self.response_time_ms = Some(ms);
self
}
#[must_use]
pub const fn with_uptime(mut self, percent: Decimal) -> Self {
self.uptime_percent = Some(percent);
self
}
#[must_use]
pub const fn with_quality(mut self, min_score: Decimal) -> Self {
self.quality_min_score = Some(min_score);
self
}
#[must_use]
pub const fn with_throughput(mut self, rps: Decimal) -> Self {
self.throughput_rps = Some(rps);
self
}
#[must_use]
pub const fn with_penalty_percent(mut self, percent: Decimal) -> Self {
self.penalty_percent = percent;
self
}
#[must_use]
pub const fn with_penalty_type(mut self, penalty_type: PenaltyType) -> Self {
self.penalty_type = penalty_type;
self
}
#[must_use]
pub const fn has_metrics(&self) -> bool {
self.response_time_ms.is_some()
|| self.uptime_percent.is_some()
|| self.quality_min_score.is_some()
|| self.throughput_rps.is_some()
}
#[must_use]
pub fn configured_metrics(&self) -> Vec<SlaMetricType> {
let mut metrics = Vec::new();
if self.response_time_ms.is_some() {
metrics.push(SlaMetricType::ResponseTimeMs);
}
if self.uptime_percent.is_some() {
metrics.push(SlaMetricType::UptimePercent);
}
if self.quality_min_score.is_some() {
metrics.push(SlaMetricType::QualityMinScore);
}
if self.throughput_rps.is_some() {
metrics.push(SlaMetricType::ThroughputRps);
}
metrics
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn metric_type_display() {
assert_eq!(SlaMetricType::ResponseTimeMs.to_string(), "response_time_ms");
assert_eq!(SlaMetricType::UptimePercent.to_string(), "uptime_percent");
assert_eq!(SlaMetricType::QualityMinScore.to_string(), "quality_min_score");
assert_eq!(SlaMetricType::ThroughputRps.to_string(), "throughput_rps");
}
#[test]
fn penalty_type_display() {
assert_eq!(PenaltyType::Credit.to_string(), "credit");
assert_eq!(PenaltyType::Refund.to_string(), "refund");
assert_eq!(PenaltyType::Suspension.to_string(), "suspension");
}
#[test]
fn penalty_type_default() {
assert_eq!(PenaltyType::default(), PenaltyType::Credit);
}
#[test]
fn sla_definition_defaults() {
let sla = SlaDefinition::new(Uuid::new_v4());
assert_eq!(sla.penalty_percent, dec!(5));
assert_eq!(sla.penalty_type, PenaltyType::Credit);
assert!(sla.active);
assert!(!sla.has_metrics());
}
#[test]
fn sla_definition_builder() {
let sla = SlaDefinition::new(Uuid::new_v4())
.with_response_time(dec!(500))
.with_uptime(dec!(99.9))
.with_quality(dec!(4.0))
.with_throughput(dec!(100))
.with_penalty_percent(dec!(10))
.with_penalty_type(PenaltyType::Refund);
assert_eq!(sla.response_time_ms, Some(dec!(500)));
assert_eq!(sla.uptime_percent, Some(dec!(99.9)));
assert_eq!(sla.quality_min_score, Some(dec!(4.0)));
assert_eq!(sla.throughput_rps, Some(dec!(100)));
assert_eq!(sla.penalty_percent, dec!(10));
assert_eq!(sla.penalty_type, PenaltyType::Refund);
}
#[test]
fn has_metrics_none() {
let sla = SlaDefinition::new(Uuid::new_v4());
assert!(!sla.has_metrics());
}
#[test]
fn has_metrics_with_one() {
let sla = SlaDefinition::new(Uuid::new_v4()).with_response_time(dec!(500));
assert!(sla.has_metrics());
}
#[test]
fn configured_metrics_all() {
let sla = SlaDefinition::new(Uuid::new_v4())
.with_response_time(dec!(500))
.with_uptime(dec!(99))
.with_quality(dec!(4))
.with_throughput(dec!(50));
let metrics = sla.configured_metrics();
assert_eq!(metrics.len(), 4);
}
#[test]
fn configured_metrics_partial() {
let sla = SlaDefinition::new(Uuid::new_v4()).with_uptime(dec!(99));
let metrics = sla.configured_metrics();
assert_eq!(metrics, vec![SlaMetricType::UptimePercent]);
}
#[test]
fn sla_serde_roundtrip() {
let sla = SlaDefinition::new(Uuid::new_v4())
.with_response_time(dec!(200))
.with_uptime(dec!(99.5));
let json = serde_json::to_string(&sla).unwrap();
let parsed: SlaDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.response_time_ms, sla.response_time_ms);
assert_eq!(parsed.uptime_percent, sla.uptime_percent);
}
}