From c9701ee6fd272df910c72b4e780870179a674a76 Mon Sep 17 00:00:00 2001 From: Jessica Black Date: Fri, 17 May 2024 17:28:44 -0700 Subject: [PATCH] Better ordering --- Cargo.toml | 1 + src/fetcher.rs | 131 ------------------ src/lib.rs | 306 +++++++++++++++++++++++++++++++++++++++-- src/locator.rs | 110 ++++++++------- src/locator_package.rs | 69 ++++++---- src/locator_strict.rs | 74 +++++----- 6 files changed, 444 insertions(+), 247 deletions(-) delete mode 100644 src/fetcher.rs diff --git a/Cargo.toml b/Cargo.toml index ed1a146..ca07a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ typed-builder = "0.10.0" utoipa = "4.2.3" serde_json = "1.0.95" documented = "0.4.1" +semver = "1.0.23" [dev-dependencies] assert_matches = "1.5.0" diff --git a/src/fetcher.rs b/src/fetcher.rs deleted file mode 100644 index da081e3..0000000 --- a/src/fetcher.rs +++ /dev/null @@ -1,131 +0,0 @@ -use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, Display, EnumIter, EnumString}; -use utoipa::ToSchema; - -/// [`Locator`](crate::Locator) is closely tied with the concept of Core's "fetchers", -/// which are asynchronous jobs tasked with downloading the code -/// referred to by a [`Locator`](crate::Locator) so that Core or some other service -/// may analyze it. -/// -/// For more information on the background of `Locator` and fetchers generally, -/// refer to [Fetchers and Locators](https://go/fetchers-doc). -#[derive( - Copy, - Clone, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, - Debug, - Display, - EnumString, - EnumIter, - AsRefStr, - Serialize, - Deserialize, - ToSchema, -)] -#[non_exhaustive] -#[serde(rename_all = "snake_case")] -pub enum Fetcher { - /// Archive locators are FOSSA specific. - #[strum(serialize = "archive")] - Archive, - - /// Interacts with Bower. - #[strum(serialize = "bower")] - Bower, - - /// Interacts with Carthage. - #[strum(serialize = "cart")] - Cart, - - /// Interacts with Cargo. - #[strum(serialize = "cargo")] - Cargo, - - /// Interacts with Composer. - #[strum(serialize = "comp")] - Comp, - - /// Interacts with Conan. - #[strum(serialize = "conan")] - Conan, - - /// Interacts with Conda. - #[strum(serialize = "conda")] - Conda, - - /// Interacts with CPAN. - #[strum(serialize = "cpan")] - Cpan, - - /// Interacts with CRAN. - #[strum(serialize = "cran")] - Cran, - - /// The `custom` fetcher describes first party projects in FOSSA. - /// - /// These projects aren't really _fetched_; - /// they're stored in FOSSA's database. - #[strum(serialize = "custom")] - Custom, - - /// Interacts with RubyGems. - #[strum(serialize = "gem")] - Gem, - - /// Interacts with git VCS hosts. - #[strum(serialize = "git")] - Git, - - /// Resolves 'git' dependencies in the same manner as Go modules. - #[strum(serialize = "go")] - Go, - - /// Interacts with Hackage. - #[strum(serialize = "hackage")] - Hackage, - - /// Interacts with Hex. - #[strum(serialize = "hex")] - Hex, - - /// Interacts with Maven. - #[strum(serialize = "mvn")] - Maven, - - /// Interacts with NPM. - #[strum(serialize = "npm")] - Npm, - - /// Interacts with Nuget. - #[strum(serialize = "nuget")] - Nuget, - - /// Interacts with PyPI. - #[strum(serialize = "pip")] - Pip, - - /// Interacts with CocoaPods. - #[strum(serialize = "pod")] - Pod, - - /// Interacts with Dart's package manager. - #[strum(serialize = "pub")] - Pub, - - /// Interact with Swift's package manager. - #[strum(serialize = "swift")] - Swift, - - /// Specifies an arbitrary URL, - /// which is downloaded and treated like an `Archive` variant. - #[strum(serialize = "url")] - Url, - - /// A user-specified package. - #[strum(serialize = "user")] - User, -} diff --git a/src/lib.rs b/src/lib.rs index b309285..b6b5731 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,24 +3,294 @@ #![deny(missing_docs)] #![warn(rust_2018_idioms)] +use std::{borrow::Cow, str::FromStr}; + use lazy_static::lazy_static; use regex::Regex; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumIter, EnumString}; +use utoipa::ToSchema; mod error; -mod fetcher; mod locator; mod locator_package; mod locator_strict; pub use error::*; -pub use fetcher::*; pub use locator::*; pub use locator_package::*; pub use locator_strict::*; +/// [`Locator`](crate::Locator) is closely tied with the concept of Core's "fetchers", +/// which are asynchronous jobs tasked with downloading the code +/// referred to by a [`Locator`](crate::Locator) so that Core or some other service +/// may analyze it. +/// +/// For more information on the background of `Locator` and fetchers generally, +/// refer to [Fetchers and Locators](https://go/fetchers-doc). +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + Display, + EnumString, + EnumIter, + AsRefStr, + Serialize, + Deserialize, + ToSchema, +)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum Fetcher { + /// Archive locators are FOSSA specific. + #[strum(serialize = "archive")] + Archive, + + /// Interacts with Bower. + #[strum(serialize = "bower")] + Bower, + + /// Interacts with Carthage. + #[strum(serialize = "cart")] + Cart, + + /// Interacts with Cargo. + #[strum(serialize = "cargo")] + Cargo, + + /// Interacts with Composer. + #[strum(serialize = "comp")] + Comp, + + /// Interacts with Conan. + #[strum(serialize = "conan")] + Conan, + + /// Interacts with Conda. + #[strum(serialize = "conda")] + Conda, + + /// Interacts with CPAN. + #[strum(serialize = "cpan")] + Cpan, + + /// Interacts with CRAN. + #[strum(serialize = "cran")] + Cran, + + /// The `custom` fetcher describes first party projects in FOSSA. + /// + /// These projects aren't really _fetched_; + /// they're stored in FOSSA's database. + #[strum(serialize = "custom")] + Custom, + + /// Interacts with RubyGems. + #[strum(serialize = "gem")] + Gem, + + /// Interacts with git VCS hosts. + #[strum(serialize = "git")] + Git, + + /// Resolves 'git' dependencies in the same manner as Go modules. + #[strum(serialize = "go")] + Go, + + /// Interacts with Hackage. + #[strum(serialize = "hackage")] + Hackage, + + /// Interacts with Hex. + #[strum(serialize = "hex")] + Hex, + + /// Interacts with Maven. + #[strum(serialize = "mvn")] + Maven, + + /// Interacts with NPM. + #[strum(serialize = "npm")] + Npm, + + /// Interacts with Nuget. + #[strum(serialize = "nuget")] + Nuget, + + /// Interacts with PyPI. + #[strum(serialize = "pip")] + Pip, + + /// Interacts with CocoaPods. + #[strum(serialize = "pod")] + Pod, + + /// Interacts with Dart's package manager. + #[strum(serialize = "pub")] + Pub, + + /// Interact with Swift's package manager. + #[strum(serialize = "swift")] + Swift, + + /// Specifies an arbitrary URL, + /// which is downloaded and treated like an `Archive` variant. + #[strum(serialize = "url")] + Url, + + /// A user-specified package. + #[strum(serialize = "user")] + User, +} + +/// Identifies the organization to which this locator is namespaced. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct OrgId(usize); + +impl TryFrom<&str> for OrgId { + type Error = ::Err; + + fn try_from(value: &str) -> Result { + Ok(OrgId(value.parse()?)) + } +} + +impl std::fmt::Display for OrgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for OrgId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +/// The project section of the locator. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct Project(String); + +impl Project { + /// View the item as a string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for Project { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for Project { + fn from(value: &str) -> Self { + Self::from(value.to_string()) + } +} + +impl std::fmt::Display for Project { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for Project { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +impl std::cmp::Ord for Project { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + alphanumeric_sort::compare_str(&self.0, &other.0) + } +} + +impl std::cmp::PartialOrd for Project { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// The revision section of the locator. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum Revision { + /// The revision is valid semver. + Semver(semver::Version), + + /// The revision is an opaque string. + Opaque(String), +} + +impl Revision { + /// View the item as a string. + pub fn as_str(&self) -> Cow<'_, str> { + match self { + Revision::Semver(v) => Cow::Owned(v.to_string()), + Revision::Opaque(v) => Cow::Borrowed(v), + } + } +} + +impl From for Revision { + fn from(value: String) -> Self { + match semver::Version::parse(&value) { + Ok(v) => Self::Semver(v), + Err(_) => Self::Opaque(value), + } + } +} + +impl From<&str> for Revision { + fn from(value: &str) -> Self { + Self::from(value.to_string()) + } +} + +impl std::fmt::Display for Revision { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Revision::Semver(v) => write!(f, "{v}"), + Revision::Opaque(v) => write!(f, "{v}"), + } + } +} + +impl std::fmt::Debug for Revision { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +impl std::cmp::Ord for Revision { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let cmp = alphanumeric_sort::compare_str; + match (self, other) { + (Revision::Semver(a), Revision::Semver(b)) => a.cmp(b), + (Revision::Semver(a), Revision::Opaque(b)) => cmp(&a.to_string(), b), + (Revision::Opaque(a), Revision::Semver(b)) => cmp(a, &b.to_string()), + (Revision::Opaque(a), Revision::Opaque(b)) => cmp(a, b), + } + } +} + +impl std::cmp::PartialOrd for Revision { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// Optionally parse an org ID and trimmed project out of a project string. -fn parse_org_project(project: &str) -> Result<(Option, &str), ProjectParseError> { +fn parse_org_project(project: &str) -> Result<(Option, Project), ProjectParseError> { lazy_static! { static ref RE: Regex = Regex::new(r"^(?:(?P\d+)/)?(?P.+)") .expect("Project parsing expression must compile"); @@ -42,14 +312,18 @@ fn parse_org_project(project: &str) -> Result<(Option, &str), ProjectPars // If we fail to parse the org_id as a valid number, don't fail the overall parse; // just don't namespace to org ID and return the input unmodified. - match capture.name("org_id").map(|m| m.as_str()).map(str::parse) { + match capture + .name("org_id") + .map(|m| m.as_str()) + .map(OrgId::try_from) + { // An org ID was provided and validly parsed, use it. - Some(Ok(org_id)) => Ok((Some(org_id), trimmed_project)), + Some(Ok(org_id)) => Ok((Some(org_id), Project::from(trimmed_project))), // Otherwise, if we either didn't get an org ID section, // or it wasn't a valid org ID, // just use the project as-is. - _ => Ok((None, project)), + _ => Ok((None, Project::from(project))), } } @@ -59,10 +333,16 @@ mod tests { use super::*; + impl Project { + fn new(value: &str) -> Self { + Self(value.to_string()) + } + } + #[test] fn parses_org_project() { - let orgs = [0usize, 1, 9809572]; - let names = ["name", "name/foo"]; + let orgs = [OrgId(0usize), OrgId(1), OrgId(9809572)]; + let names = [Project::new("name"), Project::new("name/foo")]; for (org, name) in izip!(orgs, names) { let test = format!("{org}/{name}"); @@ -76,9 +356,15 @@ mod tests { #[test] fn parses_org_project_no_org() { - let names = ["/name/foo", "/name", "abcd/1234/name", "1abc2/name"]; + let names = [ + Project::new("/name/foo"), + Project::new("/name"), + Project::new("abcd/1234/name"), + Project::new("1abc2/name"), + ]; for test in names { - let Ok((org_id, project)) = parse_org_project(test) else { + let input = &format!("{test}"); + let Ok((org_id, project)) = parse_org_project(input) else { panic!("must parse '{test}'") }; assert_eq!(org_id, None, "'org_id' must be None in '{test}'"); diff --git a/src/locator.rs b/src/locator.rs index c4ec795..3d1d04b 100644 --- a/src/locator.rs +++ b/src/locator.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, fmt::Display}; +use std::fmt::Display; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -12,7 +12,10 @@ use utoipa::{ ToSchema, }; -use crate::{parse_org_project, Error, Fetcher, PackageLocator, ParseError, StrictLocator}; +use crate::{ + parse_org_project, Error, Fetcher, OrgId, PackageLocator, ParseError, Project, Revision, + StrictLocator, +}; /// Core, and most services that interact with Core, /// refer to open source packages via the `Locator` type. @@ -32,6 +35,21 @@ use crate::{parse_org_project, Error, Fetcher, PackageLocator, ParseError, Stric /// FOSSA employees may refer to /// [Fetchers and Locators](https://go/fetchers-doc). /// +/// ## Ordering +/// +/// Locators order by: +/// 1. Fetcher, alphanumerically. +/// 2. Organization ID, alphanumerically; missing organizations are sorted higher. +/// 3. The project field, alphanumerically. +/// 4. The revision field: +/// If both comparing locators use semver, these are compared using semver rules; +/// otherwise these are compared alphanumerically. +/// Missing revisions are sorted higher. +/// +/// Importantly, there may be other metrics for ordering using the actual code host +/// which contains the package (for example, ordering by release date). +/// This library does not perform such ordering. +/// /// ## Parsing /// /// The input string must be in one of the following forms: @@ -48,33 +66,45 @@ use crate::{parse_org_project, Error, Fetcher, PackageLocator, ParseError, Stric /// - `{fetcher}+{org_id}/{project}${revision}` /// /// This parse function is based on the function used in FOSSA Core for maximal compatibility. -#[derive(Clone, Eq, PartialEq, Hash, Debug, TypedBuilder, Getters, CopyGetters, Documented)] +#[derive( + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + TypedBuilder, + Getters, + CopyGetters, + Documented, +)] pub struct Locator { /// Determines which fetcher is used to download this project. #[getset(get_copy = "pub")] fetcher: Fetcher, /// Specifies the organization ID to which this project is namespaced. - #[builder(default, setter(strip_option))] + #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] - org_id: Option, + org_id: Option, /// Specifies the unique identifier for the project by fetcher. /// /// For example, the `git` fetcher fetching a github project /// uses a value in the form of `{user_name}/{project_name}`. - #[builder(setter(transform = |project: impl ToString| project.to_string()))] + #[builder(setter(transform = |project: impl ToString| Project(project.to_string())))] #[getset(get = "pub")] - project: String, + project: Project, /// Specifies the version for the project by fetcher. /// /// For example, the `git` fetcher fetching a github project /// uses a value in the form of `{git_sha}` or `{git_tag}`, /// and the fetcher disambiguates. - #[builder(default, setter(transform = |revision: impl ToString| Some(revision.to_string())))] + #[builder(default, setter(transform = |revision: impl ToString| Some(Revision::from(revision.to_string()))))] #[getset(get = "pub")] - revision: Option, + revision: Option, } impl Locator { @@ -120,7 +150,7 @@ impl Locator { if s.is_empty() { None } else { - Some(s.to_string()) + Some(Revision::from(s)) } }); @@ -128,13 +158,13 @@ impl Locator { Ok((org_id @ Some(_), project)) => Ok(Locator { fetcher, org_id, - project: String::from(project), + project, revision, }), Ok((org_id @ None, _)) => Ok(Locator { fetcher, org_id, - project, + project: Project::from(project.as_str()), revision, }), Err(error) => Err(Error::Parse(ParseError::Project { @@ -153,11 +183,14 @@ impl Locator { let locator = StrictLocator::builder() .fetcher(self.fetcher) .project(self.project) - .revision(self.revision.unwrap_or_else(|| revision.to_string())); + .revision( + self.revision + .unwrap_or_else(|| Revision::from(revision.to_string())), + ); match self.org_id { None => locator.build(), - Some(org_id) => locator.org_id(org_id).build(), + Some(OrgId(id)) => locator.org_id(id).build(), } } @@ -169,11 +202,11 @@ impl Locator { let locator = StrictLocator::builder() .fetcher(self.fetcher) .project(self.project) - .revision(self.revision.unwrap_or_else(revision)); + .revision(self.revision.unwrap_or_else(|| Revision::from(revision()))); match self.org_id { None => locator.build(), - Some(org_id) => locator.org_id(org_id).build(), + Some(OrgId(id)) => locator.org_id(id).build(), } } @@ -185,36 +218,11 @@ impl Locator { /// Explodes the locator into its (owned) parts. /// Used for conversions without cloning. - pub(crate) fn explode(self) -> (Fetcher, Option, String, Option) { + pub(crate) fn explode(self) -> (Fetcher, Option, Project, Option) { (self.fetcher, self.org_id, self.project, self.revision) } } -impl Ord for Locator { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.fetcher.cmp(&other.fetcher) { - Ordering::Equal => {} - ord => return ord, - } - match alphanumeric_sort::compare_str(&self.project, &other.project) { - Ordering::Equal => {} - ord => return ord, - } - match (&self.revision, &other.revision) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Greater, - (Some(_), None) => Ordering::Less, - (Some(a), Some(b)) => alphanumeric_sort::compare_str(a, b), - } - } -} - -impl PartialOrd for Locator { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl Display for Locator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fetcher = &self.fetcher; @@ -319,6 +327,8 @@ impl From<&StrictLocator> for Locator { #[cfg(test)] mod tests { + use std::borrow::Cow; + use assert_matches::assert_matches; use itertools::{izip, Itertools}; use pretty_assertions::assert_eq; @@ -387,7 +397,13 @@ mod tests { #[test] fn parse_with_org() { let fetchers = Fetcher::iter().map(|fetcher| format!("{fetcher}")); - let orgs = [0usize, 1, 1234, 2385028, 19847938492847928]; + let orgs = [ + OrgId(0usize), + OrgId(1), + OrgId(1234), + OrgId(2385028), + OrgId(19847938492847928), + ]; let projects = ["github.com/foo/bar", "some-name"]; let revisions = ["", "$", "$1", "$1234abcd1234"]; @@ -416,7 +432,7 @@ mod tests { let revision = if revision.is_empty() || revision == "$" { None } else { - Some(revision) + Some(Cow::Borrowed(revision)) }; assert_eq!( parsed.revision().as_ref().map(|r| r.as_str()), @@ -552,12 +568,12 @@ mod tests { .expect("must parse locators"); let expected = vec![ + "custom+baz$1234", "custom+1/bam$1234", - "custom+2/bam$1234", "custom+2/bam", - "custom+baz$1234", - "git+github.com/foo/bar$1234", + "custom+2/bam$1234", "git+github.com/foo/bar", + "git+github.com/foo/bar$1234", ]; let sorted = locators .iter() diff --git a/src/locator_package.rs b/src/locator_package.rs index 5ee0d5b..5f03880 100644 --- a/src/locator_package.rs +++ b/src/locator_package.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, fmt::Display}; +use std::fmt::Display; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -10,13 +10,24 @@ use utoipa::{ ToSchema, }; -use crate::{Error, Fetcher, Locator, StrictLocator}; +use crate::{Error, Fetcher, Locator, OrgId, Project, StrictLocator}; /// A [`Locator`] specialized to not include the `revision` component. /// /// Any [`Locator`] may be converted to a `PackageLocator` by simply discarding the `revision` component. /// To create a [`Locator`] from a `PackageLocator`, the value for `revision` must be provided; see [`Locator`] for details. /// +/// ## Ordering +/// +/// Locators order by: +/// 1. Fetcher, alphanumerically. +/// 2. Organization ID, alphanumerically; missing organizations are sorted higher. +/// 3. The project field, alphanumerically. +/// +/// Importantly, there may be other metrics for ordering using the actual code host +/// which contains the package (for example, ordering by release date). +/// This library does not perform such ordering. +/// /// ## Parsing /// /// The input string must be in one of the following forms: @@ -33,24 +44,36 @@ use crate::{Error, Fetcher, Locator, StrictLocator}; /// - `{fetcher}+{org_id}/{project}${revision}` /// /// This implementation ignores the `revision` segment if it exists. If this is not preferred, use [`Locator`] instead. -#[derive(Clone, Eq, PartialEq, Hash, Debug, TypedBuilder, Getters, CopyGetters, Documented)] +#[derive( + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + TypedBuilder, + Getters, + CopyGetters, + Documented, +)] pub struct PackageLocator { /// Determines which fetcher is used to download this project. #[getset(get_copy = "pub")] fetcher: Fetcher, /// Specifies the organization ID to which this project is namespaced. - #[builder(default, setter(strip_option))] + #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] - org_id: Option, + org_id: Option, /// Specifies the unique identifier for the project by fetcher. /// /// For example, the `git` fetcher fetching a github project /// uses a value in the form of `{user_name}/{project_name}`. - #[builder(setter(transform = |project: impl ToString| project.to_string()))] + #[builder(setter(transform = |project: impl ToString| Project(project.to_string())))] #[getset(get = "pub")] - project: String, + project: Project, } impl PackageLocator { @@ -70,8 +93,8 @@ impl PackageLocator { match (self.org_id, revision) { (None, None) => locator.build(), (None, Some(revision)) => locator.revision(revision).build(), - (Some(org_id), None) => locator.org_id(org_id).build(), - (Some(org_id), Some(revision)) => locator.org_id(org_id).revision(revision).build(), + (Some(OrgId(id)), None) => locator.org_id(id).build(), + (Some(OrgId(id)), Some(revision)) => locator.org_id(id).revision(revision).build(), } } @@ -84,33 +107,17 @@ impl PackageLocator { match self.org_id { None => locator.build(), - Some(org_id) => locator.org_id(org_id).build(), + Some(OrgId(id)) => locator.org_id(id).build(), } } /// Explodes the locator into its (owned) parts. /// Used for conversions without cloning. - pub(crate) fn explode(self) -> (Fetcher, Option, String) { + pub(crate) fn explode(self) -> (Fetcher, Option, Project) { (self.fetcher, self.org_id, self.project) } } -impl Ord for PackageLocator { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.fetcher.cmp(&other.fetcher) { - Ordering::Equal => {} - ord => return ord, - } - alphanumeric_sort::compare_str(&self.project, &other.project) - } -} - -impl PartialOrd for PackageLocator { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl Display for PackageLocator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let converted = Locator::from(self); @@ -260,7 +267,13 @@ mod tests { #[test] fn parse_with_org() { let fetchers = Fetcher::iter().map(|fetcher| format!("{fetcher}")); - let orgs = [0usize, 1, 1234, 2385028, 19847938492847928]; + let orgs = [ + OrgId(0usize), + OrgId(1), + OrgId(1234), + OrgId(2385028), + OrgId(19847938492847928), + ]; let projects = ["github.com/foo/bar", "some-name"]; let revisions = ["", "$", "$1", "$1234abcd1234"]; diff --git a/src/locator_strict.rs b/src/locator_strict.rs index a1a241a..893b249 100644 --- a/src/locator_strict.rs +++ b/src/locator_strict.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, fmt::Display}; +use std::fmt::Display; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -10,10 +10,24 @@ use utoipa::{ ToSchema, }; -use crate::{Error, Fetcher, Locator, PackageLocator, ParseError}; +use crate::{Error, Fetcher, Locator, OrgId, PackageLocator, ParseError, Project, Revision}; /// A [`Locator`] specialized to **require** the `revision` component. /// +/// ## Ordering +/// +/// Locators order by: +/// 1. Fetcher, alphanumerically. +/// 2. Organization ID, alphanumerically; missing organizations are sorted higher. +/// 3. The project field, alphanumerically. +/// 4. The revision field: +/// If both comparing locators use semver, these are compared using semver rules; +/// otherwise these are compared alphanumerically. +/// +/// Importantly, there may be other metrics for ordering using the actual code host +/// which contains the package (for example, ordering by release date). +/// This library does not perform such ordering. +/// /// ## Parsing /// /// The input string must be in the following format: @@ -28,33 +42,45 @@ use crate::{Error, Fetcher, Locator, PackageLocator, ParseError}; /// ```ignore /// {fetcher}+{org_id}/{project}${revision} /// ``` -#[derive(Clone, Eq, PartialEq, Hash, Debug, TypedBuilder, Getters, CopyGetters, Documented)] +#[derive( + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + TypedBuilder, + Getters, + CopyGetters, + Documented, +)] pub struct StrictLocator { /// Determines which fetcher is used to download this project. #[getset(get_copy = "pub")] fetcher: Fetcher, /// Specifies the organization ID to which this project is namespaced. - #[builder(default, setter(strip_option))] + #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] - org_id: Option, + org_id: Option, /// Specifies the unique identifier for the project by fetcher. /// /// For example, the `git` fetcher fetching a github project /// uses a value in the form of `{user_name}/{project_name}`. - #[builder(setter(transform = |project: impl ToString| project.to_string()))] + #[builder(setter(transform = |project: impl ToString| Project(project.to_string())))] #[getset(get = "pub")] - project: String, + project: Project, /// Specifies the version for the project by fetcher. /// /// For example, the `git` fetcher fetching a github project /// uses a value in the form of `{git_sha}` or `{git_tag}`, /// and the fetcher disambiguates. - #[builder(setter(transform = |revision: impl ToString| revision.to_string()))] + #[builder(setter(transform = |revision: impl ToString| Revision::from(revision.to_string())))] #[getset(get = "pub")] - revision: String, + revision: Revision, } impl StrictLocator { @@ -92,31 +118,11 @@ impl StrictLocator { /// Explodes the locator into its (owned) parts. /// Used for conversions without cloning. - pub(crate) fn explode(self) -> (Fetcher, Option, String, String) { + pub(crate) fn explode(self) -> (Fetcher, Option, Project, Revision) { (self.fetcher, self.org_id, self.project, self.revision) } } -impl Ord for StrictLocator { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.fetcher.cmp(&other.fetcher) { - Ordering::Equal => {} - ord => return ord, - } - match alphanumeric_sort::compare_str(&self.project, &other.project) { - Ordering::Equal => {} - ord => return ord, - } - alphanumeric_sort::compare_str(&self.revision, &other.revision) - } -} - -impl PartialOrd for StrictLocator { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl Display for StrictLocator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fetcher = &self.fetcher; @@ -229,7 +235,13 @@ mod tests { #[test] fn parse_with_org() { let fetchers = Fetcher::iter().map(|fetcher| format!("{fetcher}")); - let orgs = [0usize, 1, 1234, 2385028, 19847938492847928]; + let orgs = [ + OrgId(0usize), + OrgId(1), + OrgId(1234), + OrgId(2385028), + OrgId(19847938492847928), + ]; let projects = ["github.com/foo/bar", "some-name"]; let revisions = ["1", "1234abcd1234"];