use std::path::Path;
use async_trait::async_trait;
use toolkit_macros::domain_model;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Confidence(f32);
impl Confidence {
pub const CLAMP_EPSILON: f32 = 1e-4;
#[must_use]
pub fn new(value: f32) -> Option<Self> {
if !(-Self::CLAMP_EPSILON..=1.0 + Self::CLAMP_EPSILON).contains(&value) {
return None;
}
Some(Self(value.clamp(0.0, 1.0)))
}
#[must_use]
pub fn get(self) -> f32 {
self.0
}
}
impl std::fmt::Display for Confidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod confidence_tests {
use super::Confidence;
#[test]
fn in_range_values_pass_through() {
assert!((Confidence::new(0.0).unwrap().get() - 0.0).abs() < f32::EPSILON);
assert!((Confidence::new(0.5).unwrap().get() - 0.5).abs() < f32::EPSILON);
assert!((Confidence::new(1.0).unwrap().get() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn tiny_drift_outside_the_range_is_clamped() {
assert!((Confidence::new(-0.000_01).unwrap().get() - 0.0).abs() < f32::EPSILON);
assert!((Confidence::new(1.000_01).unwrap().get() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn far_out_of_range_values_are_rejected_not_clamped() {
assert!(Confidence::new(5.0).is_none());
assert!(Confidence::new(-5.0).is_none());
}
#[test]
fn nan_is_rejected() {
assert!(Confidence::new(f32::NAN).is_none());
}
}
#[domain_model]
#[derive(Debug, Clone, PartialEq)]
pub struct DetectedType {
pub extension: String,
pub confidence: Confidence,
}
#[async_trait]
pub trait ContentTypeDetector: Send + Sync {
async fn detect(&self, bytes: bytes::Bytes) -> Option<DetectedType>;
async fn detect_path(&self, path: &Path) -> Option<DetectedType>;
}