use sha2::{Digest, Sha256};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamIntent {
Auto,
Realtime,
Install,
}
#[derive(Debug, thiserror::Error)]
pub enum ProfileError {
#[error("latency weight must be between 0 and 100 inclusive")]
LatencyWeightOutOfRange,
#[error("resilience weight must be between 0 and 100 inclusive")]
ResilienceWeightOutOfRange,
#[error("latency and resilience weights cannot both be zero")]
ZeroTotalWeight,
}
#[derive(Debug, Clone)]
pub struct StreamProfile {
intent: StreamIntent,
latency_weight: u8,
resilience_weight: u8,
}
impl StreamProfile {
pub fn auto() -> Self {
Self {
intent: StreamIntent::Auto,
latency_weight: 50,
resilience_weight: 50,
}
}
pub fn realtime() -> Self {
Self {
intent: StreamIntent::Realtime,
latency_weight: 80,
resilience_weight: 20,
}
}
pub fn install() -> Self {
Self {
intent: StreamIntent::Install,
latency_weight: 25,
resilience_weight: 75,
}
}
pub fn with_weights(intent: StreamIntent, latency_weight: u8, resilience_weight: u8) -> Self {
Self {
intent,
latency_weight,
resilience_weight,
}
}
pub fn compile(self) -> Result<CompiledStreamProfile, ProfileError> {
if self.latency_weight > 100 {
return Err(ProfileError::LatencyWeightOutOfRange);
}
if self.resilience_weight > 100 {
return Err(ProfileError::ResilienceWeightOutOfRange);
}
if self.latency_weight == 0 && self.resilience_weight == 0 {
return Err(ProfileError::ZeroTotalWeight);
}
let mut hasher = Sha256::new();
hasher.update(&[self.latency_weight, self.resilience_weight]);
hasher.update(&[self.intent as u8]);
let digest = hasher.finalize();
let config_id = digest.iter().map(|byte| format!("{:02x}", byte)).collect();
Ok(CompiledStreamProfile {
intent: self.intent,
latency_weight: self.latency_weight,
resilience_weight: self.resilience_weight,
config_id,
})
}
}
#[derive(Debug, Clone)]
pub struct CompiledStreamProfile {
intent: StreamIntent,
latency_weight: u8,
resilience_weight: u8,
config_id: String,
}
impl CompiledStreamProfile {
pub fn config_id(&self) -> &str {
&self.config_id
}
pub fn latency_weight(&self) -> u8 {
self.latency_weight
}
pub fn resilience_weight(&self) -> u8 {
self.resilience_weight
}
}
impl Default for StreamProfile {
fn default() -> Self {
Self::auto()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compile_non_zero_weights() {
let profile = StreamProfile::with_weights(StreamIntent::Auto, 10, 40);
let compiled = profile.compile().expect("must compile");
assert_eq!(compiled.latency_weight(), 10);
assert_eq!(compiled.resilience_weight(), 40);
}
#[test]
fn compile_config_id_deterministic() {
let a = StreamProfile::auto().compile().unwrap();
let b = StreamProfile::auto().compile().unwrap();
assert_eq!(a.config_id(), b.config_id());
}
#[test]
fn reject_zero_weights() {
let profile = StreamProfile::with_weights(StreamIntent::Auto, 0, 0);
assert!(matches!(
profile.compile(),
Err(ProfileError::ZeroTotalWeight)
));
}
#[test]
fn reject_overflow_lat() {
let profile = StreamProfile::with_weights(StreamIntent::Auto, 200, 0);
assert!(matches!(
profile.compile(),
Err(ProfileError::LatencyWeightOutOfRange)
));
}
}