1use mago_php_version::PHPVersion;
2use mago_php_version::PHPVersionRange;
3use serde::Deserialize;
4use serde::Serialize;
5
6#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct VersionConstraint {
16 pub ranges: Vec<PHPVersionRange>,
18}
19
20impl VersionConstraint {
21 #[inline]
22 #[must_use]
23 pub const fn unconstrained() -> Self {
24 Self { ranges: Vec::new() }
25 }
26
27 #[inline]
28 #[must_use]
29 pub const fn is_unconstrained(&self) -> bool {
30 self.ranges.is_empty()
31 }
32
33 #[inline]
37 pub fn push_since(&mut self, version: PHPVersion) {
38 self.ranges.push(PHPVersionRange::from(version));
39 }
40
41 #[inline]
45 pub fn push_until(&mut self, version: PHPVersion) {
46 if let Some(last) = self.ranges.last_mut()
47 && last.max.is_none()
48 {
49 last.max = Some(version);
50 return;
51 }
52
53 self.ranges.push(PHPVersionRange::until(version));
54 }
55
56 #[inline]
59 #[must_use]
60 pub fn allows_version(&self, version: PHPVersion) -> bool {
61 if self.ranges.is_empty() {
62 return true;
63 }
64
65 self.ranges.iter().any(|r| r.includes(version))
66 }
67
68 #[inline]
72 #[must_use]
73 pub fn allows_version_range(&self, range: PHPVersionRange) -> bool {
74 if self.ranges.is_empty() {
75 return true;
76 }
77
78 let min = range.min.map_or(PHPVersion::from_version_id(0), |v| v);
79 let max = range.max.map_or(PHPVersion::from_version_id(u32::MAX), |v| v);
80
81 if min > max {
82 return true;
83 }
84
85 let mut sorted: Vec<&PHPVersionRange> = self.ranges.iter().collect();
86 sorted.sort_by_key(|r| r.min);
87
88 let mut next_required = min;
89 for r in sorted {
90 let r_min = r.min.unwrap_or(PHPVersion::from_version_id(0));
91 let r_max = r.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
92
93 if r_max < next_required {
95 continue;
96 }
97
98 if r_min > next_required {
102 return false;
103 }
104
105 if r_max >= max {
106 return true;
107 }
108
109 next_required = PHPVersion::from_version_id(r_max.to_version_id().saturating_add(1));
113 }
114
115 false
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn v(major: u32, minor: u32, patch: u32) -> PHPVersion {
124 PHPVersion::new(major, minor, patch)
125 }
126
127 #[test]
128 fn unconstrained_allows_everything() {
129 let c = VersionConstraint::unconstrained();
130 assert!(c.allows_version(v(7, 0, 0)));
131 assert!(c.allows_version(v(8, 5, 0)));
132 assert!(c.allows_version_range(PHPVersionRange::between(v(7, 0, 0), v(9, 0, 0))));
133 }
134
135 #[test]
136 fn since_only_open_on_the_right() {
137 let mut c = VersionConstraint::unconstrained();
138 c.push_since(v(8, 1, 0));
139 assert!(!c.allows_version(v(8, 0, 0)));
140 assert!(c.allows_version(v(8, 1, 0)));
141 assert!(c.allows_version(v(8, 5, 0)));
142 }
143
144 #[test]
145 fn until_only_open_on_the_left() {
146 let mut c = VersionConstraint::unconstrained();
147 c.push_until(v(8, 3, 0));
148 assert!(c.allows_version(v(7, 0, 0)));
149 assert!(c.allows_version(v(8, 3, 0)));
150 assert!(!c.allows_version(v(8, 4, 0)));
151 }
152
153 #[test]
154 fn since_then_until_closes_the_range() {
155 let mut c = VersionConstraint::unconstrained();
156 c.push_since(v(8, 1, 0));
157 c.push_until(v(8, 3, 0));
158 assert!(!c.allows_version(v(8, 0, 0)));
159 assert!(c.allows_version(v(8, 1, 0)));
160 assert!(c.allows_version(v(8, 3, 0)));
161 assert!(!c.allows_version(v(8, 4, 0)));
162 }
163
164 #[test]
165 fn disjoint_ranges_compose() {
166 let mut c = VersionConstraint::unconstrained();
167 c.push_since(v(8, 1, 0));
168 c.push_until(v(8, 3, 0));
169 c.push_since(v(8, 5, 0));
170
171 assert!(!c.allows_version(v(8, 0, 0)));
172 assert!(c.allows_version(v(8, 1, 0)));
173 assert!(c.allows_version(v(8, 3, 0)));
174 assert!(!c.allows_version(v(8, 4, 0)));
175 assert!(c.allows_version(v(8, 5, 0)));
176 assert!(c.allows_version(v(9, 0, 0)));
177 }
178
179 #[test]
180 fn range_query_requires_full_coverage() {
181 let mut c = VersionConstraint::unconstrained();
182 c.push_since(v(8, 1, 0));
183 c.push_until(v(8, 3, 0));
184 c.push_since(v(8, 5, 0));
185
186 assert!(!c.allows_version_range(PHPVersionRange::between(v(8, 0, 0), v(8, 7, 0))));
188 assert!(c.allows_version_range(PHPVersionRange::between(v(8, 5, 0), v(8, 7, 0))));
190 assert!(c.allows_version_range(PHPVersionRange::between(v(8, 1, 0), v(8, 3, 0))));
192 }
193}