1use std::collections::HashMap;
13
14use crate::error::Result;
15
16#[derive(Debug, Clone)]
18struct DictEntry {
19 pattern: String,
21 code: String,
23 #[allow(dead_code)]
25 frequency: u32,
26}
27
28#[derive(Debug, Clone)]
30pub struct DictConfig {
31 pub max_entries: usize,
34 pub min_pattern_length: usize,
37 pub min_frequency: u32,
40}
41
42impl Default for DictConfig {
43 fn default() -> Self {
44 Self {
45 max_entries: 256,
46 min_pattern_length: 4,
47 min_frequency: 2,
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct DictCompressResult {
55 pub data: String,
57 pub substitutions: usize,
59 pub bytes_saved: usize,
61 pub has_dict_header: bool,
63}
64
65pub struct DictCompressor {
70 config: DictConfig,
71 entries: HashMap<String, DictEntry>,
73 candidates: HashMap<String, u32>,
75 next_code: u16,
77 builtin_entries: Vec<DictEntry>,
79}
80
81impl DictCompressor {
82 pub fn new() -> Self {
83 Self::with_config(DictConfig::default())
84 }
85
86 pub fn with_config(config: DictConfig) -> Self {
87 let builtin = build_builtin_dictionary();
88 let mut compressor = Self {
89 config,
90 entries: HashMap::new(),
91 candidates: HashMap::new(),
92 next_code: 0,
93 builtin_entries: builtin,
94 };
95 compressor.load_builtins();
96 compressor
97 }
98
99 fn load_builtins(&mut self) {
101 for entry in &self.builtin_entries.clone() {
102 self.entries.insert(entry.pattern.clone(), entry.clone());
103 }
104 self.next_code = self.entries.len() as u16;
105 }
106
107 pub fn observe(&mut self, json_str: &str) {
109 let fields = extract_json_fields(json_str);
111 for field in fields {
112 if field.len() >= self.config.min_pattern_length {
113 let count = self.candidates.entry(field.clone()).or_insert(0);
114 *count += 1;
115
116 if *count >= self.config.min_frequency
118 && !self.entries.contains_key(&field)
119 && self.entries.len() < self.config.max_entries
120 {
121 let code = format!("~{:X}", self.next_code);
122 self.next_code += 1;
123 self.entries.insert(
124 field.clone(),
125 DictEntry {
126 pattern: field,
127 code,
128 frequency: *count,
129 },
130 );
131 }
132 }
133 }
134 }
135
136 pub fn compress(&self, json_str: &str) -> Result<DictCompressResult> {
141 if self.entries.is_empty() {
142 return Ok(DictCompressResult {
143 data: json_str.to_string(),
144 substitutions: 0,
145 bytes_saved: 0,
146 has_dict_header: false,
147 });
148 }
149
150 let mut result = json_str.to_string();
151 let mut substitutions = 0usize;
152 let mut bytes_saved = 0usize;
153 let mut used_entries: Vec<&DictEntry> = Vec::new();
154
155 let mut sorted_entries: Vec<&DictEntry> = self.entries.values().collect();
157 sorted_entries.sort_by(|a, b| {
158 b.pattern
159 .len()
160 .cmp(&a.pattern.len())
161 .then_with(|| a.pattern.cmp(&b.pattern))
162 });
163
164 for entry in &sorted_entries {
165 let search = format!("\"{}\":", entry.pattern);
167 let replace = format!("\"{}\":", entry.code);
168
169 if result.contains(&search) {
170 let count = result.matches(&search).count();
171 result = result.replace(&search, &replace);
172 substitutions += count;
173 bytes_saved += count * (search.len() - replace.len());
174 used_entries.push(entry);
175 }
176 }
177
178 let has_dict_header = !used_entries.is_empty();
180 if has_dict_header {
181 let header = format_dict_header(&used_entries);
182 result = format!("{header}\n{result}");
183 }
184
185 Ok(DictCompressResult {
186 data: result,
187 substitutions,
188 bytes_saved,
189 has_dict_header,
190 })
191 }
192
193 pub fn dict_size(&self) -> usize {
195 self.entries.len()
196 }
197
198 pub fn reset(&mut self) {
200 self.entries.clear();
201 self.candidates.clear();
202 self.next_code = 0;
203 self.load_builtins();
204 }
205}
206
207impl Default for DictCompressor {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213fn build_builtin_dictionary() -> Vec<DictEntry> {
217 let common_fields = [
218 ("status_code", "~s"),
220 ("message", "~m"),
221 ("timestamp", "~ts"),
222 ("created_at", "~ca"),
223 ("updated_at", "~ua"),
224 ("deleted_at", "~da"),
225 ("description", "~dc"),
226 ("metadata", "~md"),
227 ("properties", "~pp"),
228 ("attributes", "~at"),
229 ("parameters", "~pm"),
230 ("configuration", "~cf"),
231 ("environment", "~ev"),
232 ("dependencies", "~dp"),
233 ("permissions", "~pr"),
234 ("resources", "~rs"),
235 ("namespace", "~ns"),
236 ("annotations", "~an"),
237 ("error_code", "~ec"),
239 ("error_message", "~em"),
240 ("stack_trace", "~st"),
241 ("exception", "~ex"),
242 ("page_size", "~ps"),
244 ("next_token", "~nt"),
245 ("total_count", "~tc"),
246 ("resource_type", "~rt"),
248 ("account_id", "~ai"),
249 ("region", "~rg"),
250 ];
251
252 common_fields
253 .iter()
254 .enumerate()
255 .map(|(_, &(pattern, code))| DictEntry {
256 pattern: pattern.to_string(),
257 code: code.to_string(),
258 frequency: 100, })
260 .collect()
261}
262
263fn extract_json_fields(json_str: &str) -> Vec<String> {
265 let mut fields = Vec::new();
266 let bytes = json_str.as_bytes();
267 let len = bytes.len();
268 let mut i = 0;
269
270 while i < len {
271 if bytes[i] == b'"' {
273 let start = i + 1;
274 i += 1;
275 while i < len && bytes[i] != b'"' {
277 if bytes[i] == b'\\' {
278 i += 1; }
280 i += 1;
281 }
282 if i < len {
283 let end = i;
284 i += 1;
285 while i < len && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n' || bytes[i] == b'\r') {
288 i += 1;
289 }
290 if i < len && bytes[i] == b':' {
291 if let Ok(field) = std::str::from_utf8(&bytes[start..end]) {
292 fields.push(field.to_string());
293 }
294 }
295 }
296 } else {
297 i += 1;
298 }
299 }
300
301 fields
302}
303
304fn format_dict_header(entries: &[&DictEntry]) -> String {
306 let mut lines = vec!["§dict§".to_string()];
307 for entry in entries {
308 lines.push(format!("{}={}", entry.code, entry.pattern));
309 }
310 lines.push("§/dict§".to_string());
311 lines.join("\n")
312}
313
314#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_new_has_builtin_entries() {
322 let comp = DictCompressor::new();
323 assert!(comp.dict_size() > 0);
324 }
325
326 #[test]
327 fn test_compress_with_known_fields() {
328 let comp = DictCompressor::new();
329 let json = r#"{"status_code":200,"message":"ok","timestamp":"2024-01-01"}"#;
330 let result = comp.compress(json).unwrap();
331 assert!(result.substitutions > 0);
332 assert!(result.bytes_saved > 0);
333 assert!(result.has_dict_header);
334 assert!(result.data.contains("§dict§"));
335 }
336
337 #[test]
338 fn test_compress_no_known_fields() {
339 let mut comp = DictCompressor::new();
340 comp.entries.clear(); let json = r#"{"x":1,"y":2}"#;
342 let result = comp.compress(json).unwrap();
343 assert_eq!(result.substitutions, 0);
344 assert_eq!(result.bytes_saved, 0);
345 assert!(!result.has_dict_header);
346 assert_eq!(result.data, json);
347 }
348
349 #[test]
350 fn test_observe_learns_patterns() {
351 let mut comp = DictCompressor::new();
352 let initial_size = comp.dict_size();
353
354 let json = r#"{"custom_field_name":1,"another_long_field":2}"#;
356 comp.observe(json);
357 comp.observe(json);
358
359 assert!(comp.dict_size() > initial_size);
360 }
361
362 #[test]
363 fn test_observe_ignores_short_fields() {
364 let mut comp = DictCompressor::new();
365 let initial_size = comp.dict_size();
366 let json = r#"{"id":1,"x":2}"#;
367 comp.observe(json);
368 comp.observe(json);
369 assert_eq!(comp.dict_size(), initial_size);
371 }
372
373 #[test]
374 fn test_extract_json_fields() {
375 let json = r#"{"name":"Alice","age":30,"nested":{"key":"val"}}"#;
376 let fields = extract_json_fields(json);
377 assert!(fields.contains(&"name".to_string()));
378 assert!(fields.contains(&"age".to_string()));
379 assert!(fields.contains(&"nested".to_string()));
380 assert!(fields.contains(&"key".to_string()));
381 }
382
383 #[test]
384 fn test_extract_json_fields_with_escaped_quotes() {
385 let json = r#"{"field_with_\"quotes\"":"value","normal":1}"#;
386 let fields = extract_json_fields(json);
387 assert!(fields.contains(&"normal".to_string()));
388 }
389
390 #[test]
391 fn test_reset_clears_learned_entries() {
392 let mut comp = DictCompressor::new();
393 let json = r#"{"custom_field_name":1}"#;
394 comp.observe(json);
395 comp.observe(json);
396 let size_after_learn = comp.dict_size();
397
398 comp.reset();
399 assert!(comp.dict_size() <= size_after_learn);
401 assert!(comp.candidates.is_empty());
402 }
403
404 #[test]
405 fn test_dict_header_format() {
406 let entry = DictEntry {
407 pattern: "status_code".to_string(),
408 code: "~s".to_string(),
409 frequency: 10,
410 };
411 let entries = vec![&entry];
412 let header = format_dict_header(&entries);
413 assert!(header.starts_with("§dict§"));
414 assert!(header.ends_with("§/dict§"));
415 assert!(header.contains("~s=status_code"));
416 }
417
418 use proptest::prelude::*;
421
422 proptest! {
423 #[test]
426 fn prop_compression_does_not_increase_content(
427 field_name in "[a-z_]{5,15}",
428 value in "[a-z0-9]{1,20}",
429 repeat in 2usize..=5usize,
430 ) {
431 let mut comp = DictCompressor::new();
432 comp.entries.clear(); let entry = format!("\"{}\":\"{}\"", field_name, value);
435 let json = format!("{{{}}}", std::iter::repeat(entry.as_str())
436 .take(repeat)
437 .collect::<Vec<_>>()
438 .join(","));
439
440 for _ in 0..3 {
442 comp.observe(&json);
443 }
444
445 let result = comp.compress(&json).unwrap();
446
447 if result.substitutions > 0 {
449 prop_assert!(
450 result.bytes_saved > 0,
451 "substitutions made but no bytes saved"
452 );
453 }
454 }
455 }
456}