diff --git a/Cargo.toml b/Cargo.toml index 75b3920..16d5a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "locator" -version = "2.0.2" +version = "2.1.0" edition = "2021" [dependencies] @@ -20,5 +20,7 @@ semver = "1.0.23" [dev-dependencies] assert_matches = "1.5.0" +impls = "1.0.3" itertools = "0.10.5" proptest = "1.0.0" +static_assertions = "1.1.0" diff --git a/src/locator.rs b/src/locator.rs index 420e49d..e2e3cdb 100644 --- a/src/locator.rs +++ b/src/locator.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -17,6 +17,54 @@ use crate::{ StrictLocator, }; +/// Convenience macro for creating a [`Locator`]. +/// Required types and fields are checked at compile time. +/// +/// ``` +/// let loc = locator::locator!(Npm, "lodash"); +/// assert_eq!("npm+lodash", &loc.to_string()); +/// +/// let loc = locator::locator!(Npm, "lodash", "1.0.0"); +/// assert_eq!("npm+lodash$1.0.0", &loc.to_string()); +/// +/// let loc = locator::locator!(org 1234 => Npm, "lodash"); +/// assert_eq!("npm+1234/lodash", &loc.to_string()); +/// +/// let loc = locator::locator!(org 1234 => Npm, "lodash", "1.0.0"); +/// assert_eq!("npm+1234/lodash$1.0.0", &loc.to_string()); +/// ``` +#[macro_export] +macro_rules! locator { + (org $org:expr => $fetcher:ident, $package:expr, $version:expr) => { + $crate::Locator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .org_id($org) + .revision($version) + .build() + }; + (org $org:expr => $fetcher:ident, $package:expr) => { + $crate::Locator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .org_id($org) + .build() + }; + ($fetcher:ident, $package:expr, $version:expr) => { + $crate::Locator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .revision($version) + .build() + }; + ($fetcher:ident, $package:expr) => { + $crate::Locator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .build() + }; +} + /// Core, and most services that interact with Core, /// refer to open source packages via the `Locator` type. /// @@ -85,6 +133,23 @@ pub struct Locator { fetcher: Fetcher, /// Specifies the organization ID to which this package is namespaced. + /// + /// Locators are namespaced to an organization when FOSSA needs to use the + /// private repositories or settings configured by the user to resolve the package. + /// + /// Generally, users can treat this as an implementation detail: + /// Organization IDs namespacing a package means the package should concretely be considered different; + /// for example `npm+lodash$1.0.0` should be considered different from `npm+1234/lodash$1.0.0`. + /// The reasoning for this is that private packages may be totally different than + /// a similarly named public package- in the example above, both of them being `lodash@1.0.0` + /// doesn't really imply that they are both the popular project known as "lodash". + /// We know the public one is (`npm+lodash$1.0.0`), but the private one could be anything. + /// + /// Examples: + /// - A public Maven package that is hosted on Maven Central is not namespaced. + /// - A private Maven package that is hosted on a private host is namespaced. + /// - A public NPM package that is hosted on NPM is not namespaced. + /// - A private NPM package that is hosted on NPM but requires credentials is namespaced. #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] org_id: Option, @@ -325,19 +390,57 @@ impl From<&StrictLocator> for Locator { } } +impl AsRef for Locator { + fn as_ref(&self) -> &Locator { + self + } +} + +impl FromStr for Locator { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + #[cfg(test)] mod tests { use std::borrow::Cow; use assert_matches::assert_matches; + use impls::impls; use itertools::{izip, Itertools}; use pretty_assertions::assert_eq; use proptest::prelude::*; use serde::Deserialize; + use static_assertions::const_assert; use strum::IntoEnumIterator; use super::*; + #[test] + fn trait_impls() { + const_assert!(impls!(Locator: AsRef)); + const_assert!(impls!(Locator: FromStr)); + const_assert!(impls!(Locator: From)); + } + + #[test] + fn parse_using_fromstr() { + let input = "git+github.com/foo/bar"; + let parsed = input.parse().expect("must parse locator"); + let expected = locator!(Git, "github.com/foo/bar"); + assert_eq!(expected, parsed); + assert_eq!(&parsed.to_string(), input); + + let input = "git+github.com/foo/bar$1234"; + let parsed = input.parse().expect("must parse locator"); + let expected = locator!(Git, "github.com/foo/bar", "1234"); + assert_eq!(expected, parsed); + assert_eq!(&parsed.to_string(), input); + } + #[test] fn parse_render_successful() { let input = "git+github.com/foo/bar"; diff --git a/src/locator_package.rs b/src/locator_package.rs index 310023d..17fa4e4 100644 --- a/src/locator_package.rs +++ b/src/locator_package.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -12,6 +12,33 @@ use utoipa::{ use crate::{Error, Fetcher, Locator, OrgId, Package, StrictLocator}; +/// Convenience macro for creating a [`PackageLocator`]. +/// Required types and fields are checked at compile time. +/// +/// ``` +/// let loc = locator::package!(Npm, "lodash"); +/// assert_eq!("npm+lodash", &loc.to_string()); +/// +/// let loc = locator::package!(org 1234 => Npm, "lodash"); +/// assert_eq!("npm+1234/lodash", &loc.to_string()); +/// ``` +#[macro_export] +macro_rules! package { + (org $org:expr => $fetcher:ident, $package:expr) => { + $crate::PackageLocator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .org_id($org) + .build() + }; + ($fetcher:ident, $package:expr) => { + $crate::PackageLocator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .build() + }; +} + /// A [`Locator`] specialized to not include the `revision` component. /// /// Any [`Locator`] may be converted to a `PackageLocator` by simply discarding the `revision` component. @@ -63,6 +90,23 @@ pub struct PackageLocator { fetcher: Fetcher, /// Specifies the organization ID to which this package is namespaced. + /// + /// Locators are namespaced to an organization when FOSSA needs to use the + /// private repositories or settings configured by the user to resolve the package. + /// + /// Generally, users can treat this as an implementation detail: + /// Organization IDs namespacing a package means the package should concretely be considered different; + /// for example `npm+lodash$1.0.0` should be considered different from `npm+1234/lodash$1.0.0`. + /// The reasoning for this is that private packages may be totally different than + /// a similarly named public package- in the example above, both of them being `lodash@1.0.0` + /// doesn't really imply that they are both the popular project known as "lodash". + /// We know the public one is (`npm+lodash$1.0.0`), but the private one could be anything. + /// + /// Examples: + /// - A public Maven package that is hosted on Maven Central is not namespaced. + /// - A private Maven package that is hosted on a private host is namespaced. + /// - A public NPM package that is hosted on NPM is not namespaced. + /// - A private NPM package that is hosted on NPM but requires credentials is namespaced. #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] org_id: Option, @@ -204,18 +248,51 @@ impl From<&StrictLocator> for PackageLocator { } } +impl AsRef for PackageLocator { + fn as_ref(&self) -> &PackageLocator { + self + } +} + +impl FromStr for PackageLocator { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; + use impls::impls; use itertools::{izip, Itertools}; use pretty_assertions::assert_eq; use serde::Deserialize; + use static_assertions::const_assert; use strum::IntoEnumIterator; use crate::ParseError; use super::*; + #[test] + fn trait_impls() { + const_assert!(impls!(PackageLocator: AsRef)); + const_assert!(impls!(PackageLocator: FromStr)); + const_assert!(impls!(PackageLocator: From)); + const_assert!(impls!(PackageLocator: From)); + } + + #[test] + fn parse_using_fromstr() { + let input = "git+github.com/foo/bar"; + let parsed = input.parse().expect("must parse locator"); + let expected = package!(Git, "github.com/foo/bar"); + assert_eq!(expected, parsed); + assert_eq!(&parsed.to_string(), input); + } + #[test] fn parse_render_successful() { let input = "git+github.com/foo/bar"; diff --git a/src/locator_strict.rs b/src/locator_strict.rs index 31e3397..006e953 100644 --- a/src/locator_strict.rs +++ b/src/locator_strict.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use documented::Documented; use getset::{CopyGetters, Getters}; @@ -12,6 +12,35 @@ use utoipa::{ use crate::{Error, Fetcher, Locator, OrgId, Package, PackageLocator, ParseError, Revision}; +/// Convenience macro for creating a [`StrictLocator`]. +/// Required types and fields are checked at compile time. +/// +/// ``` +/// let loc = locator::strict!(Npm, "lodash", "1.0.0"); +/// assert_eq!("npm+lodash$1.0.0", &loc.to_string()); +/// +/// let loc = locator::strict!(org 1234 => Npm, "lodash", "1.0.0"); +/// assert_eq!("npm+1234/lodash$1.0.0", &loc.to_string()); +/// ``` +#[macro_export] +macro_rules! strict { + (org $org:expr => $fetcher:ident, $package:expr, $version:expr) => { + $crate::StrictLocator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .org_id($org) + .revision($version) + .build() + }; + ($fetcher:ident, $package:expr, $version:expr) => { + $crate::StrictLocator::builder() + .fetcher($crate::Fetcher::$fetcher) + .package($package) + .revision($version) + .build() + }; +} + /// A [`Locator`] specialized to **require** the `revision` component. /// /// ## Ordering @@ -61,6 +90,23 @@ pub struct StrictLocator { fetcher: Fetcher, /// Specifies the organization ID to which this package is namespaced. + /// + /// Locators are namespaced to an organization when FOSSA needs to use the + /// private repositories or settings configured by the user to resolve the package. + /// + /// Generally, users can treat this as an implementation detail: + /// Organization IDs namespacing a package means the package should concretely be considered different; + /// for example `npm+lodash$1.0.0` should be considered different from `npm+1234/lodash$1.0.0`. + /// The reasoning for this is that private packages may be totally different than + /// a similarly named public package- in the example above, both of them being `lodash@1.0.0` + /// doesn't really imply that they are both the popular project known as "lodash". + /// We know the public one is (`npm+lodash$1.0.0`), but the private one could be anything. + /// + /// Examples: + /// - A public Maven package that is hosted on Maven Central is not namespaced. + /// - A private Maven package that is hosted on a private host is namespaced. + /// - A public NPM package that is hosted on NPM is not namespaced. + /// - A private NPM package that is hosted on NPM but requires credentials is namespaced. #[builder(default, setter(transform = |id: usize| Some(OrgId(id))))] #[getset(get_copy = "pub")] org_id: Option, @@ -177,16 +223,47 @@ impl<'a> ToSchema<'a> for StrictLocator { } } +impl AsRef for StrictLocator { + fn as_ref(&self) -> &StrictLocator { + self + } +} + +impl FromStr for StrictLocator { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; + use impls::impls; use itertools::{izip, Itertools}; use pretty_assertions::assert_eq; use serde::Deserialize; + use static_assertions::const_assert; use strum::IntoEnumIterator; use super::*; + #[test] + fn trait_impls() { + const_assert!(impls!(StrictLocator: AsRef)); + const_assert!(impls!(StrictLocator: FromStr)); + } + + #[test] + fn parse_using_fromstr() { + let input = "git+github.com/foo/bar$abcd"; + let parsed = input.parse().expect("must parse locator"); + let expected = strict!(Git, "github.com/foo/bar", "abcd"); + assert_eq!(expected, parsed); + assert_eq!(&parsed.to_string(), input); + } + #[test] fn parse_render_successful() { let input = "git+github.com/foo/bar$abcd";