tatara_pkgs/set.rs
1//! `PackageSet` — the trait every package universe implements.
2
3use thiserror::Error;
4
5use tatara_nix::derivation::Derivation;
6
7#[derive(Debug, Error)]
8pub enum PackageSetError {
9 #[error("unknown package: {0}")]
10 Unknown(String),
11
12 #[error("backend: {0}")]
13 Backend(String),
14}
15
16/// Lookup result — either the derivation, or "I don't have it" (not an error).
17pub type PackageLookup = Option<Derivation>;
18
19/// A queryable set of packages. Implementations include:
20/// - `NixpkgsBridge` — resolves names to nixpkgs attributes on disk
21/// - `LispPackageSet` — tatara-lisp-authored derivations (future)
22/// - `OverlayPackageSet` — one set composed over another
23pub trait PackageSet: Send + Sync {
24 /// Retrieve the derivation for a named package. `None` if not present.
25 fn get(&self, name: &str) -> Result<PackageLookup, PackageSetError>;
26
27 /// Short list of package names this set knows about. May be a finite
28 /// enumeration (`LispPackageSet`) or a caller-provided seed
29 /// (`NixpkgsBridge` doesn't enumerate nixpkgs by default — too large).
30 fn names(&self) -> Vec<String>;
31
32 /// Optional provenance label for logging / error messages.
33 fn label(&self) -> &str {
34 "anonymous"
35 }
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn error_display_unknown() {
44 // PackageSetError::Unknown bubbles up to user-visible messages
45 // ("package X doesn't exist anywhere"). Pin the Display form
46 // so a rewording drifts tests first, not log dashboards.
47 let e = PackageSetError::Unknown("curl".into());
48 assert_eq!(e.to_string(), "unknown package: curl");
49 }
50
51 #[test]
52 fn error_display_backend() {
53 // Backend errors wrap arbitrary upstream messages — the
54 // prefix ("backend: ") is the only load-bearing part.
55 let e = PackageSetError::Backend("nix eval timed out".into());
56 assert_eq!(e.to_string(), "backend: nix eval timed out");
57 }
58
59 #[test]
60 fn default_label_is_anonymous() {
61 // Minimal PackageSet impl — only `get` and `names` are
62 // mandatory; `label` defaults. If someone drops the default
63 // body, every PackageSet not overriding label() stops
64 // compiling — pin the default so a forced-override refactor
65 // fails this test first.
66 struct Empty;
67 impl PackageSet for Empty {
68 fn get(&self, _name: &str) -> Result<PackageLookup, PackageSetError> {
69 Ok(None)
70 }
71 fn names(&self) -> Vec<String> {
72 vec![]
73 }
74 }
75 assert_eq!(Empty.label(), "anonymous");
76 }
77
78 #[test]
79 fn custom_label_override_wins() {
80 struct Labeled;
81 impl PackageSet for Labeled {
82 fn get(&self, _name: &str) -> Result<PackageLookup, PackageSetError> {
83 Ok(None)
84 }
85 fn names(&self) -> Vec<String> {
86 vec![]
87 }
88 fn label(&self) -> &str {
89 "custom-label"
90 }
91 }
92 assert_eq!(Labeled.label(), "custom-label");
93 }
94
95 #[test]
96 fn package_lookup_is_option_derivation_alias() {
97 // PackageLookup is `Option<Derivation>`. Callers rely on this
98 // to pattern-match Some/None without importing Derivation.
99 // If the alias drifts to `Option<Result<Derivation, _>>` or
100 // similar, every call site breaks — pin the shape via None.
101 let none: PackageLookup = None;
102 assert!(none.is_none());
103 }
104}