llm_cost_ops/compression/
types.rs1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum CompressionAlgorithm {
11 Gzip,
13 Brotli,
15 Deflate,
17 Identity,
19}
20
21impl CompressionAlgorithm {
22 pub fn all() -> Vec<Self> {
24 vec![
25 Self::Brotli,
26 Self::Gzip,
27 Self::Deflate,
28 Self::Identity,
29 ]
30 }
31
32 pub fn as_str(&self) -> &'static str {
34 match self {
35 Self::Gzip => "gzip",
36 Self::Brotli => "br",
37 Self::Deflate => "deflate",
38 Self::Identity => "identity",
39 }
40 }
41
42 pub fn quality_score(&self) -> u8 {
44 match self {
45 Self::Brotli => 100, Self::Gzip => 90, Self::Deflate => 80, Self::Identity => 0, }
50 }
51
52 pub fn is_compressed(&self) -> bool {
54 !matches!(self, Self::Identity)
55 }
56}
57
58impl fmt::Display for CompressionAlgorithm {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "{}", self.as_str())
61 }
62}
63
64impl FromStr for CompressionAlgorithm {
65 type Err = super::CompressionError;
66
67 fn from_str(s: &str) -> Result<Self, Self::Err> {
68 match s.to_lowercase().as_str() {
69 "gzip" | "x-gzip" => Ok(Self::Gzip),
70 "br" | "brotli" => Ok(Self::Brotli),
71 "deflate" => Ok(Self::Deflate),
72 "identity" => Ok(Self::Identity),
73 _ => Err(super::CompressionError::UnsupportedAlgorithm(
74 s.to_string(),
75 )),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[derive(Default)]
83pub enum CompressionLevel {
84 Fastest,
86 Fast,
88 #[default]
90 Default,
91 Best,
93 Custom(u32),
95}
96
97impl CompressionLevel {
98 pub fn gzip_level(&self) -> u32 {
100 match self {
101 Self::Fastest => 1,
102 Self::Fast => 3,
103 Self::Default => 6,
104 Self::Best => 9,
105 Self::Custom(level) => (*level).min(9),
106 }
107 }
108
109 pub fn brotli_level(&self) -> u32 {
111 match self {
112 Self::Fastest => 1,
113 Self::Fast => 4,
114 Self::Default => 6,
115 Self::Best => 11,
116 Self::Custom(level) => (*level).min(11),
117 }
118 }
119}
120
121
122#[derive(Debug, Clone, PartialEq)]
124pub struct ContentEncoding {
125 pub algorithm: CompressionAlgorithm,
127 pub quality: f32,
129}
130
131impl ContentEncoding {
132 pub fn new(algorithm: CompressionAlgorithm, quality: f32) -> Self {
134 Self {
135 algorithm,
136 quality: quality.clamp(0.0, 1.0),
137 }
138 }
139
140 pub fn with_algorithm(algorithm: CompressionAlgorithm) -> Self {
142 Self::new(algorithm, 1.0)
143 }
144
145 pub fn parse_accept_encoding(header: &str) -> Vec<Self> {
147 let mut encodings = Vec::new();
148
149 for part in header.split(',') {
150 let part = part.trim();
151 let (encoding, quality) = if let Some((enc, q)) = part.split_once(';') {
152 let enc = enc.trim();
153 let quality = q
154 .trim()
155 .strip_prefix("q=")
156 .and_then(|q| q.parse::<f32>().ok())
157 .unwrap_or(1.0);
158 (enc, quality)
159 } else {
160 (part, 1.0)
161 };
162
163 if let Ok(algorithm) = CompressionAlgorithm::from_str(encoding) {
164 encodings.push(Self::new(algorithm, quality));
165 } else if encoding == "*" {
166 for algo in CompressionAlgorithm::all() {
168 encodings.push(Self::new(algo, quality * 0.5));
169 }
170 }
171 }
172
173 encodings.sort_by(|a, b| {
175 let quality_cmp = b.quality.partial_cmp(&a.quality).unwrap();
176 if quality_cmp == std::cmp::Ordering::Equal {
177 b.algorithm
178 .quality_score()
179 .cmp(&a.algorithm.quality_score())
180 } else {
181 quality_cmp
182 }
183 });
184
185 encodings
186 }
187
188 pub fn select_best(accept_encoding: &str, supported: &[CompressionAlgorithm]) -> Option<CompressionAlgorithm> {
190 let encodings = Self::parse_accept_encoding(accept_encoding);
191
192 for encoding in encodings {
193 if encoding.quality > 0.0 && supported.contains(&encoding.algorithm) {
194 return Some(encoding.algorithm);
195 }
196 }
197
198 None
199 }
200}
201
202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204pub struct CompressionStats {
205 pub original_size: usize,
207 pub compressed_size: usize,
209 pub compression_ratio: f64,
211 pub bytes_saved: usize,
213 pub algorithm: Option<CompressionAlgorithm>,
215 pub duration_ms: f64,
217}
218
219impl CompressionStats {
220 pub fn new(
222 original_size: usize,
223 compressed_size: usize,
224 algorithm: CompressionAlgorithm,
225 duration_ms: f64,
226 ) -> Self {
227 let compression_ratio = if original_size > 0 {
228 compressed_size as f64 / original_size as f64
229 } else {
230 1.0
231 };
232
233 let bytes_saved = original_size.saturating_sub(compressed_size);
234
235 Self {
236 original_size,
237 compressed_size,
238 compression_ratio,
239 bytes_saved,
240 algorithm: Some(algorithm),
241 duration_ms,
242 }
243 }
244
245 pub fn compression_percentage(&self) -> f64 {
247 (1.0 - self.compression_ratio) * 100.0
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_algorithm_from_str() {
257 assert_eq!(
258 CompressionAlgorithm::from_str("gzip").unwrap(),
259 CompressionAlgorithm::Gzip
260 );
261 assert_eq!(
262 CompressionAlgorithm::from_str("br").unwrap(),
263 CompressionAlgorithm::Brotli
264 );
265 assert_eq!(
266 CompressionAlgorithm::from_str("deflate").unwrap(),
267 CompressionAlgorithm::Deflate
268 );
269 assert_eq!(
270 CompressionAlgorithm::from_str("identity").unwrap(),
271 CompressionAlgorithm::Identity
272 );
273 }
274
275 #[test]
276 fn test_algorithm_as_str() {
277 assert_eq!(CompressionAlgorithm::Gzip.as_str(), "gzip");
278 assert_eq!(CompressionAlgorithm::Brotli.as_str(), "br");
279 assert_eq!(CompressionAlgorithm::Deflate.as_str(), "deflate");
280 assert_eq!(CompressionAlgorithm::Identity.as_str(), "identity");
281 }
282
283 #[test]
284 fn test_compression_level_gzip() {
285 assert_eq!(CompressionLevel::Fastest.gzip_level(), 1);
286 assert_eq!(CompressionLevel::Fast.gzip_level(), 3);
287 assert_eq!(CompressionLevel::Default.gzip_level(), 6);
288 assert_eq!(CompressionLevel::Best.gzip_level(), 9);
289 assert_eq!(CompressionLevel::Custom(5).gzip_level(), 5);
290 assert_eq!(CompressionLevel::Custom(20).gzip_level(), 9); }
292
293 #[test]
294 fn test_compression_level_brotli() {
295 assert_eq!(CompressionLevel::Fastest.brotli_level(), 1);
296 assert_eq!(CompressionLevel::Fast.brotli_level(), 4);
297 assert_eq!(CompressionLevel::Default.brotli_level(), 6);
298 assert_eq!(CompressionLevel::Best.brotli_level(), 11);
299 assert_eq!(CompressionLevel::Custom(8).brotli_level(), 8);
300 assert_eq!(CompressionLevel::Custom(20).brotli_level(), 11); }
302
303 #[test]
304 fn test_parse_accept_encoding_simple() {
305 let encodings = ContentEncoding::parse_accept_encoding("gzip, deflate, br");
306 assert_eq!(encodings.len(), 3);
307
308 assert_eq!(encodings[0].algorithm, CompressionAlgorithm::Brotli);
310 assert_eq!(encodings[1].algorithm, CompressionAlgorithm::Gzip);
311 assert_eq!(encodings[2].algorithm, CompressionAlgorithm::Deflate);
312 }
313
314 #[test]
315 fn test_parse_accept_encoding_with_quality() {
316 let encodings = ContentEncoding::parse_accept_encoding("gzip;q=0.8, br;q=1.0, deflate;q=0.5");
317 assert_eq!(encodings.len(), 3);
318
319 assert_eq!(encodings[0].algorithm, CompressionAlgorithm::Brotli);
321 assert_eq!(encodings[0].quality, 1.0);
322
323 assert_eq!(encodings[1].algorithm, CompressionAlgorithm::Gzip);
324 assert_eq!(encodings[1].quality, 0.8);
325
326 assert_eq!(encodings[2].algorithm, CompressionAlgorithm::Deflate);
327 assert_eq!(encodings[2].quality, 0.5);
328 }
329
330 #[test]
331 fn test_select_best_encoding() {
332 let supported = vec![CompressionAlgorithm::Gzip, CompressionAlgorithm::Brotli];
333
334 let best = ContentEncoding::select_best("gzip, br", &supported);
335 assert_eq!(best, Some(CompressionAlgorithm::Brotli)); let best = ContentEncoding::select_best("gzip;q=1.0, br;q=0.5", &supported);
338 assert_eq!(best, Some(CompressionAlgorithm::Gzip)); }
340
341 #[test]
342 fn test_compression_stats() {
343 let stats = CompressionStats::new(1000, 300, CompressionAlgorithm::Gzip, 10.5);
344
345 assert_eq!(stats.original_size, 1000);
346 assert_eq!(stats.compressed_size, 300);
347 assert_eq!(stats.compression_ratio, 0.3);
348 assert_eq!(stats.bytes_saved, 700);
349 assert_eq!(stats.compression_percentage(), 70.0);
350 assert_eq!(stats.duration_ms, 10.5);
351 }
352}