// 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(()) } }