diff --git a/libnest/src/cache/available/mod.rs b/libnest/src/cache/available/mod.rs index f057efb..a202609 100644 --- a/libnest/src/cache/available/mod.rs +++ b/libnest/src/cache/available/mod.rs @@ -18,7 +18,7 @@ use failure::{Error, ResultExt}; use serde_json; use crate::lock_file::LockFileOwnership; -use crate::package::{PackageManifest, PackageRequirement}; +use crate::package::{PackageManifest, SoftPackageRequirement}; use crate::repository::Repository; /// Structure representing the cache of available packages @@ -87,7 +87,7 @@ impl<'cache_root, 'lock_file> AvailablePackages<'cache_root, 'lock_file> { #[inline] pub fn query<'pkg_req>( &self, - requirement: &'pkg_req PackageRequirement, + requirement: &'pkg_req SoftPackageRequirement, ) -> AvailablePackagesCacheQuery<'cache_root, 'pkg_req> { AvailablePackagesCacheQuery::from(&self.cache_root, requirement) } diff --git a/libnest/src/cache/available/query.rs b/libnest/src/cache/available/query.rs index c716998..115c079 100644 --- a/libnest/src/cache/available/query.rs +++ b/libnest/src/cache/available/query.rs @@ -4,8 +4,8 @@ use std::path::Path; use failure::{Error, ResultExt}; use crate::package::{ - CategoryName, Manifest, PackageFullName, PackageID, PackageManifest, PackageRequirement, - RepositoryName, + CategoryName, Manifest, PackageFullName, PackageID, PackageManifest, RepositoryName, + SoftPackageRequirement, }; /// The result of a query to the packages cache @@ -76,7 +76,7 @@ pub enum AvailablePackagesCacheQueryStrategy { #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct AvailablePackagesCacheQuery<'a, 'b> { cache_root: &'a Path, - requirement: &'b PackageRequirement, + requirement: &'b SoftPackageRequirement, strategy: AvailablePackagesCacheQueryStrategy, } @@ -84,7 +84,7 @@ impl<'a, 'b> AvailablePackagesCacheQuery<'a, 'b> { #[inline] pub(crate) fn from( cache_root: &'a Path, - requirement: &'b PackageRequirement, + requirement: &'b SoftPackageRequirement, ) -> AvailablePackagesCacheQuery<'a, 'b> { AvailablePackagesCacheQuery { cache_root, diff --git a/libnest/src/package/error.rs b/libnest/src/package/error.rs index f1e8ea9..300f5f1 100644 --- a/libnest/src/package/error.rs +++ b/libnest/src/package/error.rs @@ -92,13 +92,51 @@ pub enum PackageShortNameParseErrorKind { use_as_error!(PackageShortNameParseError, PackageShortNameParseErrorKind); -/// Type for errors related to the parsing of a [`PackageRequirement`] +/// Type for errors related to the parsing of a [`SoftPackageRequirement`] +#[derive(Debug)] +pub struct SoftPackageRequirementParseError { + inner: Context, +} + +/// Type describing a kind of error related to the parsing of a [`SoftPackageRequirement`] +#[derive(Clone, Eq, PartialEq, Hash, Debug, Fail)] +pub enum SoftPackageRequirementParseErrorKind { + /// The given string does not follow the format for package requirements + #[fail( + display = "\"{}\" doesn't follow the `repository::category/name#version` format", + _0 + )] + InvalidFormat(String), + + /// The name component of the package requirement has invalid characters + #[fail(display = "{}", _0)] + InvalidName(#[cause] PackageNameParseError), + + /// The category component of the package requirement has invalid characters + #[fail(display = "{}", _0)] + InvalidCategory(#[cause] CategoryNameParseError), + + /// The repository component of the package requirement has invalid characters + #[fail(display = "{}", _0)] + InvalidRepository(#[cause] RepositoryNameParseError), + + /// The version component of the package requirement is not a valid version + #[fail(display = "invalid version syntax")] + InvalidVersion, +} + +use_as_error!( + SoftPackageRequirementParseError, + SoftPackageRequirementParseErrorKind +); + +/// Type for errors related to the parsing of a [`SoftPackageRequirement`] #[derive(Debug)] pub struct PackageRequirementParseError { inner: Context, } -/// Type describing a kind of error related to the parsing of a [`PackageRequirement`] +/// Type describing a kind of error related to the parsing of a [`SoftPackageRequirement`] #[derive(Clone, Eq, PartialEq, Hash, Debug, Fail)] pub enum PackageRequirementParseErrorKind { /// The given string does not follow the format for package requirements diff --git a/libnest/src/package/mod.rs b/libnest/src/package/mod.rs index 00d1085..19c6089 100644 --- a/libnest/src/package/mod.rs +++ b/libnest/src/package/mod.rs @@ -82,7 +82,7 @@ pub use identification::{ pub use manifest::{Kind, Manifest, PackageManifest, VersionData}; pub use metadata::{License, Maintainer, Metadata, Tag, UpstreamURL}; pub use npf::{NPFExplorer, NPFFile}; -pub use requirement::{HardPackageRequirement, PackageRequirement}; +pub use requirement::{HardPackageRequirement, PackageRequirement, SoftPackageRequirement}; lazy_static::lazy_static! { /// A regular expression to match and parse a package's string representation diff --git a/libnest/src/package/requirement.rs b/libnest/src/package/requirement.rs index 0eb8172..cc83baa 100644 --- a/libnest/src/package/requirement.rs +++ b/libnest/src/package/requirement.rs @@ -9,26 +9,26 @@ use super::identification::{PackageFullName, PackageID}; use super::REGEX_PACKAGE_ID; use super::{CategoryName, PackageName, RepositoryName}; -/// A structure representing a package requirement: parts of a package name and a +/// A structure representing a soft package requirement: parts of a package name and a /// version requirement. /// -/// Each part may be optional except the package name (you can match, for exemple, any +/// Each part may be optional except the package name (you can match, for example, any /// package named 'gcc' in any category in any repository) /// /// The version requirement follows SemVer v2 #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct PackageRequirement { +pub struct SoftPackageRequirement { repository: Option, category: Option, name: PackageName, version_requirement: VersionReq, } -impl PackageRequirement { +impl SoftPackageRequirement { /// Creates a package requirement that matches the given [`PackageFullName`] and version requirement #[inline] - pub fn from(full_name: &PackageFullName, version_req: VersionReq) -> PackageRequirement { - PackageRequirement { + pub fn from(full_name: &PackageFullName, version_req: VersionReq) -> SoftPackageRequirement { + SoftPackageRequirement { repository: Some(full_name.repository().clone()), category: Some(full_name.category().clone()), name: full_name.name().clone(), @@ -38,8 +38,8 @@ impl PackageRequirement { /// Creates a package requirement that matches the given [`PackageFullName`] and version requirement. #[inline] - pub fn from_id(id: &PackageID) -> PackageRequirement { - PackageRequirement { + pub fn from_id(id: &PackageID) -> SoftPackageRequirement { + SoftPackageRequirement { repository: Some(id.repository().clone()), category: Some(id.category().clone()), name: id.name().clone(), @@ -47,7 +47,7 @@ impl PackageRequirement { } } - /// Parses a string into a [`PackageFullName`], or returns a [`PackageRequirementParseError`] + /// Parses a string into a [`SoftPackageRequirement`], or returns a [`PackageRequirementParseError`] /// if the parsing failed. /// /// # Examples @@ -56,24 +56,24 @@ impl PackageRequirement { /// # extern crate libnest; /// # extern crate failure; /// # fn main() -> Result<(), failure::Error> { - /// use libnest::package::{CategoryName, PackageRequirement}; + /// use libnest::package::{CategoryName, SoftPackageRequirement}; /// - /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// let req = SoftPackageRequirement::parse("sys-bin/coreutils#^1.0")?; /// assert!(req.repository().is_none()); /// assert_eq!(*req.category(), Some(CategoryName::parse("sys-bin")?)); /// assert_eq!(req.name().as_str(), "coreutils"); /// assert_eq!(req.version_requirement().to_string(), "^1.0"); /// - /// assert!(PackageRequirement::parse("sys-bin/coreutils#not_a_version").is_err()); + /// assert!(SoftPackageRequirement::parse("sys-bin/coreutils#not_a_version").is_err()); /// # Ok(()) /// # } /// ``` #[inline] - pub fn parse(repr: &str) -> Result { + pub fn parse(repr: &str) -> Result { let matches = REGEX_PACKAGE_ID .captures(repr) .ok_or_else(|| Context::from(repr.to_string())) - .context(PackageRequirementParseErrorKind::InvalidFormat( + .context(SoftPackageRequirementParseErrorKind::InvalidFormat( repr.to_string(), ))?; @@ -81,33 +81,31 @@ impl PackageRequirement { if let Some(req) = matches.name("version") { VersionReq::parse(req.as_str()) .context(repr.to_string()) - .context(PackageRequirementParseErrorKind::InvalidVersion)? + .context(SoftPackageRequirementParseErrorKind::InvalidVersion)? } else { VersionReq::any() } }; let repository = if let Some(repository) = matches.name("repository") { - Some(RepositoryName::parse(repository.as_str()).or_else(|_| { - Err(PackageRequirementParseErrorKind::InvalidRepository( - RepositoryNameParseError(repository.as_str().to_string()), - )) - })?) + Some( + RepositoryName::parse(repository.as_str()) + .map_err(SoftPackageRequirementParseErrorKind::InvalidRepository)?, + ) } else { None }; let category = if let Some(category) = matches.name("category") { - Some(CategoryName::parse(category.as_str()).or_else(|_| { - Err(PackageRequirementParseErrorKind::InvalidCategory( - CategoryNameParseError(category.as_str().to_string()), - )) - })?) + Some( + CategoryName::parse(category.as_str()) + .map_err(SoftPackageRequirementParseErrorKind::InvalidCategory)?, + ) } else { None }; - Ok(PackageRequirement { + Ok(SoftPackageRequirement { repository, category, name: PackageName::parse(matches.name("package").unwrap().as_str())?, @@ -155,9 +153,9 @@ impl PackageRequirement { /// # extern crate libnest; /// # extern crate failure; /// # fn main() -> Result<(), failure::Error> { - /// use libnest::package::{PackageID, PackageRequirement}; + /// use libnest::package::{PackageID, SoftPackageRequirement}; /// - /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// let req = SoftPackageRequirement::parse("sys-bin/coreutils#^1.0")?; /// let id = PackageID::parse("stable::sys-bin/coreutils#1.0.1").unwrap(); /// assert!(req.matches(&id)); /// # Ok(()) @@ -185,9 +183,9 @@ impl PackageRequirement { /// # extern crate libnest; /// # extern crate failure; /// # fn main() -> Result<(), failure::Error> { - /// use libnest::package::{PackageID, PackageRequirement}; + /// use libnest::package::{PackageID, SoftPackageRequirement}; /// - /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// let req = SoftPackageRequirement::parse("sys-bin/coreutils#^1.0")?; /// let id = PackageID::parse("stable::sys-bin/coreutils#1.0.1").unwrap(); /// assert!(req.matches(&id)); /// # Ok(()) @@ -206,7 +204,7 @@ impl PackageRequirement { } } -impl std::fmt::Display for PackageRequirement { +impl std::fmt::Display for SoftPackageRequirement { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { if let Some(repository) = &self.repository { @@ -219,6 +217,213 @@ impl std::fmt::Display for PackageRequirement { } } +/// A structure representing a package requirement: parts of a package name and a +/// version requirement. +/// +/// Each part may be optional except the category and the package name (you can match, +/// for exemple, any version of a package named 'sys-bin/gcc' in any repository) +/// +/// The version requirement follows SemVer v2 +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct PackageRequirement { + repository: Option, + category: CategoryName, + name: PackageName, + version_requirement: VersionReq, +} + +impl PackageRequirement { + /// Creates a package requirement that matches the given [`PackageFullName`] and version requirement + #[inline] + pub fn from(full_name: &PackageFullName, version_req: VersionReq) -> PackageRequirement { + PackageRequirement { + repository: Some(full_name.repository().clone()), + category: full_name.category().clone(), + name: full_name.name().clone(), + version_requirement: version_req, + } + } + + /// Creates a package requirement that matches the given [`PackageFullName`] and version requirement. + #[inline] + pub fn from_id(id: &PackageID) -> PackageRequirement { + PackageRequirement { + repository: Some(id.repository().clone()), + category: id.category().clone(), + name: id.name().clone(), + version_requirement: VersionReq::exact(id.version()), + } + } + + /// Parses a string into a [`PackageRequirement`], or returns a [`PackageRequirementParseError`] + /// if the parsing failed. + /// + /// # Examples + /// + /// ``` + /// # extern crate libnest; + /// # extern crate failure; + /// # fn main() -> Result<(), failure::Error> { + /// use libnest::package::{CategoryName, PackageRequirement}; + /// + /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// assert!(req.repository().is_none()); + /// assert_eq!(*req.category(), CategoryName::parse("sys-bin")?); + /// assert_eq!(req.name().as_str(), "coreutils"); + /// assert_eq!(req.version_requirement().to_string(), "^1.0"); + /// + /// assert!(PackageRequirement::parse("sys-bin/coreutils#not_a_version").is_err()); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn parse(repr: &str) -> Result { + let matches = REGEX_PACKAGE_ID + .captures(repr) + .ok_or_else(|| Context::from(repr.to_string())) + .context(PackageRequirementParseErrorKind::InvalidFormat( + repr.to_string(), + ))?; + + match (matches.name("category"), matches.name("package")) { + (Some(category), Some(package)) => { + let version_req = { + if let Some(req) = matches.name("version") { + VersionReq::parse(req.as_str()) + .context(repr.to_string()) + .context(PackageRequirementParseErrorKind::InvalidVersion)? + } else { + VersionReq::any() + } + }; + + let repository = if let Some(repository) = matches.name("repository") { + Some( + RepositoryName::parse(repository.as_str()) + .map_err(PackageRequirementParseErrorKind::InvalidRepository)?, + ) + } else { + None + }; + + let category = CategoryName::parse(category.as_str()) + .map_err(PackageRequirementParseErrorKind::InvalidCategory)?; + let name = PackageName::parse(package.as_str()) + .map_err(PackageRequirementParseErrorKind::InvalidName)?; + + Ok(PackageRequirement { + repository, + category, + name, + version_requirement: version_req, + }) + } + _ => Err(PackageRequirementParseErrorKind::InvalidFormat(repr.to_string()).into()), + } + } + + /// Changes the version requirement to match any version + #[inline] + pub fn any_version(mut self) -> Self { + self.version_requirement = VersionReq::any(); + self + } + + /// Returns an [`Option`] over the repository part of this package requirement + #[inline] + pub fn repository(&self) -> &Option { + &self.repository + } + + /// Returns an [`Option`] over the category part of this package requirement + #[inline] + pub fn category(&self) -> &CategoryName { + &self.category + } + + /// Returns the package name that the target package must have + #[inline] + pub fn name(&self) -> &PackageName { + &self.name + } + + /// Returns the version requirement that the target package's version must match + #[inline] + pub fn version_requirement(&self) -> &VersionReq { + &self.version_requirement + } + + /// Tests if a given [`PackageID`] matches this package requirement, matching the name imprecisely + /// The name of the package only needs to contain the name of the requirement to match + /// + /// # Examples + /// + /// ``` + /// # extern crate libnest; + /// # extern crate failure; + /// # fn main() -> Result<(), failure::Error> { + /// use libnest::package::{PackageID, PackageRequirement}; + /// + /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// let id = PackageID::parse("stable::sys-bin/coreutils#1.0.1").unwrap(); + /// assert!(req.matches(&id)); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn matches(&self, id: &PackageID) -> bool { + let mut out = true; + if let Some(repository) = &self.repository { + out &= repository == id.repository(); + } + out && (&self.category == id.category()) + && (id.name().contains(self.name.as_ref())) + && (self.version_requirement.matches(id.version())) + } + + /// Tests if a given [`PackageID`] matches this package requirement, matching the name precisely + /// The name of the package needs to be exactly equal to the name of the requirement to match + /// + /// # Examples + /// + /// ``` + /// # extern crate libnest; + /// # extern crate failure; + /// # fn main() -> Result<(), failure::Error> { + /// use libnest::package::{PackageID, PackageRequirement}; + /// + /// let req = PackageRequirement::parse("sys-bin/coreutils#^1.0")?; + /// let id = PackageID::parse("stable::sys-bin/coreutils#1.0.1").unwrap(); + /// assert!(req.matches(&id)); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn matches_precisely(&self, id: &PackageID) -> bool { + let mut out = true; + if let Some(repository) = &self.repository { + out &= repository == id.repository(); + } + out && (&self.category == id.category()) + && (id.name() == &self.name) + && (self.version_requirement.matches(id.version())) + } +} + +impl std::fmt::Display for PackageRequirement { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(repository) = &self.repository { + write!(f, "{}::", repository)?; + } + write!( + f, + "{}/{}#{}", + self.category, self.name, self.version_requirement + ) + } +} + /// A structure represenging a hard package requirement. /// This type of requirement is said to be "hard", because only the version has yet to be selected. /// The other parts of the package information (repository, category and name) are already known. @@ -263,8 +468,8 @@ impl std::fmt::Display for HardPackageRequirement { } } -impl std::convert::Into for HardPackageRequirement { - fn into(self) -> PackageRequirement { - PackageRequirement::from(&self.full_name, self.version_requirement) +impl std::convert::Into for HardPackageRequirement { + fn into(self) -> SoftPackageRequirement { + SoftPackageRequirement::from(&self.full_name, self.version_requirement) } } diff --git a/nest-cli/src/bin/commands/install.rs b/nest-cli/src/bin/commands/install.rs index 6825944..78af523 100644 --- a/nest-cli/src/bin/commands/install.rs +++ b/nest-cli/src/bin/commands/install.rs @@ -3,7 +3,7 @@ use failure::{format_err, Error}; use libnest::cache::available::AvailablePackagesCacheQueryStrategy; use libnest::cache::depgraph::{DependencyGraphDiff, RequirementKind, RequirementManagementMethod}; use libnest::config::Config; -use libnest::package::{HardPackageRequirement, PackageRequirement}; +use libnest::package::{HardPackageRequirement, SoftPackageRequirement}; use libnest::transaction::Transaction; use super::operations::download::download_packages; @@ -19,7 +19,7 @@ pub fn install(config: &Config, matches: &ArgMatches) -> Result<(), Error> { let packages_cache = config.available_packages_cache(&lock_file_ownership); for target in &matches.values_of_lossy("PACKAGE").unwrap() { - let requirement = PackageRequirement::parse(&target)?; + let requirement = SoftPackageRequirement::parse(&target)?; let matched_packages = packages_cache .query(&requirement) diff --git a/nest-cli/src/bin/commands/requirement.rs b/nest-cli/src/bin/commands/requirement.rs index c3dccff..801f038 100644 --- a/nest-cli/src/bin/commands/requirement.rs +++ b/nest-cli/src/bin/commands/requirement.rs @@ -5,7 +5,7 @@ use failure::{format_err, Error}; use libnest::cache::available::AvailablePackagesCacheQueryStrategy; use libnest::cache::depgraph::{GroupName, RequirementKind, RequirementManagementMethod}; use libnest::config::Config; -use libnest::package::{HardPackageRequirement, PackageRequirement}; +use libnest::package::{HardPackageRequirement, SoftPackageRequirement}; pub fn requirement_add( config: &Config, @@ -29,7 +29,7 @@ pub fn requirement_add( let packages_cache = config.available_packages_cache(&lock_file_ownership); for target in &matches.values_of_lossy("PACKAGE").unwrap() { - let requirement = PackageRequirement::parse(&target)?; + let requirement = SoftPackageRequirement::parse(&target)?; let matched_packages = packages_cache .query(&requirement) @@ -92,7 +92,7 @@ pub fn requirement_remove( let packages_cache = config.available_packages_cache(&lock_file_ownership); for target in &matches.values_of_lossy("PACKAGE").unwrap() { - let requirement = PackageRequirement::parse(&target)?; + let requirement = SoftPackageRequirement::parse(&target)?; let matches = packages_cache.query(&requirement).perform()?; diff --git a/nest-cli/src/bin/commands/uninstall.rs b/nest-cli/src/bin/commands/uninstall.rs index 65e4319..2c7ec62 100644 --- a/nest-cli/src/bin/commands/uninstall.rs +++ b/nest-cli/src/bin/commands/uninstall.rs @@ -2,7 +2,7 @@ use clap::ArgMatches; use failure::{format_err, Error}; use libnest::cache::depgraph::{DependencyGraphDiff, RequirementKind}; use libnest::config::Config; -use libnest::package::PackageRequirement; +use libnest::package::SoftPackageRequirement; use super::{ask_confirmation, print_transactions, process_transactions}; @@ -16,7 +16,7 @@ pub fn uninstall(config: &Config, matches: &ArgMatches) -> Result<(), Error> { let packages_cache = config.available_packages_cache(&lock_file_ownership); for target in &matches.values_of_lossy("PACKAGE").unwrap() { - let requirement = PackageRequirement::parse(&target)?; + let requirement = SoftPackageRequirement::parse(&target)?; let matches = packages_cache.query(&requirement).perform()?;