Skip to main content

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}