Skip to main content

atelier_sdk_diff/
package.rs

1use std::fmt;
2
3use thiserror::Error;
4
5use crate::model::Delta;
6
7/// The identity of a format package: its name plus semver version.
8///
9/// Determinism is contract (ADR-0003): outputs carry this id and caches key
10/// on it, so a version bump is a new projection.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct PackageId {
13    /// The package's name, unique across the registry.
14    pub name: &'static str,
15    /// The package's semver version; a bump is a new projection.
16    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/// A deterministic text rendering of a document, stamped with the package
26/// that produced it. The original document is always kept.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Projection {
29    /// The package that produced the text.
30    pub package: PackageId,
31    /// The deterministic text rendering.
32    pub text: String,
33}
34
35/// Why a package could not handle a document it detected.
36#[derive(Debug, Error)]
37#[error("{package}: {reason}")]
38pub struct PackageError {
39    /// The package that refused.
40    pub package: PackageId,
41    /// Why it refused, in the package's own words.
42    pub reason: String,
43}
44
45/// How strongly a package claims a document.
46///
47/// When several packages claim the same document, the highest confidence
48/// wins and ties break by package id — selection never depends on registry
49/// construction order.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
51pub enum Confidence {
52    /// The path matches the format's naming convention; the content was
53    /// not examined.
54    Extension,
55    /// The content itself was verified: magic bytes or structure.
56    Content,
57}
58
59/// One format's support, shipped as its own versioned unit: the ecosystem's
60/// public ABI (ADR-0003).
61///
62/// A package never gates a diff — when it is absent or fails, fidelity drops
63/// to the text or binary rung instead. Determinism is contract: the same
64/// bytes under the same package version must produce the same projection
65/// and the same diff. `Send + Sync` is part of the ABI: a workspace crosses
66/// threads (a watcher, a server), so its packages must too.
67pub trait FormatPackage: Send + Sync {
68    /// The package's stable identity.
69    fn id(&self) -> PackageId;
70
71    /// How confidently this package claims the document at `path` with
72    /// these bytes; `None` when it does not handle the document.
73    fn detect(&self, path: &str, bytes: &[u8]) -> Option<Confidence>;
74
75    /// Render the document to its text projection.
76    fn project(&self, bytes: &[u8]) -> Result<Projection, PackageError>;
77
78    /// The rich diff in the format's own terms, or `None` while the package
79    /// ships no differ — the ladder then falls back to projected text.
80    fn diff(&self, _before: &[u8], _after: &[u8]) -> Option<Result<Vec<Delta>, PackageError>> {
81        None
82    }
83}
84
85/// The package that claims the document most confidently; equal claims
86/// break by package id, so selection is deterministic whatever order the
87/// registry was built in.
88#[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
112/// The tie-break order between equally confident packages: by name, then
113/// by version.
114fn 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}