loadsmith_registry/lib.rs
1//! Registry abstraction and built-in sources for the loadsmith mod-manager
2//! library.
3//!
4//! This is an internal crate of the [`loadsmith`] workspace. Most consumers
5//! should depend on the `loadsmith` facade crate instead of using this
6//! crate directly.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use loadsmith_registry::{RegistrySet, OfflineRegistry, Package, PackageVersion};
12//! use loadsmith_core::{PackageId, Version, FileUrl};
13//! use std::collections::HashMap;
14//!
15//! // Register an offline (pre-defined) source alongside other registries.
16//! let mut set = RegistrySet::new();
17//!
18//! let pkg = Package::new(
19//! PackageId::new("author-name"),
20//! vec![PackageVersion::new(
21//! Version::new(1, 0, 0),
22//! FileUrl::try_from_url("https://example.com/pkg.zip").unwrap(),
23//! )],
24//! );
25//! let mut packages = HashMap::new();
26//! packages.insert(PackageId::new("author-name"), pkg);
27//! set.add("offline", OfflineRegistry::new(packages));
28//!
29//! let registry = set.get("offline").expect("offline registry should exist");
30//! assert!(format!("{registry:?}").contains("OfflineRegistry"));
31//! ```
32
33use std::{collections::HashMap, fmt::Debug, pin::Pin};
34
35use loadsmith_core::{Checksum, Dependency, FileUrl, PackageId, PackageRef, Version};
36use serde::de::DeserializeOwned;
37
38mod error;
39
40mod registries;
41
42pub use error::{Error, Result};
43pub use registries::local::{self, LocalRegistry};
44pub use registries::offline::{self, OfflineRegistry};
45
46/// A single available version reported by a registry.
47///
48/// Returned by [`Registry::version_info`] to list which versions of a package
49/// the registry can provide.
50#[derive(Debug, Clone)]
51pub struct VersionInfo {
52 /// The concrete version number (e.g. `1.0.0`).
53 pub version: Version,
54}
55
56/// The fully resolved metadata for a specific package version.
57///
58/// Produced by [`Registry::resolve`] and contains all information needed to
59/// download or verify a package: the download URL, optional size and checksum,
60/// and the dependency list.
61#[derive(Debug, Clone)]
62pub struct ResolvedVersion {
63 /// URL (or local file path) where the package archive can be obtained.
64 pub url: FileUrl,
65 /// Uncompressed size of the package archive in bytes, if known.
66 pub size: Option<u64>,
67 /// Cryptographic checksum of the package archive, if the registry provides one.
68 pub checksum: Option<Checksum>,
69 /// Direct dependencies declared by this package version.
70 pub deps: Vec<Dependency>,
71}
72
73/// A source that can list available versions and resolve package references.
74///
75/// Every [`Registry`] implementation must be [`Send`] + [`Sync`] so it can be
76/// shared across asynchronous tasks. The trait provides three operations:
77///
78/// * [`version_info`](Registry::version_info) — list all known versions of a package.
79/// * [`resolve`](Registry::resolve) — turn a [`PackageRef`] into a [`ResolvedVersion`] with a download URL, checksum, and dependencies.
80/// * [`revalidate_checksum`](Registry::revalidate_checksum) — optionally re-compute a checksum from the original source (default: no-op).
81///
82/// # Examples
83///
84/// ```rust
85/// use loadsmith_registry::{OfflineRegistry, Package, PackageVersion, Registry};
86/// use loadsmith_core::{PackageId, Version, FileUrl, PackageRef};
87/// use std::collections::HashMap;
88///
89/// # #[tokio::main]
90/// # async fn main() {
91/// let package = Package::new(
92/// PackageId::new("author-name"),
93/// vec![PackageVersion::new(
94/// Version::new(1, 0, 0),
95/// FileUrl::try_from_url("https://example.com/pkg.zip").unwrap(),
96/// )],
97/// );
98/// let mut packages = HashMap::new();
99/// packages.insert(PackageId::new("author-name"), package);
100/// let registry = OfflineRegistry::new(packages);
101///
102/// let ref_ = PackageRef::new("author-name", Version::new(1, 0, 0));
103/// let resolved = registry.resolve(&ref_, None).await.unwrap();
104/// assert!(resolved.url.to_string().contains("example.com"));
105/// # }
106/// ```
107pub trait Registry: Debug + Send + Sync {
108 /// List all available versions of the package identified by `id`.
109 ///
110 /// Some registries require additional metadata (e.g. a local filesystem path)
111 /// which must be supplied via the `metadata` parameter as a JSON value.
112 fn version_info<'a>(
113 &'a self,
114 id: &'a PackageId,
115 metadata: Option<&'a serde_json::Value>,
116 ) -> Pin<Box<dyn Future<Output = Result<Vec<VersionInfo>>> + 'a>>;
117
118 /// Resolve a package reference into concrete download information.
119 ///
120 /// Returns a [`ResolvedVersion`] containing the download URL, file size,
121 /// checksum, and dependency list for the exact version requested by `ref_`.
122 fn resolve<'a>(
123 &'a self,
124 ref_: &'a PackageRef,
125 metadata: Option<&'a serde_json::Value>,
126 ) -> Pin<Box<dyn Future<Output = Result<ResolvedVersion>> + 'a>>;
127
128 /// Re-compute and return the checksum for the package identified by `ref_`,
129 /// or return `None` if the registry does not support checksum revalidation.
130 ///
131 /// The default implementation returns `Ok(None)`.
132 fn revalidate_checksum<'a>(
133 &'a self,
134 ref_: &'a PackageRef,
135 metadata: Option<&'a serde_json::Value>,
136 ) -> Result<Option<Checksum>> {
137 let _ = (ref_, metadata);
138 Ok(None)
139 }
140}
141
142/// A named collection of [`Registry`] implementations.
143///
144/// Each registry is stored under a string identifier (e.g. `"thunderstore"`,
145/// `"local"`) and can be looked up by name at runtime.
146///
147/// # Examples
148///
149/// ```rust
150/// use loadsmith_registry::{RegistrySet, LocalRegistry};
151///
152/// let mut set = RegistrySet::new();
153/// assert!(set.get("local").is_none());
154///
155/// // Registries can be added and retrieved by name.
156/// set.add("local", LocalRegistry::new());
157/// assert!(set.get("local").is_some());
158/// ```
159#[derive(Debug)]
160pub struct RegistrySet {
161 registries: HashMap<String, Box<dyn Registry>>,
162}
163
164impl Default for RegistrySet {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl RegistrySet {
171 /// Create an empty set of registries.
172 pub fn new() -> Self {
173 Self {
174 registries: HashMap::new(),
175 }
176 }
177
178 /// Register a registry under the given identifier.
179 ///
180 /// If a registry with the same `id` already exists, it is replaced.
181 pub fn add<R: Registry + 'static>(&mut self, id: impl Into<String>, registry: R) {
182 self.registries.insert(id.into(), Box::new(registry));
183 }
184
185 /// Look up a registry by its identifier.
186 ///
187 /// Returns `None` if no registry has been registered under that name.
188 pub fn get(&self, id: &str) -> Option<&dyn Registry> {
189 self.registries.get(id).map(|r| r.as_ref())
190 }
191}
192
193/// Deserialize `metadata` from an optional JSON value, or return `T::default()`
194/// when `metadata` is `None`.
195///
196/// # Examples
197///
198/// ```rust
199/// use loadsmith_registry::read_metadata_or_default;
200/// use serde::Deserialize;
201///
202/// #[derive(Deserialize, Default)]
203/// struct Config {
204/// name: String,
205/// threshold: f64,
206/// }
207///
208/// // When metadata is provided, it is deserialized.
209/// let json = serde_json::json!({"name": "test", "threshold": 0.8});
210/// let cfg: Config = read_metadata_or_default(Some(&json)).unwrap();
211/// assert_eq!(cfg.name, "test");
212///
213/// // When metadata is None, the default is returned.
214/// let cfg: Config = read_metadata_or_default(None).unwrap();
215/// assert_eq!(cfg.name, "");
216/// assert_eq!(cfg.threshold, 0.0);
217/// ```
218pub fn read_metadata_or_default<T: DeserializeOwned + Default>(
219 metadata: Option<&serde_json::Value>,
220) -> Result<T> {
221 match metadata {
222 Some(metadata) => read_metadata_some(metadata),
223 None => Ok(T::default()),
224 }
225}
226
227/// Deserialize `metadata` from an optional JSON value.
228///
229/// Returns [`Error::MissingMetadata`] when `metadata` is `None`.
230///
231/// # Examples
232///
233/// ```rust
234/// use loadsmith_registry::read_metadata;
235/// use serde::Deserialize;
236///
237/// #[derive(Deserialize)]
238/// struct Config {
239/// name: String,
240/// }
241///
242/// let json = serde_json::json!({"name": "hello"});
243/// let cfg: Config = read_metadata(Some(&json)).unwrap();
244/// assert_eq!(cfg.name, "hello");
245///
246/// let err = read_metadata::<Config>(None);
247/// assert!(err.is_err());
248/// ```
249pub fn read_metadata<T: DeserializeOwned>(metadata: Option<&serde_json::Value>) -> Result<T> {
250 match metadata {
251 Some(metadata) => read_metadata_some(metadata),
252 None => Err(Error::MissingMetadata),
253 }
254}
255
256fn read_metadata_some<T: DeserializeOwned>(metadata: &serde_json::Value) -> Result<T> {
257 serde_json::from_value(metadata.clone()).map_err(Error::InvalidMetadata)
258}