1use sha2::{Digest, Sha256};
2
3#[repr(u8)]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum StreamIntent {
9 Auto,
11 Realtime,
13 Install,
15}
16
17#[derive(Debug, thiserror::Error)]
19pub enum ProfileError {
20 #[error("latency weight must be between 0 and 100 inclusive")]
21 LatencyWeightOutOfRange,
22 #[error("resilience weight must be between 0 and 100 inclusive")]
23 ResilienceWeightOutOfRange,
24 #[error("latency and resilience weights cannot both be zero")]
25 ZeroTotalWeight,
26}
27
28#[derive(Debug, Clone)]
32pub struct StreamProfile {
33 intent: StreamIntent,
34 latency_weight: u8,
35 resilience_weight: u8,
36}
37
38impl StreamProfile {
39 pub fn auto() -> Self {
41 Self {
42 intent: StreamIntent::Auto,
43 latency_weight: 50,
44 resilience_weight: 50,
45 }
46 }
47
48 pub fn realtime() -> Self {
50 Self {
51 intent: StreamIntent::Realtime,
52 latency_weight: 80,
53 resilience_weight: 20,
54 }
55 }
56
57 pub fn install() -> Self {
59 Self {
60 intent: StreamIntent::Install,
61 latency_weight: 25,
62 resilience_weight: 75,
63 }
64 }
65
66 pub fn with_weights(intent: StreamIntent, latency_weight: u8, resilience_weight: u8) -> Self {
68 Self {
69 intent,
70 latency_weight,
71 resilience_weight,
72 }
73 }
74
75 pub fn compile(self) -> Result<CompiledStreamProfile, ProfileError> {
81 if self.latency_weight > 100 {
82 return Err(ProfileError::LatencyWeightOutOfRange);
83 }
84 if self.resilience_weight > 100 {
85 return Err(ProfileError::ResilienceWeightOutOfRange);
86 }
87 if self.latency_weight == 0 && self.resilience_weight == 0 {
88 return Err(ProfileError::ZeroTotalWeight);
89 }
90
91 let mut hasher = Sha256::new();
92 hasher.update(&[self.latency_weight, self.resilience_weight]);
93 hasher.update(&[self.intent as u8]);
94 let digest = hasher.finalize();
95 let config_id = digest.iter().map(|byte| format!("{:02x}", byte)).collect();
96
97 Ok(CompiledStreamProfile {
98 intent: self.intent,
99 latency_weight: self.latency_weight,
100 resilience_weight: self.resilience_weight,
101 config_id,
102 })
103 }
104}
105
106#[derive(Debug, Clone)]
110pub struct CompiledStreamProfile {
111 intent: StreamIntent,
112 latency_weight: u8,
113 resilience_weight: u8,
114 config_id: String,
115}
116
117impl CompiledStreamProfile {
118 pub fn config_id(&self) -> &str {
120 &self.config_id
121 }
122
123 pub fn latency_weight(&self) -> u8 {
125 self.latency_weight
126 }
127
128 pub fn resilience_weight(&self) -> u8 {
130 self.resilience_weight
131 }
132}
133
134impl Default for StreamProfile {
135 fn default() -> Self {
136 Self::auto()
137 }
138
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn compile_non_zero_weights() {
147 let profile = StreamProfile::with_weights(StreamIntent::Auto, 10, 40);
148 let compiled = profile.compile().expect("must compile");
149 assert_eq!(compiled.latency_weight(), 10);
150 assert_eq!(compiled.resilience_weight(), 40);
151 }
152
153 #[test]
154 fn compile_config_id_deterministic() {
155 let a = StreamProfile::auto().compile().unwrap();
156 let b = StreamProfile::auto().compile().unwrap();
157 assert_eq!(a.config_id(), b.config_id());
158 }
159
160 #[test]
161 fn reject_zero_weights() {
162 let profile = StreamProfile::with_weights(StreamIntent::Auto, 0, 0);
163 assert!(matches!(
164 profile.compile(),
165 Err(ProfileError::ZeroTotalWeight)
166 ));
167 }
168
169 #[test]
170 fn reject_overflow_lat() {
171 let profile = StreamProfile::with_weights(StreamIntent::Auto, 200, 0);
172 assert!(matches!(
173 profile.compile(),
174 Err(ProfileError::LatencyWeightOutOfRange)
175 ));
176 }
177}