blob: 2803bfb9fcec956f43d7954e23597762dfbeaf81 [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use clap::ValueEnum;
use semver::{Version, VersionReq};
/// How strictly to enforce semver compatibility.
#[derive(Copy, Clone, ValueEnum)]
pub enum SemverCompatibilityRule {
/// Ignore semantic version. Consider any two versions compatible.
Ignore,
/// Consider 0.x and 0.y to be compatible, but otherwise follow standard rules.
Loose,
/// Follow standard semantic version rules, under which 0.x and 0.y are incompatible.
Strict,
}
/// A trait for determining semver compatibility.
pub trait IsUpgradableTo {
/// Returns true if the object version is upgradable to 'other', according to
/// the specified semantic version compatibility strictness.
fn is_upgradable_to(
&self,
other: &Version,
semver_compatibility: SemverCompatibilityRule,
) -> bool;
}
impl IsUpgradableTo for semver::Version {
fn is_upgradable_to(
&self,
other: &Version,
semver_compatibility: SemverCompatibilityRule,
) -> bool {
other > self
&& VersionReq::parse(&self.to_string())
.is_ok_and(|req| req.matches_with_compatibility_rule(other, semver_compatibility))
}
}
/// A trait for custom semver compatibility logic, allowing it to be ignored or relaxed.
pub trait MatchesWithCompatibilityRule {
/// Returns true if the version matches the req, according to the
/// custom compatibility requirements of 'semver_compatibility'.
fn matches_with_compatibility_rule(
&self,
version: &Version,
semver_compatibility: SemverCompatibilityRule,
) -> bool;
}
impl MatchesWithCompatibilityRule for VersionReq {
fn matches_with_compatibility_rule(
&self,
version: &Version,
semver_compatibility: SemverCompatibilityRule,
) -> bool {
match semver_compatibility {
SemverCompatibilityRule::Ignore => true,
SemverCompatibilityRule::Loose => {
if self.comparators.len() == 1
&& self.comparators[0].major == 0
&& version.major == 0
{
let mut fake_v = version.clone();
fake_v.major = 1;
let mut fake_req = self.clone();
fake_req.comparators[0].major = 1;
fake_req.matches(&fake_v)
} else {
self.matches(version)
}
}
SemverCompatibilityRule::Strict => self.matches(version),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
#[test]
fn test_is_upgradable() -> Result<()> {
let version = Version::parse("2.3.4")?;
let patch = Version::parse("2.3.5")?;
let minor = Version::parse("2.4.0")?;
let major = Version::parse("3.0.0")?;
let older = Version::parse("2.3.3")?;
// All have same behavior for SemverCompatibility::LOOSE.
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Strict),
"Patch update, strict"
);
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Loose),
"Patch update, loose"
);
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Ignore),
"Patch update, ignore"
);
assert!(
version.is_upgradable_to(&minor, SemverCompatibilityRule::Strict),
"Minor version update, strict"
);
assert!(
version.is_upgradable_to(&minor, SemverCompatibilityRule::Loose),
"Minor version update, loose"
);
assert!(
version.is_upgradable_to(&minor, SemverCompatibilityRule::Ignore),
"Minor version update, ignore"
);
assert!(
!version.is_upgradable_to(&major, SemverCompatibilityRule::Strict),
"Incompatible (major version) update, strict"
);
assert!(
!version.is_upgradable_to(&major, SemverCompatibilityRule::Loose),
"Incompatible (major version) update, loose"
);
assert!(
version.is_upgradable_to(&major, SemverCompatibilityRule::Ignore),
"Incompatible (major version) update, ignore"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Strict),
"Downgrade, strict"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Loose),
"Downgrade, loose"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Ignore),
"Downgrade, ignore"
);
Ok(())
}
#[test]
fn test_is_upgradable_major_zero() -> Result<()> {
let version = Version::parse("0.3.4")?;
let patch = Version::parse("0.3.5")?;
let minor = Version::parse("0.4.0")?;
let major = Version::parse("1.0.0")?;
let older = Version::parse("0.3.3")?;
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Strict),
"Patch update, strict"
);
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Loose),
"Patch update, loose"
);
assert!(
version.is_upgradable_to(&patch, SemverCompatibilityRule::Ignore),
"Patch update, ignore"
);
// Different behavior for minor version changes.
assert!(
!version.is_upgradable_to(&minor, SemverCompatibilityRule::Strict),
"Minor version update, strict"
);
assert!(
version.is_upgradable_to(&minor, SemverCompatibilityRule::Loose),
"Minor version update, loose"
);
assert!(
version.is_upgradable_to(&minor, SemverCompatibilityRule::Ignore),
"Minor version update, ignore"
);
assert!(
!version.is_upgradable_to(&major, SemverCompatibilityRule::Strict),
"Incompatible (major version) update, strict"
);
assert!(
!version.is_upgradable_to(&major, SemverCompatibilityRule::Loose),
"Incompatible (major version) update, loose"
);
assert!(
version.is_upgradable_to(&major, SemverCompatibilityRule::Ignore),
"Incompatible (major version) update, ignore"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Strict),
"Downgrade, strict"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Loose),
"Downgrade, loose"
);
assert!(
!version.is_upgradable_to(&older, SemverCompatibilityRule::Ignore),
"Downgrade, ignore"
);
Ok(())
}
}