use std::fmt;
use thiserror::Error;
use crate::model::Delta;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PackageId {
pub name: &'static str,
pub version: &'static str,
}
impl fmt::Display for PackageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.name, self.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Projection {
pub package: PackageId,
pub text: String,
}
#[derive(Debug, Error)]
#[error("{package}: {reason}")]
pub struct PackageError {
pub package: PackageId,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Confidence {
Extension,
Content,
}
pub trait FormatPackage: Send + Sync {
fn id(&self) -> PackageId;
fn detect(&self, path: &str, bytes: &[u8]) -> Option<Confidence>;
fn project(&self, bytes: &[u8]) -> Result<Projection, PackageError>;
fn diff(&self, _before: &[u8], _after: &[u8]) -> Option<Result<Vec<Delta>, PackageError>> {
None
}
}
#[must_use]
pub fn detect_package<'a>(
packages: &'a [Box<dyn FormatPackage>],
path: &str,
bytes: &[u8],
) -> Option<&'a dyn FormatPackage> {
let mut best: Option<(Confidence, &'a dyn FormatPackage)> = None;
for package in packages {
let Some(confidence) = package.detect(path, bytes) else {
continue;
};
let replaces = match best {
None => true,
Some((held, incumbent)) => {
confidence > held || (confidence == held && precedes(package.id(), incumbent.id()))
}
};
if replaces {
best = Some((confidence, package.as_ref()));
}
}
best.map(|(_, package)| package)
}
fn precedes(candidate: PackageId, incumbent: PackageId) -> bool {
(candidate.name, candidate.version) < (incumbent.name, incumbent.version)
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake {
name: &'static str,
extension: &'static str,
confidence: Confidence,
}
impl FormatPackage for Fake {
fn id(&self) -> PackageId {
PackageId {
name: self.name,
version: "1.2.3",
}
}
fn detect(&self, path: &str, _bytes: &[u8]) -> Option<Confidence> {
path.ends_with(self.extension).then_some(self.confidence)
}
fn project(&self, _bytes: &[u8]) -> Result<Projection, PackageError> {
Ok(Projection {
package: self.id(),
text: String::new(),
})
}
}
fn fake(
name: &'static str,
extension: &'static str,
confidence: Confidence,
) -> Box<dyn FormatPackage> {
Box::new(Fake {
name,
extension,
confidence,
})
}
#[test]
fn package_id_displays_name_at_version() {
let id = PackageId {
name: "format-docx",
version: "0.1.0",
};
assert_eq!(id.to_string(), "format-docx@0.1.0");
}
#[test]
fn detection_picks_the_highest_confidence_whatever_the_registry_order() {
let forward: Vec<Box<dyn FormatPackage>> = vec![
fake("by-name", ".a", Confidence::Extension),
fake("by-content", ".a", Confidence::Content),
];
let backward: Vec<Box<dyn FormatPackage>> = vec![
fake("by-content", ".a", Confidence::Content),
fake("by-name", ".a", Confidence::Extension),
];
for packages in [&forward, &backward] {
let found = detect_package(packages, "doc.a", b"").unwrap();
assert_eq!(found.id().name, "by-content");
}
}
#[test]
fn detection_ties_break_by_package_id_whatever_the_registry_order() {
let forward: Vec<Box<dyn FormatPackage>> = vec![
fake("zebra", ".a", Confidence::Content),
fake("aardvark", ".a", Confidence::Content),
];
let backward: Vec<Box<dyn FormatPackage>> = vec![
fake("aardvark", ".a", Confidence::Content),
fake("zebra", ".a", Confidence::Content),
];
for packages in [&forward, &backward] {
let found = detect_package(packages, "doc.a", b"").unwrap();
assert_eq!(found.id().name, "aardvark");
}
}
#[test]
fn detection_yields_none_when_no_package_matches() {
let packages: Vec<Box<dyn FormatPackage>> = vec![fake("first", ".a", Confidence::Content)];
assert!(detect_package(&packages, "doc.z", b"").is_none());
}
#[test]
fn diff_defaults_to_none_until_a_package_ships_a_differ() {
let package = fake("first", ".a", Confidence::Content);
assert!(package.diff(b"before", b"after").is_none());
}
}