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