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]
58 pub fn merge(&mut self, other: VersionConstraint) {
59 if self.is_unconstrained() || other.is_unconstrained() {
60 self.ranges.clear();
61 return;
62 }
63
64 self.ranges.extend(other.ranges);
65 }
66
67 #[inline]
70 #[must_use]
71 pub fn allows_version(&self, version: PHPVersion) -> bool {
72 if self.ranges.is_empty() {
73 return true;
74 }
75
76 self.ranges.iter().any(|r| r.includes(version))
77 }
78
79 #[inline]
83 #[must_use]
84 pub fn allows_version_range(&self, range: PHPVersionRange) -> bool {
85 if self.ranges.is_empty() {
86 return true;
87 }
88
89 let min = range.min.map_or(PHPVersion::from_version_id(0), |v| v);
90 let max = range.max.map_or(PHPVersion::from_version_id(u32::MAX), |v| v);
91
92 if min > max {
93 return true;
94 }
95
96 let mut sorted: Vec<&PHPVersionRange> = self.ranges.iter().collect();
97 sorted.sort_by_key(|r| r.min);
98
99 let mut next_required = min;
100 for r in sorted {
101 let r_min = r.min.unwrap_or(PHPVersion::from_version_id(0));
102 let r_max = r.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
103
104 if r_max < next_required {
106 continue;
107 }
108
109 if r_min > next_required {
113 return false;
114 }
115
116 if r_max >= max {
117 return true;
118 }
119
120 next_required = PHPVersion::from_version_id(r_max.to_version_id().saturating_add(1));
124 }
125
126 false
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn v(major: u32, minor: u32, patch: u32) -> PHPVersion {
135 PHPVersion::new(major, minor, patch)
136 }
137
138 #[test]
139 fn unconstrained_allows_everything() {
140 let c = VersionConstraint::unconstrained();
141 assert!(c.allows_version(v(7, 0, 0)));
142 assert!(c.allows_version(v(8, 5, 0)));
143 assert!(c.allows_version_range(PHPVersionRange::between(v(7, 0, 0), v(9, 0, 0))));
144 }
145
146 #[test]
147 fn since_only_open_on_the_right() {
148 let mut c = VersionConstraint::unconstrained();
149 c.push_since(v(8, 1, 0));
150 assert!(!c.allows_version(v(8, 0, 0)));
151 assert!(c.allows_version(v(8, 1, 0)));
152 assert!(c.allows_version(v(8, 5, 0)));
153 }
154
155 #[test]
156 fn until_only_open_on_the_left() {
157 let mut c = VersionConstraint::unconstrained();
158 c.push_until(v(8, 3, 0));
159 assert!(c.allows_version(v(7, 0, 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 since_then_until_closes_the_range() {
166 let mut c = VersionConstraint::unconstrained();
167 c.push_since(v(8, 1, 0));
168 c.push_until(v(8, 3, 0));
169 assert!(!c.allows_version(v(8, 0, 0)));
170 assert!(c.allows_version(v(8, 1, 0)));
171 assert!(c.allows_version(v(8, 3, 0)));
172 assert!(!c.allows_version(v(8, 4, 0)));
173 }
174
175 #[test]
176 fn disjoint_ranges_compose() {
177 let mut c = VersionConstraint::unconstrained();
178 c.push_since(v(8, 1, 0));
179 c.push_until(v(8, 3, 0));
180 c.push_since(v(8, 5, 0));
181
182 assert!(!c.allows_version(v(8, 0, 0)));
183 assert!(c.allows_version(v(8, 1, 0)));
184 assert!(c.allows_version(v(8, 3, 0)));
185 assert!(!c.allows_version(v(8, 4, 0)));
186 assert!(c.allows_version(v(8, 5, 0)));
187 assert!(c.allows_version(v(9, 0, 0)));
188 }
189
190 #[test]
191 fn range_query_requires_full_coverage() {
192 let mut c = VersionConstraint::unconstrained();
193 c.push_since(v(8, 1, 0));
194 c.push_until(v(8, 3, 0));
195 c.push_since(v(8, 5, 0));
196
197 assert!(!c.allows_version_range(PHPVersionRange::between(v(8, 0, 0), v(8, 7, 0))));
199 assert!(c.allows_version_range(PHPVersionRange::between(v(8, 5, 0), v(8, 7, 0))));
201 assert!(c.allows_version_range(PHPVersionRange::between(v(8, 1, 0), v(8, 3, 0))));
203 }
204}