1use std::collections::HashMap;
45use std::fmt;
46use std::time::Instant;
47use thiserror::Error;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
57pub struct CodecId(pub u32);
58
59impl CodecId {
60 pub const NONE: Self = Self(0);
62 pub const ZSTD: Self = Self(1);
64 pub const LZ4: Self = Self(2);
66 pub const SNAPPY: Self = Self(3);
68 pub const BROTLI: Self = Self(4);
70 pub const ARROW_IPC: Self = Self(10);
72 pub const GARW: Self = Self(11);
74
75 #[inline]
77 pub fn value(self) -> u32 {
78 self.0
79 }
80}
81
82impl fmt::Display for CodecId {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(f, "CodecId({})", self.0)
85 }
86}
87
88impl From<u32> for CodecId {
89 fn from(v: u32) -> Self {
90 Self(v)
91 }
92}
93
94impl From<CodecId> for u32 {
95 fn from(c: CodecId) -> Self {
96 c.0
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
107pub enum SpeedClass {
108 VeryFast,
110 Fast,
112 Balanced,
114 Slow,
116 VerySlow,
118}
119
120#[derive(Debug, Clone)]
124pub struct CodecDescriptor {
125 pub id: CodecId,
127 pub name: String,
129 pub compression_ratio_estimate: f32,
134 pub speed_class: SpeedClass,
136 pub is_lossless: bool,
138}
139
140#[derive(Debug, Error)]
144pub enum CodecError {
145 #[error("codec id {0} is already registered")]
147 AlreadyRegistered(u32),
148 #[error("unknown codec id {0}")]
150 UnknownCodec(u32),
151}
152
153#[derive(Debug)]
158pub struct CodecNegotiationRecord {
159 pub local_offered: Vec<CodecId>,
161 pub remote_offered: Vec<CodecId>,
163 pub agreed: Option<CodecId>,
165 pub negotiated_at: Instant,
167 pub negotiation_ms: u64,
169}
170
171pub struct CodecRegistry {
178 codecs: HashMap<u32, CodecDescriptor>,
180}
181
182impl CodecRegistry {
183 pub fn new() -> Self {
185 let mut registry = Self {
186 codecs: HashMap::new(),
187 };
188
189 let built_ins: &[CodecDescriptor] = &[
190 CodecDescriptor {
191 id: CodecId::NONE,
192 name: "none".to_string(),
193 compression_ratio_estimate: 1.0,
194 speed_class: SpeedClass::VeryFast,
195 is_lossless: true,
196 },
197 CodecDescriptor {
198 id: CodecId::ZSTD,
199 name: "zstd".to_string(),
200 compression_ratio_estimate: 0.30,
201 speed_class: SpeedClass::Balanced,
202 is_lossless: true,
203 },
204 CodecDescriptor {
205 id: CodecId::LZ4,
206 name: "lz4".to_string(),
207 compression_ratio_estimate: 0.55,
208 speed_class: SpeedClass::VeryFast,
209 is_lossless: true,
210 },
211 CodecDescriptor {
212 id: CodecId::SNAPPY,
213 name: "snappy".to_string(),
214 compression_ratio_estimate: 0.60,
215 speed_class: SpeedClass::VeryFast,
216 is_lossless: true,
217 },
218 CodecDescriptor {
219 id: CodecId::BROTLI,
220 name: "brotli".to_string(),
221 compression_ratio_estimate: 0.25,
222 speed_class: SpeedClass::Slow,
223 is_lossless: true,
224 },
225 CodecDescriptor {
226 id: CodecId::ARROW_IPC,
227 name: "arrow_ipc".to_string(),
228 compression_ratio_estimate: 0.70,
229 speed_class: SpeedClass::Fast,
230 is_lossless: true,
231 },
232 CodecDescriptor {
233 id: CodecId::GARW,
234 name: "garw".to_string(),
235 compression_ratio_estimate: 0.40,
236 speed_class: SpeedClass::Fast,
237 is_lossless: true,
238 },
239 ];
240
241 for desc in built_ins {
242 registry.codecs.insert(desc.id.value(), desc.clone());
244 }
245
246 registry
247 }
248
249 pub fn register(&mut self, descriptor: CodecDescriptor) -> Result<(), CodecError> {
254 let key = descriptor.id.value();
255 if self.codecs.contains_key(&key) {
256 return Err(CodecError::AlreadyRegistered(key));
257 }
258 self.codecs.insert(key, descriptor);
259 Ok(())
260 }
261
262 pub fn get(&self, id: CodecId) -> Option<&CodecDescriptor> {
266 self.codecs.get(&id.value())
267 }
268
269 pub fn list_all(&self) -> Vec<&CodecDescriptor> {
271 let mut entries: Vec<&CodecDescriptor> = self.codecs.values().collect();
272 entries.sort_by_key(|d| d.id);
273 entries
274 }
275
276 pub fn negotiate(local: &[CodecId], remote: &[CodecId]) -> Option<CodecId> {
283 let remote_set: std::collections::HashSet<CodecId> = remote.iter().copied().collect();
285 local.iter().find(|id| remote_set.contains(id)).copied()
286 }
287
288 pub fn best_for_speed(&self) -> Option<&CodecDescriptor> {
296 self.codecs.values().min_by(|a, b| {
297 a.speed_class
298 .cmp(&b.speed_class)
299 .then_with(|| a.id.cmp(&b.id))
300 })
301 }
302
303 pub fn best_for_compression(&self) -> Option<&CodecDescriptor> {
312 self.codecs.values().min_by(|a, b| {
313 a.compression_ratio_estimate
314 .partial_cmp(&b.compression_ratio_estimate)
315 .unwrap_or(std::cmp::Ordering::Equal)
316 .then_with(|| a.id.cmp(&b.id))
317 })
318 }
319}
320
321impl Default for CodecRegistry {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
336 fn test_builtin_codecs_all_present() {
337 let r = CodecRegistry::new();
338 let ids = [
339 CodecId::NONE,
340 CodecId::ZSTD,
341 CodecId::LZ4,
342 CodecId::SNAPPY,
343 CodecId::BROTLI,
344 CodecId::ARROW_IPC,
345 CodecId::GARW,
346 ];
347 for id in &ids {
348 assert!(
349 r.get(*id).is_some(),
350 "codec {} should be pre-registered",
351 id
352 );
353 }
354 }
355
356 #[test]
359 fn test_builtin_codec_count() {
360 let r = CodecRegistry::new();
361 assert_eq!(r.list_all().len(), 7);
362 }
363
364 #[test]
367 fn test_get_zstd_descriptor() {
368 let r = CodecRegistry::new();
369 let desc = r.get(CodecId::ZSTD).expect("ZSTD should exist");
370 assert_eq!(desc.id, CodecId::ZSTD);
371 assert_eq!(desc.name, "zstd");
372 assert!(desc.is_lossless);
373 assert_eq!(desc.speed_class, SpeedClass::Balanced);
374 }
375
376 #[test]
377 fn test_get_none_descriptor() {
378 let r = CodecRegistry::new();
379 let desc = r.get(CodecId::NONE).expect("NONE should exist");
380 assert_eq!(desc.compression_ratio_estimate, 1.0);
381 assert_eq!(desc.speed_class, SpeedClass::VeryFast);
382 }
383
384 #[test]
387 fn test_get_unknown_returns_none() {
388 let r = CodecRegistry::new();
389 assert!(r.get(CodecId::from(9999)).is_none());
390 }
391
392 #[test]
395 fn test_negotiate_finds_common_codec() {
396 let local = vec![CodecId::ZSTD, CodecId::LZ4, CodecId::NONE];
397 let remote = vec![CodecId::LZ4, CodecId::NONE];
398 let agreed = CodecRegistry::negotiate(&local, &remote);
399 assert_eq!(agreed, Some(CodecId::LZ4));
400 }
401
402 #[test]
403 fn test_negotiate_respects_local_preference_order() {
404 let local = vec![CodecId::ZSTD, CodecId::LZ4, CodecId::NONE];
406 let remote = vec![CodecId::NONE, CodecId::LZ4, CodecId::ZSTD];
407 let agreed = CodecRegistry::negotiate(&local, &remote);
408 assert_eq!(agreed, Some(CodecId::ZSTD));
409 }
410
411 #[test]
414 fn test_negotiate_no_common_returns_none() {
415 let local = vec![CodecId::ZSTD, CodecId::BROTLI];
416 let remote = vec![CodecId::LZ4, CodecId::SNAPPY];
417 assert_eq!(CodecRegistry::negotiate(&local, &remote), None);
418 }
419
420 #[test]
421 fn test_negotiate_empty_lists_returns_none() {
422 assert_eq!(CodecRegistry::negotiate(&[], &[]), None);
423 assert_eq!(CodecRegistry::negotiate(&[CodecId::ZSTD], &[]), None);
424 assert_eq!(CodecRegistry::negotiate(&[], &[CodecId::ZSTD]), None);
425 }
426
427 #[test]
430 fn test_best_for_speed_is_very_fast() {
431 let r = CodecRegistry::new();
432 let best = r.best_for_speed().expect("registry is non-empty");
433 assert_eq!(
434 best.speed_class,
435 SpeedClass::VeryFast,
436 "expected VeryFast, got {:?}",
437 best.speed_class
438 );
439 }
440
441 #[test]
444 fn test_best_for_compression_is_lowest_ratio() {
445 let r = CodecRegistry::new();
446 let best = r.best_for_compression().expect("registry is non-empty");
447 assert_eq!(
449 best.id,
450 CodecId::BROTLI,
451 "expected BROTLI (0.25), got {} ({})",
452 best.name,
453 best.compression_ratio_estimate
454 );
455 }
456
457 #[test]
460 fn test_register_custom_codec() {
461 let mut r = CodecRegistry::new();
462 let custom = CodecDescriptor {
463 id: CodecId::from(1000),
464 name: "my_codec".to_string(),
465 compression_ratio_estimate: 0.45,
466 speed_class: SpeedClass::Fast,
467 is_lossless: true,
468 };
469 r.register(custom)
470 .expect("custom registration should succeed");
471 let found = r
472 .get(CodecId::from(1000))
473 .expect("custom codec should exist");
474 assert_eq!(found.name, "my_codec");
475 }
476
477 #[test]
480 fn test_register_duplicate_returns_error() {
481 let mut r = CodecRegistry::new();
482 let dup = CodecDescriptor {
483 id: CodecId::ZSTD, name: "duplicate_zstd".to_string(),
485 compression_ratio_estimate: 0.30,
486 speed_class: SpeedClass::Balanced,
487 is_lossless: true,
488 };
489 let err = r.register(dup).expect_err("duplicate should be rejected");
490 match err {
491 CodecError::AlreadyRegistered(id) => assert_eq!(id, CodecId::ZSTD.value()),
492 other => panic!("unexpected error: {}", other),
493 }
494 }
495
496 #[test]
499 fn test_list_all_sorted_order() {
500 let r = CodecRegistry::new();
501 let list = r.list_all();
502 let ids: Vec<u32> = list.iter().map(|d| d.id.value()).collect();
503 let mut sorted = ids.clone();
504 sorted.sort_unstable();
505 assert_eq!(ids, sorted, "list_all() must be sorted by CodecId");
506 }
507
508 #[test]
509 fn test_list_all_first_is_none() {
510 let r = CodecRegistry::new();
511 let list = r.list_all();
512 assert_eq!(list[0].id, CodecId::NONE);
513 }
514
515 #[test]
518 fn test_codec_id_constants() {
519 assert_eq!(CodecId::NONE.value(), 0);
520 assert_eq!(CodecId::ZSTD.value(), 1);
521 assert_eq!(CodecId::LZ4.value(), 2);
522 assert_eq!(CodecId::SNAPPY.value(), 3);
523 assert_eq!(CodecId::BROTLI.value(), 4);
524 assert_eq!(CodecId::ARROW_IPC.value(), 10);
525 assert_eq!(CodecId::GARW.value(), 11);
526 }
527
528 #[test]
531 fn test_codec_id_display() {
532 assert_eq!(CodecId::ZSTD.to_string(), "CodecId(1)");
533 assert_eq!(CodecId::NONE.to_string(), "CodecId(0)");
534 }
535
536 #[test]
539 fn test_codec_negotiation_record() {
540 let local = vec![CodecId::ZSTD, CodecId::LZ4];
541 let remote = vec![CodecId::LZ4];
542 let start = Instant::now();
543 let agreed = CodecRegistry::negotiate(&local, &remote);
544 let elapsed_ms = start.elapsed().as_millis() as u64;
545
546 let record = CodecNegotiationRecord {
547 local_offered: local.clone(),
548 remote_offered: remote.clone(),
549 agreed,
550 negotiated_at: Instant::now(),
551 negotiation_ms: elapsed_ms,
552 };
553
554 assert_eq!(record.agreed, Some(CodecId::LZ4));
555 assert_eq!(record.local_offered.len(), 2);
556 assert_eq!(record.remote_offered.len(), 1);
557 }
558
559 #[test]
562 fn test_speed_class_ordering() {
563 assert!(SpeedClass::VeryFast < SpeedClass::Fast);
564 assert!(SpeedClass::Fast < SpeedClass::Balanced);
565 assert!(SpeedClass::Balanced < SpeedClass::Slow);
566 assert!(SpeedClass::Slow < SpeedClass::VerySlow);
567 }
568
569 #[test]
572 fn test_best_for_speed_custom_only_slow() {
573 let mut r = CodecRegistry::new();
574 let custom = CodecDescriptor {
577 id: CodecId::from(2000),
578 name: "super_slow".to_string(),
579 compression_ratio_estimate: 0.10,
580 speed_class: SpeedClass::VerySlow,
581 is_lossless: true,
582 };
583 r.register(custom).expect("should register");
584 let best = r.best_for_speed().expect("non-empty");
585 assert_eq!(best.speed_class, SpeedClass::VeryFast);
586 }
587
588 #[test]
591 fn test_unknown_codec_error_display() {
592 let err = CodecError::UnknownCodec(42);
593 assert!(err.to_string().contains("42"));
594 }
595
596 #[test]
599 fn test_default_matches_new() {
600 let a = CodecRegistry::new();
601 let b = CodecRegistry::default();
602 assert_eq!(a.list_all().len(), b.list_all().len());
603 }
604}