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