1use mago_php_version::PHPVersion;
2use mago_php_version::PHPVersionRange;
3
4#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct VersionConstraint {
15 pub ranges: Vec<PHPVersionRange>,
17}
18
19impl VersionConstraint {
20 #[inline]
21 #[must_use]
22 pub const fn unconstrained() -> Self {
23 Self { ranges: Vec::new() }
24 }
25
26 #[inline]
27 #[must_use]
28 pub const fn is_unconstrained(&self) -> bool {
29 self.ranges.is_empty()
30 }
31
32 #[inline]
36 pub fn push_since(&mut self, version: PHPVersion) {
37 self.ranges.push(PHPVersionRange::from(version));
38 }
39
40 #[inline]
44 pub fn push_until(&mut self, version: PHPVersion) {
45 if let Some(last) = self.ranges.last_mut()
46 && last.max.is_none()
47 {
48 last.max = Some(version);
49 return;
50 }
51
52 self.ranges.push(PHPVersionRange::until(version));
53 }
54
55 #[inline]
57 pub fn merge(&mut self, other: VersionConstraint) {
58 if self.is_unconstrained() || other.is_unconstrained() {
59 self.ranges.clear();
60 return;
61 }
62
63 self.ranges.extend(other.ranges);
64 }
65
66 #[inline]
69 #[must_use]
70 pub fn allows_version(&self, version: PHPVersion) -> bool {
71 if self.ranges.is_empty() {
72 return true;
73 }
74
75 self.ranges.iter().any(|r| r.includes(version))
76 }
77
78 #[inline]
82 #[must_use]
83 pub fn allows_version_range(&self, range: PHPVersionRange) -> bool {
84 if self.ranges.is_empty() {
85 return true;
86 }
87
88 let min = range.min.unwrap_or(PHPVersion::from_version_id(0));
89 let max = range.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
90
91 if min > max {
92 return true;
93 }
94
95 let mut sorted: Vec<&PHPVersionRange> = self.ranges.iter().collect();
96 sorted.sort_by_key(|r| r.min);
97
98 let mut next_required = min;
99 for r in sorted {
100 let r_min = r.min.unwrap_or(PHPVersion::from_version_id(0));
101 let r_max = r.max.unwrap_or(PHPVersion::from_version_id(u32::MAX));
102
103 if r_max < next_required {
105 continue;
106 }
107
108 if r_min > next_required {
112 return false;
113 }
114
115 if r_max >= max {
116 return true;
117 }
118
119 next_required = PHPVersion::from_version_id(r_max.to_version_id().saturating_add(1));
123 }
124
125 false
126 }
127}
128
129#[cfg(test)]
130mod tests {
131
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}