semver_php/constraint/
single.rs

1use super::{Bound, Constraint, Operator};
2use crate::version::version_compare;
3use std::{cmp::Ordering, fmt};
4
5/// A single version constraint (e.g., >=1.0.0, <2.0.0, ==1.5.0).
6#[derive(Debug, Clone)]
7pub struct SingleConstraint {
8	operator: Operator,
9	version: String,
10	pretty_string: Option<String>,
11}
12
13impl SingleConstraint {
14	/// Create a new single constraint.
15	pub fn new(operator: Operator, version: impl Into<String>) -> Self {
16		Self {
17			operator,
18			version: version.into(),
19			pretty_string: None,
20		}
21	}
22
23	/// Get the operator.
24	#[must_use]
25	pub const fn operator(&self) -> Operator {
26		self.operator
27	}
28
29	/// Get the version string.
30	#[must_use]
31	pub fn version(&self) -> &str {
32		&self.version
33	}
34
35	/// Check if this version is a dev branch (starts with "dev-").
36	#[must_use]
37	pub fn is_dev_branch(&self) -> bool {
38		self.version.starts_with("dev-")
39	}
40
41	/// Match against another single constraint.
42	/// This implements the core matching logic from PHP's `Constraint::matchSpecific`.
43	#[must_use]
44	pub fn match_specific(&self, other: &Self, compare_branches: bool) -> bool {
45		let self_is_branch = self.is_dev_branch();
46		let other_is_branch = other.is_dev_branch();
47
48		// Handle != operator specially
49		let is_ne = self.operator == Operator::Ne;
50		let is_other_ne = other.operator == Operator::Ne;
51
52		if is_ne || is_other_ne {
53			// Special case: != with dev branches
54			// If one side is != and other is not (== or !=) and the other is a dev branch: no match
55			if is_ne && !is_other_ne && other.operator != Operator::Eq && other_is_branch {
56				return false;
57			}
58			if is_other_ne && !is_ne && self.operator != Operator::Eq && self_is_branch {
59				return false;
60			}
61
62			// If neither is ==, there's always a solution for !=
63			if self.operator != Operator::Eq && other.operator != Operator::Eq {
64				return true;
65			}
66
67			// Otherwise compare versions (using version_compare for non-branches)
68			if self_is_branch || other_is_branch {
69				return self.version != other.version;
70			}
71			// Fall through to normal comparison
72		}
73
74		// Dev branches have special matching rules
75		if self_is_branch && other_is_branch {
76			// Both branches with == - match if same branch
77			if self.operator == Operator::Eq && other.operator == Operator::Eq {
78				return self.version == other.version;
79			}
80
81			// Any other combination with both branches doesn't match
82			return false;
83		}
84
85		// If one is a branch and the other isn't
86		if self_is_branch != other_is_branch {
87			// When branches are not comparable, dev branches never match anything
88			if !compare_branches {
89				return false;
90			}
91
92			// When compare_branches is true, use version_compare
93			// Fall through to normal comparison logic below
94		}
95
96		// Non-branch version comparisons
97
98		// Handle != operator specially
99		if self.operator == Operator::Ne {
100			// != matches anything except == to the same version
101			if other.operator == Operator::Eq {
102				return self.version != other.version;
103			}
104			return true;
105		}
106		if other.operator == Operator::Ne {
107			if self.operator == Operator::Eq {
108				return self.version != other.version;
109			}
110			return true;
111		}
112
113		// For comparison operators
114		let cmp = version_compare(&self.version, &other.version);
115
116		// Check if constraints can have any intersection
117		match (&self.operator, &other.operator) {
118			// Both equality - must be same version
119			(Operator::Eq, Operator::Eq) => cmp == Ordering::Equal,
120
121			// One side is equality - check if it satisfies the other constraint
122			(Operator::Eq, op) | (op, Operator::Eq) => {
123				let (eq_version, other_op, other_version) = if self.operator == Operator::Eq {
124					(&self.version, op, &other.version)
125				} else {
126					(&other.version, op, &self.version)
127				};
128
129				let cmp = version_compare(eq_version, other_version);
130				match other_op {
131					Operator::Lt => cmp == Ordering::Less,
132					Operator::Le => cmp != Ordering::Greater,
133					Operator::Gt => cmp == Ordering::Greater,
134					Operator::Ge => cmp != Ordering::Less,
135					Operator::Eq => cmp == Ordering::Equal,
136					Operator::Ne => cmp != Ordering::Equal,
137				}
138			},
139
140			// Both are range operators - check for intersection
141			// Any < or <= constraints always intersect, as do any > or >= constraints
142			(Operator::Lt | Operator::Le, Operator::Lt | Operator::Le)
143			| (Operator::Gt | Operator::Ge, Operator::Gt | Operator::Ge) => true,
144
145			// One upper bound, one lower bound - check for overlap
146			(Operator::Lt | Operator::Le, Operator::Gt) | (Operator::Lt, Operator::Ge) => {
147				cmp == Ordering::Greater
148			},
149			(Operator::Le, Operator::Ge) => cmp != Ordering::Less,
150
151			(Operator::Gt | Operator::Ge, Operator::Lt) | (Operator::Gt, Operator::Le) => {
152				cmp == Ordering::Less
153			},
154			(Operator::Ge, Operator::Le) => cmp != Ordering::Greater,
155
156			_ => false,
157		}
158	}
159
160	/// Extract the lower bound from this constraint.
161	fn extract_lower_bound(&self) -> Bound {
162		match self.operator {
163			Operator::Ne | Operator::Lt | Operator::Le => Bound::zero(),
164			Operator::Gt => Bound::new(&self.version, false),
165			Operator::Eq | Operator::Ge => Bound::new(&self.version, true),
166		}
167	}
168
169	/// Extract the upper bound from this constraint.
170	fn extract_upper_bound(&self) -> Bound {
171		match self.operator {
172			Operator::Ne | Operator::Gt | Operator::Ge => Bound::positive_infinity(),
173			Operator::Lt => Bound::new(&self.version, false),
174			Operator::Eq | Operator::Le => Bound::new(&self.version, true),
175		}
176	}
177}
178
179impl Constraint for SingleConstraint {
180	fn matches(&self, other: &dyn Constraint) -> bool {
181		// Try to match against specific types
182		if let Some(single) = other.as_single() {
183			return self.match_specific(single, false);
184		}
185
186		if other.is_match_all() {
187			return true;
188		}
189
190		if other.is_match_none() {
191			return false;
192		}
193
194		// For multi-constraints, delegate to their matching logic
195		if let Some(multi) = other.as_multi() {
196			return multi.matches(self);
197		}
198
199		// Default: use bounds-based matching
200		// This is a fallback and shouldn't normally be hit
201		true
202	}
203
204	fn lower_bound(&self) -> Bound {
205		self.extract_lower_bound()
206	}
207
208	fn upper_bound(&self) -> Bound {
209		self.extract_upper_bound()
210	}
211
212	fn set_pretty_string(&mut self, pretty: String) {
213		self.pretty_string = Some(pretty);
214	}
215
216	fn pretty_string(&self) -> String {
217		self.pretty_string
218			.clone()
219			.unwrap_or_else(|| self.to_string())
220	}
221
222	fn as_single(&self) -> Option<&SingleConstraint> {
223		Some(self)
224	}
225}
226
227impl fmt::Display for SingleConstraint {
228	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229		// Use == for display instead of =
230		let op = if self.operator == Operator::Eq {
231			"=="
232		} else {
233			self.operator.as_str()
234		};
235		write!(f, "{} {}", op, self.version)
236	}
237}
238
239#[cfg(test)]
240mod tests {
241	use super::*;
242
243	#[test]
244	fn test_single_constraint_creation() {
245		let c = SingleConstraint::new(Operator::Ge, "1.0.0");
246		assert_eq!(c.operator(), Operator::Ge);
247		assert_eq!(c.version(), "1.0.0");
248		assert!(!c.is_dev_branch());
249	}
250
251	#[test]
252	fn test_dev_branch_detection() {
253		let c = SingleConstraint::new(Operator::Eq, "dev-master");
254		assert!(c.is_dev_branch());
255
256		let c2 = SingleConstraint::new(Operator::Eq, "1.0.0");
257		assert!(!c2.is_dev_branch());
258	}
259
260	#[test]
261	fn test_basic_matching() {
262		let eq_2 = SingleConstraint::new(Operator::Eq, "2.0.0.0");
263		let lt_3 = SingleConstraint::new(Operator::Lt, "3.0.0.0");
264		assert!(eq_2.match_specific(&lt_3, false));
265
266		let gt_3 = SingleConstraint::new(Operator::Gt, "3.0.0.0");
267		assert!(!eq_2.match_specific(&gt_3, false));
268	}
269
270	#[test]
271	fn test_bounds() {
272		let c = SingleConstraint::new(Operator::Ge, "1.0.0");
273		let lower = c.lower_bound();
274		assert_eq!(lower.version(), "1.0.0");
275		assert!(lower.is_inclusive());
276
277		let upper = c.upper_bound();
278		assert!(upper.is_positive_infinity());
279	}
280}