use super::specifier::higher_version;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ActionId(pub String);
impl ActionId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn base_repo(&self) -> Repository {
Repository::from(self.0.split('/').take(2).collect::<Vec<_>>().join("/"))
}
#[must_use]
pub fn compare_url(&self, from: &Version, to: &Version) -> String {
format!(
"https://github.com/{}/compare/{}...{}",
self.base_repo(),
from,
to
)
}
}
impl fmt::Display for ActionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for ActionId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ActionId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Version(pub String);
impl Version {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn normalized(s: &str) -> Self {
if s.starts_with(|c: char| c.is_ascii_digit()) {
Self(format!("v{s}"))
} else {
Self(s.to_owned())
}
}
#[must_use]
pub fn highest(versions: &[Version]) -> Option<Version> {
versions
.iter()
.reduce(|a, b| if higher_version(a, b) == a { a } else { b })
.cloned()
}
#[must_use]
pub fn precision(&self) -> Option<VersionPrecision> {
let stripped = self
.0
.strip_prefix('v')
.or_else(|| self.0.strip_prefix('V'))
.unwrap_or(&self.0);
let base = stripped.split('-').next().unwrap_or(stripped);
let parts: Vec<&str> = base.split('.').collect();
match parts.as_slice() {
[major] if !major.is_empty() && major.chars().all(|c| c.is_ascii_digit()) => {
Some(VersionPrecision::Major)
}
[major, minor]
if !major.is_empty()
&& major.chars().all(|c| c.is_ascii_digit())
&& !minor.is_empty()
&& minor.chars().all(|c| c.is_ascii_digit()) =>
{
Some(VersionPrecision::Minor)
}
[major, minor, patch]
if !major.is_empty()
&& major.chars().all(|c| c.is_ascii_digit())
&& !minor.is_empty()
&& minor.chars().all(|c| c.is_ascii_digit())
&& !patch.is_empty()
&& patch.chars().all(|c| c.is_ascii_digit()) =>
{
Some(VersionPrecision::Patch)
}
_ => None,
}
}
#[must_use]
pub fn specifier(&self) -> Option<String> {
let stripped = self
.0
.strip_prefix('v')
.or_else(|| self.0.strip_prefix('V'))
.unwrap_or(&self.0);
match self.precision()? {
VersionPrecision::Major | VersionPrecision::Minor => Some(format!("^{stripped}")),
VersionPrecision::Patch => Some(format!("~{stripped}")),
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Version {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Version {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionPrecision {
Major,
Minor,
Patch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommitSha(pub String);
impl CommitSha {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_valid(s: &str) -> bool {
s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
}
}
impl fmt::Display for CommitSha {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for CommitSha {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CommitSha {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Repository(String);
impl Repository {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Repository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Repository {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Repository {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CommitDate(String);
impl CommitDate {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for CommitDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for CommitDate {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CommitDate {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::{ActionId, CommitSha, Version, VersionPrecision};
#[test]
fn action_id_base_repo() {
let simple = ActionId::from("actions/checkout");
assert_eq!(simple.base_repo().as_str(), "actions/checkout");
let subpath = ActionId::from("github/codeql-action/upload-sarif");
assert_eq!(subpath.base_repo().as_str(), "github/codeql-action");
}
#[test]
fn compare_url_builds_github_compare_link() {
let id = ActionId::from("actions/checkout");
assert_eq!(
id.compare_url(&Version::from("v6.0.1"), &Version::from("v6.0.3")),
"https://github.com/actions/checkout/compare/v6.0.1...v6.0.3"
);
}
#[test]
fn compare_url_collapses_subpath_to_base_repo() {
let id = ActionId::from("github/codeql-action/upload-sarif");
assert_eq!(
id.compare_url(&Version::from("v3.28.0"), &Version::from("v3.29.0")),
"https://github.com/github/codeql-action/compare/v3.28.0...v3.29.0"
);
}
#[test]
fn commit_sha_is_valid() {
assert!(CommitSha::is_valid(
"a1b2c3d4e5f6789012345678901234567890abcd"
));
assert!(CommitSha::is_valid(
"0000000000000000000000000000000000000000"
));
assert!(CommitSha::is_valid(
"ffffffffffffffffffffffffffffffffffffffff"
));
}
#[test]
fn commit_sha_is_valid_invalid_length() {
assert!(!CommitSha::is_valid("abc123")); assert!(!CommitSha::is_valid(
"a1b2c3d4e5f6789012345678901234567890abcde"
)); assert!(!CommitSha::is_valid("")); }
#[test]
fn commit_sha_is_valid_invalid_chars() {
assert!(!CommitSha::is_valid(
"g1b2c3d4e5f6789012345678901234567890abcd"
)); assert!(!CommitSha::is_valid(
"a1b2c3d4e5f6789012345678901234567890abc!"
)); }
#[test]
fn version_normalized_with_v_prefix() {
assert_eq!(Version::normalized("v4").as_str(), "v4");
assert_eq!(Version::normalized("v4.1.0").as_str(), "v4.1.0");
assert_eq!(Version::normalized("V4").as_str(), "V4");
}
#[test]
fn version_normalized_without_v_prefix() {
assert_eq!(Version::normalized("4").as_str(), "v4");
assert_eq!(Version::normalized("4.1.0").as_str(), "v4.1.0");
}
#[test]
fn precision_major() {
assert_eq!(
Version::from("v4").precision(),
Some(VersionPrecision::Major)
);
assert_eq!(
Version::from("v12").precision(),
Some(VersionPrecision::Major)
);
}
#[test]
fn precision_minor() {
assert_eq!(
Version::from("v4.1").precision(),
Some(VersionPrecision::Minor)
);
assert_eq!(
Version::from("v4.0").precision(),
Some(VersionPrecision::Minor)
);
}
#[test]
fn precision_patch() {
assert_eq!(
Version::from("v4.1.0").precision(),
Some(VersionPrecision::Patch)
);
assert_eq!(
Version::from("v4.1.2").precision(),
Some(VersionPrecision::Patch)
);
}
#[test]
fn precision_non_semver() {
assert!(Version::from("main").precision().is_none());
assert!(
Version::from("abc123def456789012345678901234567890abcd")
.precision()
.is_none()
);
assert!(Version::from("").precision().is_none());
}
#[test]
fn precision_prerelease_patch() {
assert_eq!(
Version::from("v3.0.0-beta.2").precision(),
Some(VersionPrecision::Patch)
);
}
#[test]
fn precision_prerelease_minor() {
assert_eq!(
Version::from("v3.0-rc.1").precision(),
Some(VersionPrecision::Minor)
);
}
#[test]
fn precision_prerelease_major() {
assert_eq!(
Version::from("v3-alpha").precision(),
Some(VersionPrecision::Major)
);
}
#[test]
fn specifier_major() {
assert_eq!(Version::from("v4").specifier(), Some("^4".to_owned()));
assert_eq!(Version::from("v12").specifier(), Some("^12".to_owned()));
}
#[test]
fn specifier_minor() {
assert_eq!(Version::from("v4.2").specifier(), Some("^4.2".to_owned()));
assert_eq!(Version::from("v4.0").specifier(), Some("^4.0".to_owned()));
}
#[test]
fn specifier_patch() {
assert_eq!(
Version::from("v4.1.0").specifier(),
Some("~4.1.0".to_owned())
);
assert_eq!(
Version::from("v4.1.2").specifier(),
Some("~4.1.2".to_owned())
);
}
#[test]
fn specifier_non_semver() {
assert!(Version::from("main").specifier().is_none());
assert!(
Version::from("abc123def456789012345678901234567890abcd")
.specifier()
.is_none()
);
}
#[test]
fn specifier_without_v_prefix() {
let v = Version::from("4.2");
assert_eq!(v.specifier(), Some("^4.2".to_owned()));
}
#[test]
fn specifier_prerelease_patch() {
assert_eq!(
Version::from("v3.0.0-beta.2").specifier(),
Some("~3.0.0-beta.2".to_owned())
);
}
#[test]
fn specifier_prerelease_minor() {
assert_eq!(
Version::from("v3.0-rc.1").specifier(),
Some("^3.0-rc.1".to_owned())
);
}
#[test]
fn specifier_prerelease_major() {
assert_eq!(
Version::from("v3-alpha").specifier(),
Some("^3-alpha".to_owned())
);
}
#[test]
fn version_specifier_uses_parse_semver() {
assert_eq!(
Version::highest(&[Version::from("v4"), Version::from("main")]),
Some(Version::from("v4"))
);
}
}