From 7f0fd4f07fcb64a83fcfcc89ba52bd39aefee19e Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 16 May 2026 23:43:41 +0900 Subject: feat(port): port MultiConstraint.php --- .../src/constraint/multi_constraint.rs | 317 +++++++++++++++++++++ 1 file changed, 317 insertions(+) (limited to 'crates/shirabe-semver/src/constraint/multi_constraint.rs') diff --git a/crates/shirabe-semver/src/constraint/multi_constraint.rs b/crates/shirabe-semver/src/constraint/multi_constraint.rs index 99df52b..ff7fbfb 100644 --- a/crates/shirabe-semver/src/constraint/multi_constraint.rs +++ b/crates/shirabe-semver/src/constraint/multi_constraint.rs @@ -1 +1,318 @@ //! ref: composer/vendor/composer/semver/src/Constraint/MultiConstraint.php + +use std::cell::RefCell; + +use anyhow::bail; + +use crate::constraint::bound::Bound; +use crate::constraint::constraint_interface::ConstraintInterface; +use crate::constraint::match_all_constraint::MatchAllConstraint; + +pub struct MultiConstraint { + pub(crate) constraints: Vec>, + pub(crate) pretty_string: Option, + string: RefCell>, + pub(crate) conjunctive: bool, + lower_bound: RefCell>, + upper_bound: RefCell>, +} + +impl std::fmt::Debug for MultiConstraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MultiConstraint") + .field("conjunctive", &self.conjunctive) + .finish() + } +} + +impl MultiConstraint { + pub fn new( + constraints: Vec>, + conjunctive: bool, + ) -> anyhow::Result { + if constraints.len() < 2 { + bail!( + "Must provide at least two constraints for a MultiConstraint. Use \ + the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use \ + MultiConstraint::create() which optimizes and handles those cases automatically." + ); + } + + Ok(Self { + constraints, + pretty_string: None, + string: RefCell::new(None), + conjunctive, + lower_bound: RefCell::new(None), + upper_bound: RefCell::new(None), + }) + } + + pub fn get_constraints(&self) -> &[Box] { + &self.constraints + } + + pub fn is_conjunctive(&self) -> bool { + self.conjunctive + } + + pub fn is_disjunctive_mc(&self) -> bool { + !self.conjunctive + } + + fn extract_bounds(&self) { + if self.lower_bound.borrow().is_some() { + return; + } + + let mut current_lower: Option = None; + let mut current_upper: Option = None; + + for constraint in &self.constraints { + if current_lower.is_none() || current_upper.is_none() { + current_lower = Some(constraint.get_lower_bound()); + current_upper = Some(constraint.get_upper_bound()); + continue; + } + + let constraint_lower = constraint.get_lower_bound(); + let is_conj = self.is_conjunctive(); + if constraint_lower + .compare_to( + current_lower.as_ref().unwrap(), + if is_conj { ">" } else { "<" }, + ) + .expect("valid operator") + { + current_lower = Some(constraint_lower); + } + + let constraint_upper = constraint.get_upper_bound(); + if constraint_upper + .compare_to( + current_upper.as_ref().unwrap(), + if is_conj { "<" } else { ">" }, + ) + .expect("valid operator") + { + current_upper = Some(constraint_upper); + } + } + + *self.lower_bound.borrow_mut() = current_lower; + *self.upper_bound.borrow_mut() = current_upper; + } + + pub fn create( + constraints: Vec>, + conjunctive: bool, + ) -> anyhow::Result> { + if constraints.is_empty() { + return Ok(Box::new(MatchAllConstraint { pretty_string: None })); + } + + if constraints.len() == 1 { + return Ok(constraints.into_iter().next().unwrap()); + } + + let (constraints, conjunctive) = Self::optimize_constraints(constraints, conjunctive); + + if constraints.len() == 1 { + return Ok(constraints.into_iter().next().unwrap()); + } + + Ok(Box::new(MultiConstraint::new(constraints, conjunctive)?)) + } + + // Returns the (possibly optimized) constraints and the effective conjunctive flag. + // Always returns the constraints vector (consuming it), whether or not optimization was applied. + // The PHP version returns null for no optimization; here we return the original values unchanged. + fn optimize_constraints( + constraints: Vec>, + conjunctive: bool, + ) -> (Vec>, bool) { + // Parse the two OR groups and if they are contiguous collapse into one constraint. + // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4] + if !conjunctive { + let mut iter = constraints.into_iter(); + let mut left: Box = iter.next().unwrap(); + let mut merged_constraints: Vec> = Vec::new(); + let mut optimized = false; + + for right in iter { + let merged: Option> = { + let maybe_l_mc = left.as_any().downcast_ref::(); + let maybe_r_mc = right.as_any().downcast_ref::(); + + if let (Some(l_mc), Some(r_mc)) = (maybe_l_mc, maybe_r_mc) { + if l_mc.conjunctive + && r_mc.conjunctive + && l_mc.constraints.len() == 2 + && r_mc.constraints.len() == 2 + { + let left0 = l_mc.constraints[0].__to_string(); + let left1 = l_mc.constraints[1].__to_string(); + let right0 = r_mc.constraints[0].__to_string(); + let right1 = r_mc.constraints[1].__to_string(); + + if left0.starts_with(">=") + && left1.starts_with('<') + && right0.starts_with(">=") + && right1.starts_with('<') + && left1.get(2..) == right0.get(3..) + { + Some(Box::new( + MultiConstraint::new( + vec![ + l_mc.constraints[0].clone(), + r_mc.constraints[1].clone(), + ], + true, + ) + .unwrap(), + ) as Box) + } else { + None + } + } else { + None + } + } else { + None + } + }; + + if let Some(new_left) = merged { + optimized = true; + left = new_left; + } else { + merged_constraints.push(left); + left = right; + } + } + + merged_constraints.push(left); + + if optimized { + return (merged_constraints, false); + } + + return (merged_constraints, conjunctive); + } + + // TODO: Here's the place to put more optimizations + + (constraints, conjunctive) + } +} + +impl ConstraintInterface for MultiConstraint { + fn compile(&self, other_operator: i64) -> String { + let mut parts = Vec::new(); + for constraint in &self.constraints { + let code = constraint.compile(other_operator); + if code == "true" { + if !self.conjunctive { + return "true".to_string(); + } + } else if code == "false" { + if self.conjunctive { + return "false".to_string(); + } + } else { + parts.push(format!("({})", code)); + } + } + + if parts.is_empty() { + return if self.conjunctive { + "true".to_string() + } else { + "false".to_string() + }; + } + + if self.conjunctive { + parts.join("&&") + } else { + parts.join("||") + } + } + + fn matches(&self, provider: &dyn ConstraintInterface) -> bool { + if !self.conjunctive { + for constraint in &self.constraints { + if provider.matches(constraint.as_ref()) { + return true; + } + } + return false; + } + + if provider.is_disjunctive() { + return provider.matches(self); + } + + for constraint in &self.constraints { + if !provider.matches(constraint.as_ref()) { + return false; + } + } + + true + } + + fn set_pretty_string(&mut self, pretty_string: Option) { + self.pretty_string = pretty_string; + } + + fn get_pretty_string(&self) -> String { + if let Some(ref s) = self.pretty_string { + if !s.is_empty() { + return s.clone(); + } + } + self.__to_string() + } + + fn __to_string(&self) -> String { + if let Some(ref s) = *self.string.borrow() { + return s.clone(); + } + + let parts: Vec = self + .constraints + .iter() + .map(|c| c.__to_string()) + .collect(); + let sep = if self.conjunctive { " " } else { " || " }; + let result = format!("[{}]", parts.join(sep)); + + *self.string.borrow_mut() = Some(result.clone()); + result + } + + fn get_lower_bound(&self) -> Bound { + self.extract_bounds(); + self.lower_bound + .borrow() + .clone() + .expect("extractBounds should have populated the lowerBound property") + } + + fn get_upper_bound(&self) -> Bound { + self.extract_bounds(); + self.upper_bound + .borrow() + .clone() + .expect("extractBounds should have populated the upperBound property") + } + + fn is_disjunctive(&self) -> bool { + !self.conjunctive + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} -- cgit v1.3.1