armature_compression/
config.rs

1//! Configuration for compression middleware
2
3use crate::CompressionAlgorithm;
4
5/// Configuration for the compression middleware
6#[derive(Debug, Clone)]
7pub struct CompressionConfig {
8    /// The compression algorithm to use
9    pub algorithm: CompressionAlgorithm,
10
11    /// Compression level (algorithm-specific range)
12    pub level: u32,
13
14    /// Minimum response size in bytes to compress (default: 860 bytes)
15    /// Responses smaller than this won't be compressed
16    pub min_size: usize,
17
18    /// Content types to compress (default: text/*, application/json, etc.)
19    pub compressible_types: Vec<String>,
20
21    /// Whether to compress responses that already have Content-Encoding
22    pub compress_encoded: bool,
23}
24
25impl Default for CompressionConfig {
26    fn default() -> Self {
27        Self {
28            algorithm: CompressionAlgorithm::Auto,
29            level: 0,      // Will use algorithm's default
30            min_size: 860, // Typical MTU threshold
31            compressible_types: default_compressible_types(),
32            compress_encoded: false,
33        }
34    }
35}
36
37impl CompressionConfig {
38    /// Create a new configuration with default settings
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Create a builder for configuration
44    pub fn builder() -> CompressionConfigBuilder {
45        CompressionConfigBuilder::new()
46    }
47
48    /// Get the effective compression level for the configured algorithm
49    pub fn effective_level(&self) -> u32 {
50        if self.level == 0 {
51            self.algorithm.default_level()
52        } else {
53            self.level
54                .clamp(self.algorithm.min_level(), self.algorithm.max_level())
55        }
56    }
57
58    /// Check if a content type should be compressed
59    pub fn should_compress_content_type(&self, content_type: &str) -> bool {
60        let ct_lower = content_type.to_lowercase();
61        let ct_base = ct_lower.split(';').next().unwrap_or(&ct_lower).trim();
62
63        self.compressible_types.iter().any(|pattern| {
64            if pattern.ends_with("/*") {
65                // Wildcard pattern like "text/*"
66                let prefix = &pattern[..pattern.len() - 1];
67                ct_base.starts_with(prefix)
68            } else {
69                ct_base == pattern
70            }
71        })
72    }
73
74    /// Check if a response should be compressed based on size
75    pub fn should_compress_size(&self, size: usize) -> bool {
76        size >= self.min_size
77    }
78}
79
80/// Builder for CompressionConfig
81#[derive(Debug, Clone, Default)]
82pub struct CompressionConfigBuilder {
83    config: CompressionConfig,
84}
85
86impl CompressionConfigBuilder {
87    /// Create a new builder with default settings
88    pub fn new() -> Self {
89        Self {
90            config: CompressionConfig::default(),
91        }
92    }
93
94    /// Set the compression algorithm
95    pub fn algorithm(mut self, algorithm: CompressionAlgorithm) -> Self {
96        self.config.algorithm = algorithm;
97        self
98    }
99
100    /// Set the compression level
101    pub fn level(mut self, level: u32) -> Self {
102        self.config.level = level;
103        self
104    }
105
106    /// Set the minimum response size to compress
107    pub fn min_size(mut self, min_size: usize) -> Self {
108        self.config.min_size = min_size;
109        self
110    }
111
112    /// Set the compressible content types
113    pub fn compressible_types(mut self, types: Vec<String>) -> Self {
114        self.config.compressible_types = types;
115        self
116    }
117
118    /// Add a compressible content type
119    pub fn add_compressible_type(mut self, content_type: impl Into<String>) -> Self {
120        self.config.compressible_types.push(content_type.into());
121        self
122    }
123
124    /// Set whether to compress already-encoded responses
125    pub fn compress_encoded(mut self, compress: bool) -> Self {
126        self.config.compress_encoded = compress;
127        self
128    }
129
130    /// Use gzip compression
131    #[cfg(feature = "gzip")]
132    pub fn gzip(mut self) -> Self {
133        self.config.algorithm = CompressionAlgorithm::Gzip;
134        self
135    }
136
137    /// Use brotli compression
138    #[cfg(feature = "brotli")]
139    pub fn brotli(mut self) -> Self {
140        self.config.algorithm = CompressionAlgorithm::Brotli;
141        self
142    }
143
144    /// Use zstd compression
145    #[cfg(feature = "zstd")]
146    pub fn zstd(mut self) -> Self {
147        self.config.algorithm = CompressionAlgorithm::Zstd;
148        self
149    }
150
151    /// Disable compression
152    pub fn no_compression(mut self) -> Self {
153        self.config.algorithm = CompressionAlgorithm::None;
154        self
155    }
156
157    /// Build the configuration
158    pub fn build(self) -> CompressionConfig {
159        self.config
160    }
161}
162
163/// Default content types that should be compressed
164fn default_compressible_types() -> Vec<String> {
165    vec![
166        // Text types
167        "text/*".to_string(),
168        // JSON
169        "application/json".to_string(),
170        "application/ld+json".to_string(),
171        // JavaScript
172        "application/javascript".to_string(),
173        "application/x-javascript".to_string(),
174        // XML
175        "application/xml".to_string(),
176        "application/xhtml+xml".to_string(),
177        "application/rss+xml".to_string(),
178        "application/atom+xml".to_string(),
179        // SVG
180        "image/svg+xml".to_string(),
181        // Fonts
182        "font/ttf".to_string(),
183        "font/otf".to_string(),
184        "application/vnd.ms-fontobject".to_string(),
185        // Other
186        "application/wasm".to_string(),
187        "application/manifest+json".to_string(),
188    ]
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_default_config() {
197        let config = CompressionConfig::default();
198        assert_eq!(config.algorithm, CompressionAlgorithm::Auto);
199        assert_eq!(config.min_size, 860);
200        assert!(!config.compress_encoded);
201    }
202
203    #[test]
204    fn test_builder() {
205        let config = CompressionConfig::builder().min_size(1024).level(6).build();
206
207        assert_eq!(config.min_size, 1024);
208        assert_eq!(config.level, 6);
209    }
210
211    #[test]
212    fn test_should_compress_content_type() {
213        let config = CompressionConfig::default();
214
215        // Should compress
216        assert!(config.should_compress_content_type("text/html"));
217        assert!(config.should_compress_content_type("text/css"));
218        assert!(config.should_compress_content_type("text/plain; charset=utf-8"));
219        assert!(config.should_compress_content_type("application/json"));
220        assert!(config.should_compress_content_type("application/javascript"));
221        assert!(config.should_compress_content_type("image/svg+xml"));
222
223        // Should not compress
224        assert!(!config.should_compress_content_type("image/png"));
225        assert!(!config.should_compress_content_type("image/jpeg"));
226        assert!(!config.should_compress_content_type("video/mp4"));
227        assert!(!config.should_compress_content_type("application/octet-stream"));
228    }
229
230    #[test]
231    fn test_should_compress_size() {
232        let config = CompressionConfig::builder().min_size(1024).build();
233
234        assert!(!config.should_compress_size(100));
235        assert!(!config.should_compress_size(1023));
236        assert!(config.should_compress_size(1024));
237        assert!(config.should_compress_size(10000));
238    }
239
240    #[cfg(feature = "gzip")]
241    #[test]
242    fn test_effective_level_gzip() {
243        let config = CompressionConfig::builder().gzip().build();
244        assert_eq!(config.effective_level(), 6); // Default for gzip
245
246        let config = CompressionConfig::builder().gzip().level(9).build();
247        assert_eq!(config.effective_level(), 9);
248
249        // Test clamping
250        let config = CompressionConfig::builder().gzip().level(100).build();
251        assert_eq!(config.effective_level(), 9); // Max for gzip
252    }
253
254    #[cfg(feature = "brotli")]
255    #[test]
256    fn test_builder_brotli() {
257        let config = CompressionConfig::builder().brotli().build();
258        assert_eq!(config.algorithm, CompressionAlgorithm::Brotli);
259    }
260}