Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jssblck committed May 15, 2024
1 parent 530544a commit 836711a
Show file tree
Hide file tree
Showing 7 changed files with 1,420 additions and 582 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
[package]
name = "locator"
version = "1.0.0"
version = "1.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
alphanumeric-sort = "1.5.3"
getset = "0.1.2"
lazy_static = "1.4.0"
pretty_assertions = "1.4.0"
regex = "1.6.0"
schemars = "0.8.19"
serde = { version = "1.0.140", features = ["derive"] }
strum = { version = "0.24.1", features = ["derive"] }
thiserror = "1.0.31"
Expand Down
17 changes: 16 additions & 1 deletion src/fetcher.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::str::FromStr;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString};

Expand All @@ -10,7 +11,21 @@ use strum::{AsRefStr, Display, EnumIter, EnumString};
///
/// 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, Hash, Debug, Display, EnumString, EnumIter, AsRefStr)]
#[derive(
Copy,
Clone,
Eq,
PartialEq,
Ord,
PartialOrd,
Hash,
Debug,
Display,
EnumString,
EnumIter,
AsRefStr,
JsonSchema,
)]
#[non_exhaustive]
pub enum Fetcher {
/// Archive locators are FOSSA specific.
Expand Down
330 changes: 39 additions & 291 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,304 +3,21 @@
#![deny(missing_docs)]
#![warn(rust_2018_idioms)]

use std::fmt::Display;

use getset::{CopyGetters, Getters};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;

mod error;
mod fetcher;
mod locator;
mod locator_package;
mod locator_strict;

pub use error::*;
pub use fetcher::*;

/// Core, and most services that interact with Core,
/// refer to open source packages via the `Locator` type.
///
/// This type is nearly universally rendered to a string
/// before being serialized to the database or sent over the network.
///
/// This type represents a _validly-constructed_ `Locator`, but does not
/// validate whether a `Locator` is actually valid. This means that a
/// given `Locator` is guaranteed to be correctly formatted data,
/// but that the actual repository or revision to which the `Locator`
/// refers is _not_ guaranteed to exist or be accessible.
/// Currently the canonical method for validating whether a given `Locator` is
/// accessible is to run it through the Core fetcher system.
///
/// For more information on the background of `Locator` and fetchers generally,
/// FOSSA employees may refer to
/// [Fetchers and Locators](https://go/fetchers-doc).
#[derive(Clone, Eq, PartialEq, Hash, Debug, TypedBuilder, Getters, CopyGetters)]
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))]
#[getset(get_copy = "pub")]
org_id: Option<usize>,

/// 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()))]
#[getset(get = "pub")]
project: String,

/// 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())))]
#[getset(get = "pub")]
revision: Option<String>,
}

impl Locator {
/// Parse a `Locator`.
///
/// The input string must be in one of the following forms:
/// - `{fetcher}+{project}`
/// - `{fetcher}+{project}$`
/// - `{fetcher}+{project}${revision}`
///
/// Projects may also be namespaced to a specific organization;
/// in such cases the organization ID is at the start of the `{project}` field
/// separated by a slash. The ID can be any non-negative integer.
/// This yields the following formats:
/// - `{fetcher}+{org_id}/{project}`
/// - `{fetcher}+{org_id}/{project}$`
/// - `{fetcher}+{org_id}/{project}${revision}`
///
/// This parse function is based on the function used in FOSSA Core for maximal compatibility.
pub fn parse(locator: &str) -> Result<Self, Error> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"^(?:(?P<fetcher>[a-z-]+)\+|)(?P<project>[^$]+)(?:\$|)(?P<revision>.+|)$"
)
.expect("Locator parsing expression must compile");
}

let mut captures = RE.captures_iter(locator);
let capture = captures.next().ok_or_else(|| ParseError::Syntax {
input: locator.to_string(),
})?;

let fetcher =
capture
.name("fetcher")
.map(|m| m.as_str())
.ok_or_else(|| ParseError::Field {
input: locator.to_owned(),
field: "fetcher".to_string(),
})?;

let fetcher = Fetcher::try_from(fetcher).map_err(|error| ParseError::Fetcher {
input: locator.to_owned(),
fetcher: fetcher.to_string(),
error,
})?;

let project = capture
.name("project")
.map(|m| m.as_str().to_owned())
.ok_or_else(|| ParseError::Field {
input: locator.to_owned(),
field: "project".to_string(),
})?;

let revision = capture.name("revision").map(|m| m.as_str()).and_then(|s| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
});

match parse_org_project(&project) {
Ok((org_id @ Some(_), project)) => Ok(Locator {
fetcher,
org_id,
project: String::from(project),
revision,
}),
Ok((org_id @ None, _)) => Ok(Locator {
fetcher,
org_id,
project,
revision,
}),
Err(error) => Err(Error::Parse(ParseError::Project {
input: locator.to_owned(),
project,
error,
})),
}
}

/// Converts the locator into a [`PackageLocator`] by discarding the `revision` component.
/// Equivalent to the `From` implementation, but offered as a method for convenience.
pub fn into_package(self) -> PackageLocator {
self.into()
}
}

impl Display for Locator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fetcher = &self.fetcher;
write!(f, "{fetcher}+")?;

let project = &self.project;
if let Some(org_id) = &self.org_id {
write!(f, "{org_id}/")?;
}
write!(f, "{project}")?;

if let Some(revision) = &self.revision {
write!(f, "${revision}")?;
}

Ok(())
}
}

impl<'de> Deserialize<'de> for Locator {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Locator::parse(&raw).map_err(serde::de::Error::custom)
}
}

impl Serialize for Locator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}

/// 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.
#[derive(Clone, Eq, PartialEq, Hash, Debug, TypedBuilder)]
pub struct PackageLocator {
/// Determines which fetcher is used to download this dependency
/// from the internet.
fetcher: Fetcher,

/// Specifies the organization ID to which this project is namespaced.
org_id: Option<usize>,

/// 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()))]
project: String,
}

impl PackageLocator {
/// Parse a `PackageLocator`.
///
/// The input string must be in one of the following forms:
/// - `{fetcher}+{project}`
/// - `{fetcher}+{project}$`
/// - `{fetcher}+{project}${revision}`
///
/// Projects may also be namespaced to a specific organization;
/// in such cases the organization ID is at the start of the `{project}` field
/// separated by a slash. The ID can be any non-negative integer.
/// This yields the following formats:
/// - `{fetcher}+{org_id}/{project}`
/// - `{fetcher}+{org_id}/{project}$`
/// - `{fetcher}+{org_id}/{project}${revision}`
///
/// This parse function is based on the function used in FOSSA Core for maximal compatibility.
///
/// This implementation ignores the `revision` segment if it exists. If this is not preferred, use [`Locator`] instead.
pub fn parse(locator: &str) -> Result<Self, Error> {
let full = Locator::parse(locator)?;
Ok(Self {
fetcher: full.fetcher,
org_id: full.org_id,
project: full.project,
})
}

/// Promote a `PackageLocator` to a [`Locator`] by providing the value to use for the `revision` component.
pub fn promote(self, revision: Option<String>) -> Locator {
Locator {
fetcher: self.fetcher,
org_id: self.org_id,
project: self.project,
revision,
}
}
}

impl Display for PackageLocator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let converted = Locator::from(self);
write!(f, "{converted}")
}
}

impl<'de> Deserialize<'de> for PackageLocator {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
PackageLocator::parse(&raw).map_err(serde::de::Error::custom)
}
}

impl Serialize for PackageLocator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}

impl From<Locator> for PackageLocator {
fn from(full: Locator) -> Self {
Self {
fetcher: full.fetcher,
org_id: full.org_id,
project: full.project,
}
}
}

impl From<PackageLocator> for Locator {
fn from(package: PackageLocator) -> Self {
Self {
fetcher: package.fetcher,
org_id: package.org_id,
project: package.project,
revision: None,
}
}
}

impl From<&PackageLocator> for Locator {
fn from(package: &PackageLocator) -> Self {
package.clone().into()
}
}
pub use locator::*;
pub use locator_package::*;
pub use locator_strict::*;

/// Optionally parse an org ID and trimmed project out of a project string.
fn parse_org_project(project: &str) -> Result<(Option<usize>, &str), ProjectParseError> {
Expand Down Expand Up @@ -337,4 +54,35 @@ fn parse_org_project(project: &str) -> Result<(Option<usize>, &str), ProjectPars
}

#[cfg(test)]
mod test;
mod tests {
use itertools::izip;

use super::*;

#[test]
fn parses_org_project() {
let orgs = [0usize, 1, 9809572];
let names = ["name", "name/foo"];

for (org, name) in izip!(orgs, names) {
let test = format!("{org}/{name}");
let Ok((Some(org_id), project)) = parse_org_project(&test) else {
panic!("must parse '{test}'")
};
assert_eq!(org_id, org, "'org_id' must match in '{test}'");
assert_eq!(project, name, "'project' must match in '{test}");
}
}

#[test]
fn parses_org_project_no_org() {
let names = ["/name/foo", "/name", "abcd/1234/name", "1abc2/name"];
for test in names {
let Ok((org_id, project)) = parse_org_project(test) else {
panic!("must parse '{test}'")
};
assert_eq!(org_id, None, "'org_id' must be None in '{test}'");
assert_eq!(project, test, "'project' must match in '{test}");
}
}
}
Loading

0 comments on commit 836711a

Please sign in to comment.