Skip to main content

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}