semver_php/constraint/
multi.rs

1use super::{Bound, Constraint, MatchAllConstraint, MatchNoneConstraint};
2use std::fmt;
3
4/// A compound constraint combining multiple constraints with AND or OR.
5#[derive(Debug)]
6pub struct MultiConstraint {
7	constraints: Vec<Box<dyn Constraint>>,
8	conjunctive: bool, // true = AND, false = OR
9	pretty_string: Option<String>,
10}
11
12impl MultiConstraint {
13	/// Create a new multi-constraint.
14	/// `conjunctive` = true means AND (all must match), false means OR (any must match).
15	#[must_use]
16	pub fn new(constraints: Vec<Box<dyn Constraint>>, conjunctive: bool) -> Self {
17		Self {
18			constraints,
19			conjunctive,
20			pretty_string: None,
21		}
22	}
23
24	/// Smart constructor that optimizes and handles edge cases.
25	/// Returns appropriate type: `MatchAll`, `MatchNone`, Single, or Multi.
26	///
27	/// # Panics
28	/// Never panics - the unwrap is only reached when length is exactly 1.
29	#[must_use]
30	pub fn create(constraints: Vec<Box<dyn Constraint>>, conjunctive: bool) -> Box<dyn Constraint> {
31		let mut filtered: Vec<Box<dyn Constraint>> = Vec::new();
32
33		for c in constraints {
34			// Skip match-all in conjunctive (AND), skip match-none in disjunctive (OR)
35			if conjunctive && c.is_match_all() {
36				continue;
37			}
38			if !conjunctive && c.is_match_none() {
39				continue;
40			}
41
42			// Short-circuit: match-none in conjunctive = match-none
43			if conjunctive && c.is_match_none() {
44				return Box::new(MatchNoneConstraint::new());
45			}
46			// Short-circuit: match-all in disjunctive = match-all
47			if !conjunctive && c.is_match_all() {
48				return Box::new(MatchAllConstraint::new());
49			}
50
51			// Flatten nested multi-constraints of the same type
52			// TODO: implement proper cloning/flattening of nested multi-constraints
53			if let Some(multi) = c.as_multi() {
54				if multi.conjunctive == conjunctive {
55					// Would need to clone inner constraints here
56					// For now, just add the whole multi
57				}
58			}
59
60			filtered.push(c);
61		}
62
63		match filtered.len() {
64			0 => {
65				if conjunctive {
66					Box::new(MatchAllConstraint::new())
67				} else {
68					Box::new(MatchNoneConstraint::new())
69				}
70			},
71			1 => filtered.into_iter().next().unwrap(),
72			_ => Box::new(Self::new(filtered, conjunctive)),
73		}
74	}
75
76	/// Check if this is a conjunctive (AND) constraint.
77	#[must_use]
78	pub const fn is_conjunctive(&self) -> bool {
79		self.conjunctive
80	}
81
82	/// Check if this is a disjunctive (OR) constraint.
83	#[must_use]
84	pub const fn is_disjunctive(&self) -> bool {
85		!self.conjunctive
86	}
87
88	/// Get the inner constraints.
89	#[must_use]
90	pub fn constraints(&self) -> &[Box<dyn Constraint>] {
91		&self.constraints
92	}
93}
94
95impl Constraint for MultiConstraint {
96	fn matches(&self, other: &dyn Constraint) -> bool {
97		if other.is_match_none() {
98			return false;
99		}
100
101		if other.is_match_all() {
102			return true;
103		}
104
105		// For disjunctive multi-constraints as "other", we need special handling
106		if let Some(other_multi) = other.as_multi() {
107			if other_multi.is_disjunctive() {
108				// For disjunctive other, any of their constraints matching is enough
109				for other_c in &other_multi.constraints {
110					if self.matches(other_c.as_ref()) {
111						return true;
112					}
113				}
114				return false;
115			}
116		}
117
118		if self.conjunctive {
119			// AND: all constraints must match
120			self.constraints.iter().all(|c| c.matches(other))
121		} else {
122			// OR: at least one constraint must match
123			self.constraints.iter().any(|c| c.matches(other))
124		}
125	}
126
127	fn lower_bound(&self) -> Bound {
128		if self.constraints.is_empty() {
129			return Bound::zero();
130		}
131
132		if self.conjunctive {
133			// AND: take the highest (most restrictive) lower bound
134			self.constraints
135				.iter()
136				.map(|c| c.lower_bound())
137				.max_by(Bound::compare_as_lower)
138				.unwrap_or_else(Bound::zero)
139		} else {
140			// OR: take the lowest (least restrictive) lower bound
141			self.constraints
142				.iter()
143				.map(|c| c.lower_bound())
144				.min_by(Bound::compare_as_lower)
145				.unwrap_or_else(Bound::zero)
146		}
147	}
148
149	fn upper_bound(&self) -> Bound {
150		if self.constraints.is_empty() {
151			return Bound::positive_infinity();
152		}
153
154		if self.conjunctive {
155			// AND: take the lowest (most restrictive) upper bound
156			self.constraints
157				.iter()
158				.map(|c| c.upper_bound())
159				.min_by(Bound::compare_as_upper)
160				.unwrap_or_else(Bound::positive_infinity)
161		} else {
162			// OR: take the highest (least restrictive) upper bound
163			self.constraints
164				.iter()
165				.map(|c| c.upper_bound())
166				.max_by(Bound::compare_as_upper)
167				.unwrap_or_else(Bound::positive_infinity)
168		}
169	}
170
171	fn set_pretty_string(&mut self, pretty: String) {
172		self.pretty_string = Some(pretty);
173	}
174
175	fn pretty_string(&self) -> String {
176		self.pretty_string
177			.clone()
178			.unwrap_or_else(|| self.to_string())
179	}
180
181	fn as_multi(&self) -> Option<&MultiConstraint> {
182		Some(self)
183	}
184}
185
186impl fmt::Display for MultiConstraint {
187	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188		let sep = if self.conjunctive { " " } else { " || " };
189		let parts: Vec<String> = self.constraints.iter().map(ToString::to_string).collect();
190		write!(f, "[{}]", parts.join(sep))
191	}
192}
193
194impl Clone for MultiConstraint {
195	fn clone(&self) -> Self {
196		// We need to clone the boxed constraints
197		// For now, we'll create new boxes with the display representation
198		// This is a limitation - proper cloning would require Clone on dyn Constraint
199		Self {
200			constraints: Vec::new(), // TODO: implement proper cloning
201			conjunctive: self.conjunctive,
202			pretty_string: self.pretty_string.clone(),
203		}
204	}
205}
206
207#[cfg(test)]
208mod tests {
209	use super::*;
210	use crate::constraint::{Operator, SingleConstraint};
211
212	#[test]
213	fn test_conjunctive_matching() {
214		let c1 = Box::new(SingleConstraint::new(Operator::Gt, "1.0.0.0"));
215		let c2 = Box::new(SingleConstraint::new(Operator::Lt, "2.0.0.0"));
216		let multi = MultiConstraint::new(vec![c1, c2], true);
217
218		// 1.5.0 should match (> 1.0 AND < 2.0)
219		let v = SingleConstraint::new(Operator::Eq, "1.5.0.0");
220		assert!(multi.matches(&v));
221
222		// 0.5.0 should not match (not > 1.0)
223		let v2 = SingleConstraint::new(Operator::Eq, "0.5.0.0");
224		assert!(!multi.matches(&v2));
225	}
226
227	#[test]
228	fn test_disjunctive_matching() {
229		let c1 = Box::new(SingleConstraint::new(Operator::Lt, "1.0.0.0"));
230		let c2 = Box::new(SingleConstraint::new(Operator::Gt, "2.0.0.0"));
231		let multi = MultiConstraint::new(vec![c1, c2], false);
232
233		// 0.5.0 should match (< 1.0)
234		let v = SingleConstraint::new(Operator::Eq, "0.5.0.0");
235		assert!(multi.matches(&v));
236
237		// 3.0.0 should match (> 2.0)
238		let v2 = SingleConstraint::new(Operator::Eq, "3.0.0.0");
239		assert!(multi.matches(&v2));
240
241		// 1.5.0 should not match (not < 1.0 and not > 2.0)
242		let v3 = SingleConstraint::new(Operator::Eq, "1.5.0.0");
243		assert!(!multi.matches(&v3));
244	}
245
246	#[test]
247	fn test_create_optimization() {
248		// Empty conjunctive -> MatchAll
249		let result = MultiConstraint::create(vec![], true);
250		assert!(result.is_match_all());
251
252		// Empty disjunctive -> MatchNone
253		let result = MultiConstraint::create(vec![], false);
254		assert!(result.is_match_none());
255
256		// Single constraint -> unwrap
257		let c = Box::new(SingleConstraint::new(Operator::Eq, "1.0.0"));
258		let result = MultiConstraint::create(vec![c], true);
259		assert!(result.as_single().is_some());
260	}
261}