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 pub fn intent(&self) -> StreamIntent {
107 self.intent
108 }
109}
110
111#[derive(Debug, Clone)]
115pub struct CompiledStreamProfile {
116 intent: StreamIntent,
117 latency_weight: u8,
118 resilience_weight: u8,
119 config_id: String,
120}
121
122impl CompiledStreamProfile {
123 pub fn config_id(&self) -> &str {
125 &self.config_id
126 }
127
128 pub fn latency_weight(&self) -> u8 {
130 self.latency_weight
131 }
132
133 pub fn resilience_weight(&self) -> u8 {
135 self.resilience_weight
136 }
137
138 pub fn intent(&self) -> StreamIntent {
140 self.intent
141 }
142}
143
144impl Default for StreamProfile {
145 fn default() -> Self {
146 Self::auto()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn compile_non_zero_weights() {
156 let profile = StreamProfile::with_weights(StreamIntent::Auto, 10, 40);
157 let compiled = profile.compile().expect("must compile");
158 assert_eq!(compiled.latency_weight(), 10);
159 assert_eq!(compiled.resilience_weight(), 40);
160 }
161
162 #[test]
163 fn compile_config_id_deterministic() {
164 let a = StreamProfile::auto().compile().unwrap();
165 let b = StreamProfile::auto().compile().unwrap();
166 assert_eq!(a.config_id(), b.config_id());
167 }
168
169 #[test]
170 fn default_profile_falls_back_to_auto() {
171 let default = StreamProfile::default().compile().unwrap();
172 let auto = StreamProfile::auto().compile().unwrap();
173 assert_eq!(default.config_id(), auto.config_id());
174 }
175
176 #[test]
177 fn builtin_profiles_have_distinct_ids() {
178 let realtime = StreamProfile::realtime().compile().unwrap();
179 let install = StreamProfile::install().compile().unwrap();
180 assert_ne!(realtime.config_id(), install.config_id());
181 }
182
183 #[test]
184 fn reject_zero_weights() {
185 let profile = StreamProfile::with_weights(StreamIntent::Auto, 0, 0);
186 assert!(matches!(
187 profile.compile(),
188 Err(ProfileError::ZeroTotalWeight)
189 ));
190 }
191
192 #[test]
193 fn reject_overflow_lat() {
194 let profile = StreamProfile::with_weights(StreamIntent::Auto, 200, 0);
195 assert!(matches!(
196 profile.compile(),
197 Err(ProfileError::LatencyWeightOutOfRange)
198 ));
199 }
200}