Skip to content

Commit

Permalink
Simplify parser, fix ToSchema types
Browse files Browse the repository at this point in the history
  • Loading branch information
jssblck committed Oct 19, 2024
1 parent 103d383 commit b54b403
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 191 deletions.
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ edition = "2021"
[dependencies]
alphanumeric-sort = "1.5.3"
getset = "0.1.2"
lazy_static = "1.4.0"
pretty_assertions = "1.4.0"
regex = "1.6.0"
serde = { version = "1.0.140", features = ["derive"] }
strum = { version = "0.24.1", features = ["derive"] }
thiserror = "1.0.31"
Expand All @@ -18,6 +16,7 @@ documented = "0.7.1"
semver = "1.0.23"
bon = "2.3.0"
duplicate = "2.0.0"
lazy-regex = { version = "3.3.0", features = ["regex"] }

[dev-dependencies]
assert_matches = "1.5.0"
Expand Down
110 changes: 53 additions & 57 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
#![deny(missing_docs)]
#![warn(rust_2018_idioms)]

use std::{borrow::Cow, str::FromStr};
use std::{borrow::Cow, num::ParseIntError, str::FromStr};

use documented::Documented;
use duplicate::duplicate;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString};
use utoipa::ToSchema;
Expand Down Expand Up @@ -198,6 +196,15 @@ impl From<usize> for OrgId {
}
}

impl FromStr for OrgId {
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let id = s.parse()?;
Ok(Self(id))
}
}

duplicate! {
[
number;
Expand Down Expand Up @@ -391,41 +398,34 @@ impl std::cmp::PartialOrd for Revision {
}

/// Optionally parse an org ID and trimmed package out of a package string.
fn parse_org_package(package: &str) -> Result<(Option<OrgId>, Package), PackageParseError> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^(?:(?P<org_id>\d+)/)?(?P<package>.+)")
.expect("Package parsing expression must compile");
}

let mut captures = RE.captures_iter(package);
let capture = captures.next().ok_or_else(|| PackageParseError::Package {
package: package.to_string(),
})?;

let trimmed_package =
capture
.name("package")
.map(|m| m.as_str())
.ok_or_else(|| PackageParseError::Field {
package: package.to_string(),
field: String::from("package"),
})?;

// 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(OrgId::try_from)
{
// An org ID was provided and validly parsed, use it.
Some(Ok(org_id)) => Ok((Some(org_id), Package::from(trimmed_package))),

// Otherwise, if we either didn't get an org ID section,
// or it wasn't a valid org ID,
// just use the package as-is.
_ => Ok((None, Package::from(package))),
fn parse_org_package(input: &str) -> (Option<OrgId>, Package) {
macro_rules! construct {
($org_id:expr, $package:expr) => {
return (Some($org_id), Package::from($package))
};
($package:expr) => {
return (None, Package::from($package))
};
}

// No `/`? This must not be namespaced.
let Some((org_id, package)) = input.split_once('/') else {
construct!(input);
};

// Nothing before or after the `/`? Still not namespaced.
if org_id.is_empty() || package.is_empty() {
construct!(input);
};

// If the part before the `/` isn't a number, it can't be a namespaced org id.
let Ok(org_id) = org_id.parse() else {
construct!(input)
};

// Ok, there was text before and after the `/`, and the content before was a number.
// Finally, we've got a namespaced package.
construct!(org_id, package)
}

#[cfg(test)]
Expand All @@ -449,27 +449,23 @@ mod tests {
};
}

#[test_case(Some(OrgId(0)), Package::new("name"); "0/name")]
#[test_case(Some(OrgId(1)), Package::new("name"); "1/name")]
#[test_case(Some(OrgId(1)), Package::new("name/foo"); "1/name/foo")]
#[test_case(Some(OrgId(9809572)), Package::new("name/foo"); "9809572/name/foo")]
#[test_case(None, Package::new("name/foo"); "name/foo")]
#[test_case(None, Package::new("name"); "name")]
#[test_case(None, Package::new("/name/foo"); "/name/foo")]
#[test_case(None, Package::new("/name"); "/name")]
#[test_case(None, Package::new("abcd/1234/name"); "abcd/1234/name")]
#[test_case(None, Package::new("1abc2/name"); "1abc2/name")]
#[test_case("0/name", Some(OrgId(0)), Package::new("name"); "0/name")]
#[test_case("1/name", Some(OrgId(1)), Package::new("name"); "1/name")]
#[test_case("1/name/foo", Some(OrgId(1)), Package::new("name/foo"); "1/name/foo")]
#[test_case("1//name/foo", Some(OrgId(1)), Package::new("/name/foo"); "doubleslash_1/name/foo")]
#[test_case("9809572/name/foo", Some(OrgId(9809572)), Package::new("name/foo"); "9809572/name/foo")]
#[test_case("name/foo", None, Package::new("name/foo"); "name/foo")]
#[test_case("name", None, Package::new("name"); "name")]
#[test_case("/name/foo", None, Package::new("/name/foo"); "/name/foo")]
#[test_case("/name", None, Package::new("/name"); "/name")]
#[test_case("abcd/1234/name", None, Package::new("abcd/1234/name"); "abcd/1234/name")]
#[test_case("1abc2/name", None, Package::new("1abc2/name"); "1abc2/name")]
#[test_case("name/1234", None, Package::new("name/1234"); "name/1234")]
#[test]
fn parse_org_package(org: Option<OrgId>, package: Package) {
let test = match org {
Some(id) => format!("{id}/{package}"),
None => format!("{package}"),
};
let Ok((org_id, name)) = parse_org_package(&test) else {
panic!("must parse '{test}'")
};
assert_eq!(org_id, org, "'org_id' must match in '{test}'");
assert_eq!(package, name, "'package' must match in '{test}");
fn parse_org_package(input: &str, org: Option<OrgId>, package: Package) {
let (org_id, name) = parse_org_package(input);
assert_eq!(org_id, org, "'org_id' must match in '{input}'");
assert_eq!(package, name, "'package' must match in '{input}");
}

#[test_case(r#""rpm-generic""#, Fetcher::LinuxRpm; "rpm-generic")]
Expand Down
Loading

0 comments on commit b54b403

Please sign in to comment.