lux_lib/remote_package_db/
mod.rs

1use std::collections::HashMap;
2
3use crate::{
4    config::{Config, ConfigError},
5    lockfile::{LocalPackageLock, LockfileIntegrityError},
6    manifest::{Manifest, ManifestError},
7    package::{
8        PackageName, PackageReq, PackageSpec, PackageVersion, RemotePackage,
9        RemotePackageTypeFilterSpec,
10    },
11    progress::{Progress, ProgressBar},
12};
13use itertools::Itertools;
14use mlua::{FromLua, UserData};
15use thiserror::Error;
16
17#[derive(Clone, FromLua, Debug)]
18pub struct RemotePackageDB(Impl);
19
20#[derive(Clone, Debug)]
21enum Impl {
22    LuarocksManifests(Vec<Manifest>),
23    Lock(LocalPackageLock),
24}
25
26#[derive(Error, Debug)]
27pub enum RemotePackageDBError {
28    #[error(transparent)]
29    ManifestError(#[from] ManifestError),
30    #[error(transparent)]
31    ConfigError(#[from] ConfigError),
32}
33
34#[derive(Error, Debug)]
35pub enum SearchError {
36    #[error(transparent)]
37    Mlua(#[from] mlua::Error),
38    #[error("no rock that matches '{0}' found")]
39    RockNotFound(PackageReq),
40    #[error("no rock that matches '{0}' found in the lockfile.")]
41    RockNotFoundInLockfile(PackageReq),
42    #[error("error when pulling manifest: {0}")]
43    Manifest(#[from] ManifestError),
44}
45
46#[derive(Error, Debug)]
47pub enum RemotePackageDbIntegrityError {
48    #[error(transparent)]
49    Lockfile(#[from] LockfileIntegrityError),
50}
51
52impl RemotePackageDB {
53    pub async fn from_config(
54        config: &Config,
55        progress: &Progress<ProgressBar>,
56    ) -> Result<Self, RemotePackageDBError> {
57        let mut manifests = Vec::new();
58        for server in config.enabled_dev_servers()? {
59            let manifest = Manifest::from_config(server, config, progress).await?;
60            manifests.push(manifest);
61        }
62        for server in config.extra_servers() {
63            let manifest = Manifest::from_config(server.clone(), config, progress).await?;
64            manifests.push(manifest);
65        }
66        manifests.push(Manifest::from_config(config.server().clone(), config, progress).await?);
67        Ok(Self(Impl::LuarocksManifests(manifests)))
68    }
69
70    /// Find a remote package that matches the requirement, returning the latest match.
71    pub(crate) fn find(
72        &self,
73        package_req: &PackageReq,
74        filter: Option<RemotePackageTypeFilterSpec>,
75        progress: &Progress<ProgressBar>,
76    ) -> Result<RemotePackage, SearchError> {
77        match &self.0 {
78            Impl::LuarocksManifests(manifests) => match manifests.iter().find_map(|manifest| {
79                progress.map(|p| p.set_message(format!("🔎 Searching {}", &manifest.server_url())));
80                manifest.find(package_req, filter.clone())
81            }) {
82                Some(package) => Ok(package),
83                None => Err(SearchError::RockNotFound(package_req.clone())),
84            },
85            Impl::Lock(lockfile) => {
86                match lockfile.has_rock(package_req, filter).map(|local_package| {
87                    RemotePackage::new(
88                        PackageSpec::new(local_package.spec.name, local_package.spec.version),
89                        local_package.source,
90                        local_package.source_url,
91                    )
92                }) {
93                    Some(package) => Ok(package),
94                    None => Err(SearchError::RockNotFoundInLockfile(package_req.clone())),
95                }
96            }
97        }
98    }
99
100    /// Search for all packages that match the requirement.
101    pub fn search(&self, package_req: &PackageReq) -> Vec<(&PackageName, Vec<&PackageVersion>)> {
102        match &self.0 {
103            Impl::LuarocksManifests(manifests) => manifests
104                .iter()
105                .flat_map(|manifest| {
106                    manifest
107                        .metadata()
108                        .repository
109                        .iter()
110                        .filter_map(|(name, elements)| {
111                            if name.to_string().contains(&package_req.name().to_string()) {
112                                Some((
113                                    name,
114                                    elements
115                                        .keys()
116                                        .filter(|version| {
117                                            package_req.version_req().matches(version)
118                                        })
119                                        .sorted_by(|a, b| Ord::cmp(b, a))
120                                        .collect_vec(),
121                                ))
122                            } else {
123                                None
124                            }
125                        })
126                })
127                .collect(),
128            Impl::Lock(lockfile) => lockfile
129                .rocks()
130                .iter()
131                .filter_map(|(_, package)| {
132                    // NOTE: This doesn't group packages by name, but we don't care for now,
133                    // as we shouldn't need to use this function with a lockfile.
134                    let name = package.name();
135                    if name.to_string().contains(&package_req.name().to_string()) {
136                        Some((name, vec![package.version()]))
137                    } else {
138                        None
139                    }
140                })
141                .collect_vec(),
142        }
143    }
144
145    /// Find the latest version for a package by name.
146    pub(crate) fn latest_version(&self, rock_name: &PackageName) -> Option<PackageVersion> {
147        self.latest_match(&rock_name.clone().into(), None)
148            .map(|result| result.version().clone())
149    }
150
151    /// Find the latest package that matches the requirement.
152    pub fn latest_match(
153        &self,
154        package_req: &PackageReq,
155        filter: Option<RemotePackageTypeFilterSpec>,
156    ) -> Option<PackageSpec> {
157        match self.find(package_req, filter, &Progress::NoProgress) {
158            Ok(result) => Some(result.package),
159            Err(_) => None,
160        }
161    }
162}
163
164impl UserData for RemotePackageDB {
165    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
166        methods.add_method("search", |_, this, package_req: PackageReq| {
167            Ok(this
168                .search(&package_req)
169                .into_iter()
170                .map(|(package_name, versions)| {
171                    (
172                        package_name.clone(),
173                        versions.into_iter().cloned().collect_vec(),
174                    )
175                })
176                .collect::<HashMap<_, _>>())
177        });
178        methods.add_method("latest_match", |_, this, package_req| {
179            Ok(this.latest_match(&package_req, None))
180        });
181    }
182}
183
184impl From<Manifest> for RemotePackageDB {
185    fn from(manifest: Manifest) -> Self {
186        Self(Impl::LuarocksManifests(vec![manifest]))
187    }
188}
189
190impl From<LocalPackageLock> for RemotePackageDB {
191    fn from(lock: LocalPackageLock) -> Self {
192        Self(Impl::Lock(lock))
193    }
194}