llm_cost_ops/compression/
types.rs

1// Core types for compression
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7/// Compression algorithm
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum CompressionAlgorithm {
11    /// Gzip compression (RFC 1952)
12    Gzip,
13    /// Brotli compression (RFC 7932)
14    Brotli,
15    /// Deflate compression (RFC 1951)
16    Deflate,
17    /// No compression (identity)
18    Identity,
19}
20
21impl CompressionAlgorithm {
22    /// Get all supported algorithms
23    pub fn all() -> Vec<Self> {
24        vec![
25            Self::Brotli,
26            Self::Gzip,
27            Self::Deflate,
28            Self::Identity,
29        ]
30    }
31
32    /// Get the content-encoding header value
33    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    /// Get quality score for algorithm selection (higher is better)
43    pub fn quality_score(&self) -> u8 {
44        match self {
45            Self::Brotli => 100,  // Best compression ratio
46            Self::Gzip => 90,     // Good compression, widely supported
47            Self::Deflate => 80,  // Legacy support
48            Self::Identity => 0,  // No compression
49        }
50    }
51
52    /// Check if algorithm requires compression
53    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/// Compression level
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[derive(Default)]
83pub enum CompressionLevel {
84    /// Fastest compression (level 1)
85    Fastest,
86    /// Fast compression (level 3)
87    Fast,
88    /// Balanced compression (level 6)
89    #[default]
90    Default,
91    /// Best compression (level 9)
92    Best,
93    /// Custom level (0-11 for brotli, 0-9 for gzip/deflate)
94    Custom(u32),
95}
96
97impl CompressionLevel {
98    /// Get numeric level for gzip/deflate (0-9)
99    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    /// Get numeric level for brotli (0-11)
110    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/// Content encoding with quality factor
123#[derive(Debug, Clone, PartialEq)]
124pub struct ContentEncoding {
125    /// Compression algorithm
126    pub algorithm: CompressionAlgorithm,
127    /// Quality factor (0.0 - 1.0)
128    pub quality: f32,
129}
130
131impl ContentEncoding {
132    /// Create a new content encoding
133    pub fn new(algorithm: CompressionAlgorithm, quality: f32) -> Self {
134        Self {
135            algorithm,
136            quality: quality.clamp(0.0, 1.0),
137        }
138    }
139
140    /// Create with default quality (1.0)
141    pub fn with_algorithm(algorithm: CompressionAlgorithm) -> Self {
142        Self::new(algorithm, 1.0)
143    }
144
145    /// Parse Accept-Encoding header value
146    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                // Wildcard - accept any encoding
167                for algo in CompressionAlgorithm::all() {
168                    encodings.push(Self::new(algo, quality * 0.5));
169                }
170            }
171        }
172
173        // Sort by quality (descending) and algorithm score
174        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    /// Select best encoding from accepted encodings
189    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/// Compression statistics
203#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204pub struct CompressionStats {
205    /// Original size in bytes
206    pub original_size: usize,
207    /// Compressed size in bytes
208    pub compressed_size: usize,
209    /// Compression ratio (compressed/original)
210    pub compression_ratio: f64,
211    /// Space saved in bytes
212    pub bytes_saved: usize,
213    /// Algorithm used
214    pub algorithm: Option<CompressionAlgorithm>,
215    /// Time taken in milliseconds
216    pub duration_ms: f64,
217}
218
219impl CompressionStats {
220    /// Create new compression stats
221    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    /// Get compression percentage
246    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); // Clamped
291    }
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); // Clamped
301    }
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        // Should be sorted by quality score (brotli > gzip > deflate)
309        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        // Should be sorted by quality first
320        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)); // Higher quality score
336
337        let best = ContentEncoding::select_best("gzip;q=1.0, br;q=0.5", &supported);
338        assert_eq!(best, Some(CompressionAlgorithm::Gzip)); // Higher quality factor
339    }
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}