Skip to main content

lux_lib/package/
mod.rs

1use itertools::Itertools;
2
3use miette::Diagnostic;
4use serde::{de, Deserialize, Deserializer, Serialize};
5use std::{cmp::Ordering, fmt::Display, str::FromStr};
6use thiserror::Error;
7mod outdated;
8mod version;
9#[allow(dead_code)]
10pub(crate) mod wally;
11
12pub use outdated::*;
13pub use version::{
14    PackageVersion, PackageVersionParseError, PackageVersionReq, PackageVersionReqError, SpecRev,
15    VersionReqToVersionError,
16};
17
18pub(crate) use version::SpecRevIterator;
19
20use crate::{
21    lockfile::RemotePackageSourceUrl,
22    lua_rockspec::{DisplayAsLuaKV, DisplayLuaKV, DisplayLuaValue},
23    package::version::HasModRev,
24    remote_package_source::RemotePackageSource,
25    rockspec::lua_dependency::LuaDependencySpec,
26    variables::{GetVariableError, HasVariables},
27};
28
29#[derive(Debug, Error, Diagnostic)]
30pub enum PackageSpecFromPackageReqError {
31    #[error("invalid version for rock {rock}: {err}")]
32    Version {
33        rock: PackageName,
34        err: VersionReqToVersionError,
35    },
36}
37
38/// Specification for a package with an exact name and version
39#[derive(Clone, Debug, PartialEq, Eq)]
40#[cfg_attr(feature = "clap", derive(clap::Args))]
41pub struct PackageSpec {
42    name: PackageName,
43    version: PackageVersion,
44}
45
46impl PackageSpec {
47    pub fn new(name: PackageName, version: PackageVersion) -> Self {
48        Self { name, version }
49    }
50    pub fn parse(name: String, version: String) -> Result<Self, PackageVersionParseError> {
51        Ok(Self::new(
52            PackageName::new(name),
53            PackageVersion::parse(&version)?,
54        ))
55    }
56    pub fn name(&self) -> &PackageName {
57        &self.name
58    }
59    pub fn version(&self) -> &PackageVersion {
60        &self.version
61    }
62    /// Note that the version requirement loses the specrev information.
63    pub fn into_package_req(self) -> PackageReq {
64        PackageReq {
65            name: self.name,
66            version_req: self.version.into_version_req(),
67        }
68    }
69}
70
71impl TryFrom<PackageReq> for PackageSpec {
72    type Error = PackageSpecFromPackageReqError;
73
74    fn try_from(value: PackageReq) -> Result<Self, Self::Error> {
75        let name = value.name;
76        let version = value.version_req.try_into().map_err(|err| {
77            PackageSpecFromPackageReqError::Version {
78                rock: name.clone(),
79                err,
80            }
81        })?;
82        Ok(Self { name, version })
83    }
84}
85
86impl<'de> Deserialize<'de> for PackageSpec {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        let (name, version) = <_>::deserialize(deserializer)?;
92        Self::parse(name, version).map_err(serde::de::Error::custom)
93    }
94}
95
96impl HasVariables for PackageSpec {
97    fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
98        Ok(match input {
99            "PACKAGE" => Some(self.name.to_string()),
100            "VERSION" => Some(self.version.to_modrev_string()),
101            _ => None,
102        })
103    }
104}
105
106#[derive(Clone, Debug)]
107pub(crate) struct RemotePackage {
108    pub spec: PackageSpec,
109    pub source: RemotePackageSource,
110    /// `Some` if present in a lockfile
111    pub source_url: Option<RemotePackageSourceUrl>,
112}
113
114impl RemotePackage {
115    pub fn new(
116        spec: PackageSpec,
117        source: RemotePackageSource,
118        source_url: Option<RemotePackageSourceUrl>,
119    ) -> Self {
120        Self {
121            spec,
122            source,
123            source_url,
124        }
125    }
126}
127
128#[derive(PartialEq, Eq, Hash, Clone, Debug)]
129pub(crate) enum RemotePackageType {
130    Rockspec,
131    Src,
132    Binary,
133}
134
135impl Ord for RemotePackageType {
136    fn cmp(&self, other: &Self) -> Ordering {
137        // Priority: binary > rockspec > src
138        match (self, other) {
139            (Self::Binary, Self::Binary)
140            | (Self::Rockspec, Self::Rockspec)
141            | (Self::Src, Self::Src) => Ordering::Equal,
142
143            (Self::Binary, _) => Ordering::Greater,
144            (_, Self::Binary) => Ordering::Less,
145            (Self::Rockspec, Self::Src) => Ordering::Greater,
146            (Self::Src, Self::Rockspec) => Ordering::Less,
147        }
148    }
149}
150
151impl PartialOrd for RemotePackageType {
152    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
153        Some(self.cmp(other))
154    }
155}
156
157#[derive(Clone, Serialize, Deserialize, Debug)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159pub struct RemotePackageTypeFilterSpec {
160    /// Include Rockspec
161    pub rockspec: bool,
162    /// Include Src
163    pub src: bool,
164    /// Include Binary
165    pub binary: bool,
166}
167
168impl Default for RemotePackageTypeFilterSpec {
169    fn default() -> Self {
170        Self {
171            rockspec: true,
172            src: true,
173            binary: true,
174        }
175    }
176}
177
178#[derive(Error, Debug, Diagnostic)]
179pub enum ParseRemotePackageError {
180    #[error("unable to parse package {0}. expected format: `name@version`")]
181    InvalidInput(String),
182    #[error("unable to parse package {package_str}:\n{error}")]
183    InvalidPackageVersion {
184        #[source]
185        error: PackageVersionParseError,
186        package_str: String,
187    },
188}
189
190impl FromStr for PackageSpec {
191    type Err = ParseRemotePackageError;
192
193    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
194        let (name, version) = s
195            .split_once('@')
196            .ok_or_else(|| ParseRemotePackageError::InvalidInput(s.to_string()))?;
197
198        Self::parse(name.to_string(), version.to_string()).map_err(|error| {
199            ParseRemotePackageError::InvalidPackageVersion {
200                error,
201                package_str: s.to_string(),
202            }
203        })
204    }
205}
206
207/// A lua package requirement with a name and an optional version requirement.
208#[derive(Debug, Clone, PartialEq, Eq, Hash)]
209#[cfg_attr(feature = "clap", derive(clap::Args))]
210pub struct PackageReq {
211    /// The name of the package.
212    pub(crate) name: PackageName,
213    /// The version requirement, for example "1.0.0" or ">=1.0.0".
214    pub(crate) version_req: PackageVersionReq,
215}
216
217impl PackageReq {
218    pub fn new(name: String, version: Option<String>) -> Result<Self, PackageVersionReqError> {
219        let version_req = match version {
220            Some(version_req_str) => PackageVersionReq::parse(version_req_str.as_str())?,
221            None => PackageVersionReq::any(),
222        };
223        Ok(Self {
224            name: PackageName::new(name),
225            version_req,
226        })
227    }
228
229    /// # Safety
230    ///
231    /// Call this only if you are absolutely sure that a `PackageReq` can be parsed from the arguments.
232    /// Calling this with invalid arguments is undefined behaviour.
233    pub unsafe fn new_unchecked(name: String, version: Option<String>) -> Self {
234        Self::new(name, version).unwrap_unchecked()
235    }
236
237    pub fn parse(pkg_constraints: &str) -> Result<Self, PackageReqParseError> {
238        Self::from_str(pkg_constraints)
239    }
240    pub fn name(&self) -> &PackageName {
241        &self.name
242    }
243    pub fn version_req(&self) -> &PackageVersionReq {
244        &self.version_req
245    }
246    /// Evaluate whether the given package satisfies the package requirement given by `self`.
247    pub fn matches(&self, package: &PackageSpec) -> bool {
248        self.name == package.name && self.version_req.matches(&package.version)
249    }
250}
251
252impl Display for PackageReq {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        if self.version_req.is_any() {
255            self.name.fmt(f)
256        } else {
257            f.write_str(format!("{}{}", self.name, self.version_req).as_str())
258        }
259    }
260}
261
262impl From<PackageSpec> for PackageReq {
263    fn from(value: PackageSpec) -> Self {
264        value.into_package_req()
265    }
266}
267
268impl From<PackageName> for PackageReq {
269    fn from(name: PackageName) -> Self {
270        Self {
271            name,
272            version_req: PackageVersionReq::any(),
273        }
274    }
275}
276
277/// Wrapper structs for proper serialization of various dependency types.
278pub(crate) struct Dependencies<'a>(pub(crate) &'a Vec<LuaDependencySpec>);
279pub(crate) struct BuildDependencies<'a>(pub(crate) &'a Vec<LuaDependencySpec>);
280
281impl DisplayAsLuaKV for Dependencies<'_> {
282    fn display_lua(&self) -> DisplayLuaKV {
283        DisplayLuaKV {
284            key: "dependencies".to_string(),
285            value: DisplayLuaValue::List(
286                self.0
287                    .iter()
288                    .map(|req| DisplayLuaValue::String(req.to_string()))
289                    .collect(),
290            ),
291        }
292    }
293}
294
295impl DisplayAsLuaKV for BuildDependencies<'_> {
296    fn display_lua(&self) -> DisplayLuaKV {
297        DisplayLuaKV {
298            key: "build_dependencies".to_string(),
299            value: DisplayLuaValue::List(
300                self.0
301                    .iter()
302                    .map(|req| DisplayLuaValue::String(req.to_string()))
303                    .collect(),
304            ),
305        }
306    }
307}
308
309#[derive(Error, Debug, Diagnostic)]
310pub enum PackageReqParseError {
311    #[error("could not parse dependency name from '{0}'")]
312    InvalidDependencyName(String),
313    #[error("could not parse version requirement in '{str}': {error}")]
314    InvalidPackageVersionReq {
315        #[source]
316        error: PackageVersionReqError,
317        str: String,
318    },
319}
320
321impl FromStr for PackageReq {
322    type Err = PackageReqParseError;
323
324    fn from_str(str: &str) -> Result<Self, PackageReqParseError> {
325        let rock_name_str = str
326            .chars()
327            .peeking_take_while(|t| t.is_alphanumeric() || matches!(t, '-' | '_' | '.'))
328            .collect::<String>();
329
330        if rock_name_str.is_empty() {
331            return Err(PackageReqParseError::InvalidDependencyName(str.to_string()));
332        }
333
334        let constraints = str.trim_start_matches(&rock_name_str).trim();
335        let version_req = match constraints {
336            "" => PackageVersionReq::any(),
337            constraints => PackageVersionReq::parse(constraints.trim_start()).map_err(|error| {
338                PackageReqParseError::InvalidPackageVersionReq {
339                    error,
340                    str: str.to_string(),
341                }
342            })?,
343        };
344        Ok(Self {
345            name: PackageName::new(rock_name_str),
346            version_req,
347        })
348    }
349}
350
351impl<'de> Deserialize<'de> for PackageReq {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: Deserializer<'de>,
355    {
356        let s = String::deserialize(deserializer)?;
357        Self::from_str(&s).map_err(de::Error::custom)
358    }
359}
360
361/// A luarocks package name, which is always lowercase
362#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
363#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
364#[cfg_attr(feature = "schema", serde(transparent))]
365pub struct PackageName(String);
366
367impl PackageName {
368    pub fn new(name: String) -> Self {
369        Self(name.to_lowercase())
370    }
371}
372
373impl<'de> Deserialize<'de> for PackageName {
374    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
375    where
376        D: serde::Deserializer<'de>,
377    {
378        Ok(PackageName::new(String::deserialize(deserializer)?))
379    }
380}
381
382impl Serialize for PackageName {
383    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
384    where
385        S: serde::Serializer,
386    {
387        self.0.serialize(serializer)
388    }
389}
390
391impl From<&str> for PackageName {
392    fn from(value: &str) -> Self {
393        Self::new(value.into())
394    }
395}
396
397impl Display for PackageName {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.write_str(self.0.as_str())
400    }
401}
402
403#[derive(Debug)]
404pub struct PackageNameList(Vec<PackageName>);
405
406impl PackageNameList {
407    pub(crate) fn new(package_names: Vec<PackageName>) -> Self {
408        Self(package_names)
409    }
410}
411
412impl Display for PackageNameList {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        f.write_str(self.0.iter().map(|pkg| pkg.to_string()).join(", ").as_str())
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[tokio::test]
423    async fn parse_name() {
424        let mut package_name: PackageName = "neorg".into();
425        assert_eq!(package_name.to_string(), "neorg");
426        package_name = "LuaFileSystem".into();
427        assert_eq!(package_name.to_string(), "luafilesystem");
428    }
429
430    #[tokio::test]
431    async fn parse_lua_package() {
432        let neorg = PackageSpec::parse("neorg".into(), "1.0.0".into()).unwrap();
433        let expected_version = PackageVersion::parse("1.0.0").unwrap();
434        assert_eq!(neorg.name().to_string(), "neorg");
435        assert!(matches!(
436            neorg.version().cmp(&expected_version),
437            std::cmp::Ordering::Equal
438        ));
439        let neorg = PackageSpec::parse("neorg".into(), "1.0".into()).unwrap();
440        assert!(matches!(
441            neorg.version().cmp(&expected_version),
442            std::cmp::Ordering::Equal
443        ));
444        let neorg = PackageSpec::parse("neorg".into(), "1".into()).unwrap();
445        assert!(matches!(
446            neorg.version().cmp(&expected_version),
447            std::cmp::Ordering::Equal
448        ));
449    }
450
451    #[tokio::test]
452    async fn parse_lua_package_req() {
453        let mut package_req = PackageReq::new("foo".into(), Some("1.0.0".into())).unwrap();
454        assert!(package_req.matches(&PackageSpec::parse("foo".into(), "1.0.0".into()).unwrap()));
455        assert!(!package_req.matches(&PackageSpec::parse("bar".into(), "1.0.0".into()).unwrap()));
456        assert!(!package_req.matches(&PackageSpec::parse("foo".into(), "2.0.0".into()).unwrap()));
457        package_req = PackageReq::new("foo".into(), Some(">= 1.0.0".into())).unwrap();
458        assert!(package_req.matches(&PackageSpec::parse("foo".into(), "2.0.0".into()).unwrap()));
459        let package_req: PackageReq = "lua >= 5.1".parse().unwrap();
460        assert_eq!(package_req.name.to_string(), "lua");
461        let package_req: PackageReq = "lua>=5.1".parse().unwrap();
462        assert_eq!(package_req.name.to_string(), "lua");
463        let package_req: PackageReq = "toml-edit >= 0.1.0".parse().unwrap();
464        assert_eq!(package_req.name.to_string(), "toml-edit");
465        let package_req: PackageReq = "plugin.nvim >= 0.1.0".parse().unwrap();
466        assert_eq!(package_req.name.to_string(), "plugin.nvim");
467        let package_req: PackageReq = "lfs".parse().unwrap();
468        assert_eq!(package_req.name.to_string(), "lfs");
469        let package_req: PackageReq = "neorg 1.0.0".parse().unwrap();
470        assert_eq!(package_req.name.to_string(), "neorg");
471        let neorg = PackageSpec::parse("neorg".into(), "1.0.0".into()).unwrap();
472        assert!(package_req.matches(&neorg));
473        let neorg = PackageSpec::parse("neorg".into(), "2.0.0".into()).unwrap();
474        assert!(!package_req.matches(&neorg));
475        let package_req: PackageReq = "neorg 2.0.0".parse().unwrap();
476        assert!(package_req.matches(&neorg));
477        let package_req: PackageReq = "neorg = 2.0.0".parse().unwrap();
478        assert!(package_req.matches(&neorg));
479        let package_req: PackageReq = "neorg == 2.0.0".parse().unwrap();
480        assert!(package_req.matches(&neorg));
481        let package_req: PackageReq = "neorg &equals; 2.0.0".parse().unwrap();
482        assert!(package_req.matches(&neorg));
483        let package_req: PackageReq = "neorg >= 1.0, &lt; 2.0".parse().unwrap();
484        let neorg = PackageSpec::parse("neorg".into(), "1.5".into()).unwrap();
485        assert!(package_req.matches(&neorg));
486        let package_req: PackageReq = "neorg &gt; 1.0, &lt; 2.0".parse().unwrap();
487        let neorg = PackageSpec::parse("neorg".into(), "1.11.0".into()).unwrap();
488        assert!(package_req.matches(&neorg));
489        let neorg = PackageSpec::parse("neorg".into(), "3.0.0".into()).unwrap();
490        assert!(!package_req.matches(&neorg));
491        let neorg = PackageSpec::parse("neorg".into(), "0.5".into()).unwrap();
492        assert!(!package_req.matches(&neorg));
493        let package_req: PackageReq = "neorg ~> 1".parse().unwrap();
494        assert!(!package_req.matches(&neorg));
495        let neorg = PackageSpec::parse("neorg".into(), "3".into()).unwrap();
496        assert!(!package_req.matches(&neorg));
497        let neorg = PackageSpec::parse("neorg".into(), "1.5".into()).unwrap();
498        assert!(package_req.matches(&neorg));
499        let package_req: PackageReq = "neorg ~> 1.4".parse().unwrap();
500        let neorg = PackageSpec::parse("neorg".into(), "1.3".into()).unwrap();
501        assert!(!package_req.matches(&neorg));
502        let neorg = PackageSpec::parse("neorg".into(), "1.5".into()).unwrap();
503        assert!(!package_req.matches(&neorg));
504        let neorg = PackageSpec::parse("neorg".into(), "1.4.10".into()).unwrap();
505        assert!(package_req.matches(&neorg));
506        let neorg = PackageSpec::parse("neorg".into(), "1.4".into()).unwrap();
507        assert!(package_req.matches(&neorg));
508        let package_req: PackageReq = "neorg ~> 1.0.5".parse().unwrap();
509        let neorg = PackageSpec::parse("neorg".into(), "1.0.4".into()).unwrap();
510        assert!(!package_req.matches(&neorg));
511        let neorg = PackageSpec::parse("neorg".into(), "1.0.5".into()).unwrap();
512        assert!(package_req.matches(&neorg));
513        let neorg = PackageSpec::parse("neorg".into(), "1.0.6".into()).unwrap();
514        assert!(!package_req.matches(&neorg));
515        // Testing incomplete version constraints
516        let package_req: PackageReq = "lua-utils.nvim ~> 1.1-1".parse().unwrap();
517        let lua_utils = PackageSpec::parse("lua-utils.nvim".into(), "1.1.4".into()).unwrap();
518        assert!(package_req.matches(&lua_utils));
519        let lua_utils = PackageSpec::parse("lua-utils.nvim".into(), "1.1.5".into()).unwrap();
520        assert!(package_req.matches(&lua_utils));
521        let lua_utils = PackageSpec::parse("lua-utils.nvim".into(), "1.2-1".into()).unwrap();
522        assert!(!package_req.matches(&lua_utils));
523    }
524
525    #[tokio::test]
526    pub async fn remote_package_type_priorities() {
527        let rock_types = vec![
528            RemotePackageType::Binary,
529            RemotePackageType::Src,
530            RemotePackageType::Rockspec,
531        ];
532        assert_eq!(
533            rock_types.into_iter().sorted().collect_vec(),
534            vec![
535                RemotePackageType::Src,
536                RemotePackageType::Rockspec,
537                RemotePackageType::Binary,
538            ]
539        );
540    }
541
542    #[tokio::test]
543    async fn package_spec_req_roundtrips() {
544        let spec = PackageSpec::new("foo".into(), "1.0.0-1".parse().unwrap());
545        let req: PackageReq = spec.clone().into();
546        assert_eq!(spec, req.try_into().unwrap());
547
548        let spec = PackageSpec::new("foo".into(), "1.0.0".parse().unwrap());
549        let req: PackageReq = spec.clone().into();
550        assert_eq!(spec, req.try_into().unwrap());
551    }
552
553    #[tokio::test]
554    async fn package_req_spec_roundtrips() {
555        let req: PackageReq = "foo@1.0.0".parse().unwrap();
556        let spec: PackageSpec = req.clone().try_into().unwrap();
557        assert_eq!(req, spec.into());
558
559        let req: PackageReq = "foo@1.0.0-1".parse().unwrap();
560        let spec: PackageSpec = req.clone().try_into().unwrap();
561        assert_eq!(req, spec.into());
562    }
563
564    #[tokio::test]
565    async fn package_spec_parses_same_as_name_version() {
566        let req: PackageReq = "foo@1.0.0".parse().unwrap();
567        let spec: PackageSpec = req.clone().try_into().unwrap();
568        let name: PackageName = "foo".into();
569        let version: PackageVersion = "1.0.0".parse().unwrap();
570        assert_eq!(spec, PackageSpec::new(name, version));
571
572        let req: PackageReq = "foo@1.0.0-1".parse().unwrap();
573        let spec: PackageSpec = req.clone().try_into().unwrap();
574        let name: PackageName = "foo".into();
575        let version: PackageVersion = "1.0.0-1".parse().unwrap();
576        assert_eq!(spec, PackageSpec::new(name, version));
577    }
578}