use mago_php_version::PHPVersion;
use mago_php_version::PHPVersionRange;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VersionConstraint {
pub ranges: Vec<PHPVersionRange>,
}
impl VersionConstraint {
#[inline]
#[must_use]
pub const fn unconstrained() -> Self {
Self { ranges: Vec::new() }
}
#[inline]
#[must_use]
pub const fn is_unconstrained(&self) -> bool {
self.ranges.is_empty()
}
#[inline]
pub fn push_since(&mut self, version: PHPVersion) {
self.ranges.push(PHPVersionRange::from(version));
}
#[inline]
pub fn push_until(&mut self, version: PHPVersion) {
if let Some(last) = self.ranges.last_mut()
&& last.max.is_none()
{
last.max = Some(version);
return;
}
self.ranges.push(PHPVersionRange::until(version));
}
#[inline]
pub fn merge(&mut self, other: VersionConstraint) {
if self.is_unconstrained() || other.is_unconstrained() {
self.ranges.clear();
return;
}
self.ranges.extend(other.ranges);
}
#[inline]
#[must_use]
pub fn allows_version(&self, version: PHPVersion) -> bool {
if self.ranges.is_empty() {
return true;
}
self.ranges.iter().any(|r| r.includes(version))
}
#[inline]
#[must_use]
pub fn allows_version_range(&self, range: PHPVersionRange) -> bool {
if self.ranges.is_empty() {
return true;
}
let min = range.min.unwrap_or(PHPVersion::from_version_id(0));
let max = range.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
if min > max {
return true;
}
let mut sorted: Vec<&PHPVersionRange> = self.ranges.iter().collect();
sorted.sort_by_key(|r| r.min);
let mut next_required = min;
for r in sorted {
let r_min = r.min.unwrap_or(PHPVersion::from_version_id(0));
let r_max = r.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
if r_max < next_required {
continue;
}
if r_min > next_required {
return false;
}
if r_max >= max {
return true;
}
next_required = PHPVersion::from_version_id(r_max.to_version_id().saturating_add(1));
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(major: u32, minor: u32, patch: u32) -> PHPVersion {
PHPVersion::new(major, minor, patch)
}
#[test]
fn unconstrained_allows_everything() {
let c = VersionConstraint::unconstrained();
assert!(c.allows_version(v(7, 0, 0)));
assert!(c.allows_version(v(8, 5, 0)));
assert!(c.allows_version_range(PHPVersionRange::between(v(7, 0, 0), v(9, 0, 0))));
}
#[test]
fn since_only_open_on_the_right() {
let mut c = VersionConstraint::unconstrained();
c.push_since(v(8, 1, 0));
assert!(!c.allows_version(v(8, 0, 0)));
assert!(c.allows_version(v(8, 1, 0)));
assert!(c.allows_version(v(8, 5, 0)));
}
#[test]
fn until_only_open_on_the_left() {
let mut c = VersionConstraint::unconstrained();
c.push_until(v(8, 3, 0));
assert!(c.allows_version(v(7, 0, 0)));
assert!(c.allows_version(v(8, 3, 0)));
assert!(!c.allows_version(v(8, 4, 0)));
}
#[test]
fn since_then_until_closes_the_range() {
let mut c = VersionConstraint::unconstrained();
c.push_since(v(8, 1, 0));
c.push_until(v(8, 3, 0));
assert!(!c.allows_version(v(8, 0, 0)));
assert!(c.allows_version(v(8, 1, 0)));
assert!(c.allows_version(v(8, 3, 0)));
assert!(!c.allows_version(v(8, 4, 0)));
}
#[test]
fn disjoint_ranges_compose() {
let mut c = VersionConstraint::unconstrained();
c.push_since(v(8, 1, 0));
c.push_until(v(8, 3, 0));
c.push_since(v(8, 5, 0));
assert!(!c.allows_version(v(8, 0, 0)));
assert!(c.allows_version(v(8, 1, 0)));
assert!(c.allows_version(v(8, 3, 0)));
assert!(!c.allows_version(v(8, 4, 0)));
assert!(c.allows_version(v(8, 5, 0)));
assert!(c.allows_version(v(9, 0, 0)));
}
#[test]
fn range_query_requires_full_coverage() {
let mut c = VersionConstraint::unconstrained();
c.push_since(v(8, 1, 0));
c.push_until(v(8, 3, 0));
c.push_since(v(8, 5, 0));
assert!(!c.allows_version_range(PHPVersionRange::between(v(8, 0, 0), v(8, 7, 0))));
assert!(c.allows_version_range(PHPVersionRange::between(v(8, 5, 0), v(8, 7, 0))));
assert!(c.allows_version_range(PHPVersionRange::between(v(8, 1, 0), v(8, 3, 0))));
}
}