1use std::fmt;
2
3use thiserror::Error;
4
5use crate::model::Delta;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PackageId {
13 pub name: &'static str,
15 pub version: &'static str,
17}
18
19impl fmt::Display for PackageId {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 write!(f, "{}@{}", self.name, self.version)
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Projection {
29 pub package: PackageId,
31 pub text: String,
33}
34
35#[derive(Debug, Error)]
37#[error("{package}: {reason}")]
38pub struct PackageError {
39 pub package: PackageId,
41 pub reason: String,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
51pub enum Confidence {
52 Extension,
55 Content,
57}
58
59pub trait FormatPackage: Send + Sync {
68 fn id(&self) -> PackageId;
70
71 fn detect(&self, path: &str, bytes: &[u8]) -> Option<Confidence>;
74
75 fn project(&self, bytes: &[u8]) -> Result<Projection, PackageError>;
77
78 fn diff(&self, _before: &[u8], _after: &[u8]) -> Option<Result<Vec<Delta>, PackageError>> {
81 None
82 }
83}
84
85#[must_use]
89pub fn detect_package<'a>(
90 packages: &'a [Box<dyn FormatPackage>],
91 path: &str,
92 bytes: &[u8],
93) -> Option<&'a dyn FormatPackage> {
94 let mut best: Option<(Confidence, &'a dyn FormatPackage)> = None;
95 for package in packages {
96 let Some(confidence) = package.detect(path, bytes) else {
97 continue;
98 };
99 let replaces = match best {
100 None => true,
101 Some((held, incumbent)) => {
102 confidence > held || (confidence == held && precedes(package.id(), incumbent.id()))
103 }
104 };
105 if replaces {
106 best = Some((confidence, package.as_ref()));
107 }
108 }
109 best.map(|(_, package)| package)
110}
111
112fn precedes(candidate: PackageId, incumbent: PackageId) -> bool {
115 (candidate.name, candidate.version) < (incumbent.name, incumbent.version)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 struct Fake {
123 name: &'static str,
124 extension: &'static str,
125 confidence: Confidence,
126 }
127
128 impl FormatPackage for Fake {
129 fn id(&self) -> PackageId {
130 PackageId {
131 name: self.name,
132 version: "1.2.3",
133 }
134 }
135
136 fn detect(&self, path: &str, _bytes: &[u8]) -> Option<Confidence> {
137 path.ends_with(self.extension).then_some(self.confidence)
138 }
139
140 fn project(&self, _bytes: &[u8]) -> Result<Projection, PackageError> {
141 Ok(Projection {
142 package: self.id(),
143 text: String::new(),
144 })
145 }
146 }
147
148 fn fake(
149 name: &'static str,
150 extension: &'static str,
151 confidence: Confidence,
152 ) -> Box<dyn FormatPackage> {
153 Box::new(Fake {
154 name,
155 extension,
156 confidence,
157 })
158 }
159
160 #[test]
161 fn package_id_displays_name_at_version() {
162 let id = PackageId {
163 name: "format-docx",
164 version: "0.1.0",
165 };
166 assert_eq!(id.to_string(), "format-docx@0.1.0");
167 }
168
169 #[test]
170 fn detection_picks_the_highest_confidence_whatever_the_registry_order() {
171 let forward: Vec<Box<dyn FormatPackage>> = vec![
172 fake("by-name", ".a", Confidence::Extension),
173 fake("by-content", ".a", Confidence::Content),
174 ];
175 let backward: Vec<Box<dyn FormatPackage>> = vec![
176 fake("by-content", ".a", Confidence::Content),
177 fake("by-name", ".a", Confidence::Extension),
178 ];
179
180 for packages in [&forward, &backward] {
181 let found = detect_package(packages, "doc.a", b"").unwrap();
182 assert_eq!(found.id().name, "by-content");
183 }
184 }
185
186 #[test]
187 fn detection_ties_break_by_package_id_whatever_the_registry_order() {
188 let forward: Vec<Box<dyn FormatPackage>> = vec![
189 fake("zebra", ".a", Confidence::Content),
190 fake("aardvark", ".a", Confidence::Content),
191 ];
192 let backward: Vec<Box<dyn FormatPackage>> = vec![
193 fake("aardvark", ".a", Confidence::Content),
194 fake("zebra", ".a", Confidence::Content),
195 ];
196
197 for packages in [&forward, &backward] {
198 let found = detect_package(packages, "doc.a", b"").unwrap();
199 assert_eq!(found.id().name, "aardvark");
200 }
201 }
202
203 #[test]
204 fn detection_yields_none_when_no_package_matches() {
205 let packages: Vec<Box<dyn FormatPackage>> = vec![fake("first", ".a", Confidence::Content)];
206 assert!(detect_package(&packages, "doc.z", b"").is_none());
207 }
208
209 #[test]
210 fn diff_defaults_to_none_until_a_package_ships_a_differ() {
211 let package = fake("first", ".a", Confidence::Content);
212 assert!(package.diff(b"before", b"after").is_none());
213 }
214}