semver_php/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2#![doc = include_str!("../README.md")]
3
4pub mod comparator;
5pub mod constraint;
6pub mod error;
7pub mod interval;
8pub mod intervals;
9pub mod parser;
10pub mod version;
11
12pub use comparator::{
13	compare, equal_to, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to,
14	not_equal_to,
15};
16pub use constraint::{
17	Bound, Constraint, MatchAllConstraint, MatchNoneConstraint, MultiConstraint, Operator,
18	SingleConstraint,
19};
20pub use error::{Result, SemverError};
21pub use interval::{BranchConstraint, Interval, IntervalResult};
22pub use intervals::Intervals;
23pub use parser::VersionParser;
24pub use version::{expand_stability, stability_order, version_compare, Stability};
25
26/// Provides convenient methods for version comparison, constraint matching, and sorting.
27pub struct Semver;
28
29impl Semver {
30	/// Check if a version satisfies a constraint string.
31	///
32	/// # Arguments
33	/// * `version` - Version string to check
34	/// * `constraints` - Constraint expression (e.g., "^1.0", ">=2.0 <3.0", "~1.2 || ^2.0")
35	///
36	/// # Errors
37	/// Returns an error if the version or constraint string is invalid.
38	///
39	/// # Examples
40	/// ```
41	/// use semver_php::Semver;
42	///
43	/// assert!(Semver::satisfies("1.2.3", "^1.0").unwrap());
44	/// assert!(Semver::satisfies("1.2.3", ">=1.0 <2.0").unwrap());
45	/// assert!(!Semver::satisfies("2.0.0", "^1.0").unwrap());
46	/// ```
47	pub fn satisfies(version: &str, constraints: &str) -> Result<bool> {
48		let normalized = VersionParser::normalize(version)?;
49		let provider = SingleConstraint::new(Operator::Eq, &normalized);
50		let parsed_constraints = VersionParser::parse_constraints(constraints)?;
51
52		Ok(parsed_constraints.matches(&provider))
53	}
54
55	/// Filter a list of versions to those that satisfy a constraint.
56	///
57	/// # Arguments
58	/// * `versions` - List of version strings
59	/// * `constraints` - Constraint expression
60	///
61	/// # Errors
62	/// Returns an error if any version or the constraint string is invalid.
63	///
64	/// # Examples
65	/// ```
66	/// use semver_php::Semver;
67	///
68	/// let versions = vec!["1.0", "1.2", "2.0", "3.0"];
69	/// let satisfied = Semver::satisfied_by(&versions, "~1.0").unwrap();
70	/// assert_eq!(satisfied, vec!["1.0", "1.2"]);
71	/// ```
72	pub fn satisfied_by(versions: &[&str], constraints: &str) -> Result<Vec<String>> {
73		let mut result = Vec::new();
74		for version in versions {
75			if Self::satisfies(version, constraints)? {
76				result.push((*version).to_string());
77			}
78		}
79		Ok(result)
80	}
81
82	/// Sort versions in ascending order.
83	///
84	/// # Errors
85	/// Returns an error if any version string is invalid.
86	///
87	/// # Examples
88	/// ```
89	/// use semver_php::Semver;
90	///
91	/// let versions = vec!["1.0", "0.1", "3.2.1", "2.4.0"];
92	/// let sorted = Semver::sort(&versions).unwrap();
93	/// assert_eq!(sorted, vec!["0.1", "1.0", "2.4.0", "3.2.1"]);
94	/// ```
95	pub fn sort(versions: &[&str]) -> Result<Vec<String>> {
96		Self::usort(versions, true)
97	}
98
99	/// Sort versions in descending order.
100	///
101	/// # Errors
102	/// Returns an error if any version string is invalid.
103	///
104	/// # Examples
105	/// ```
106	/// use semver_php::Semver;
107	///
108	/// let versions = vec!["1.0", "0.1", "3.2.1", "2.4.0"];
109	/// let sorted = Semver::rsort(&versions).unwrap();
110	/// assert_eq!(sorted, vec!["3.2.1", "2.4.0", "1.0", "0.1"]);
111	/// ```
112	pub fn rsort(versions: &[&str]) -> Result<Vec<String>> {
113		Self::usort(versions, false)
114	}
115
116	fn usort(versions: &[&str], ascending: bool) -> Result<Vec<String>> {
117		// Normalize all versions and store with original index
118		let mut normalized: Vec<(String, usize)> = Vec::with_capacity(versions.len());
119		for (idx, version) in versions.iter().enumerate() {
120			let mut norm = VersionParser::normalize(version)?;
121			#[allow(deprecated)]
122			{
123				norm = VersionParser::normalize_default_branch(&norm);
124			}
125			normalized.push((norm, idx));
126		}
127
128		// Sort by normalized version using version_compare directly
129		// (avoids redundant normalization that less_than would do)
130		normalized.sort_by(|(a, _), (b, _)| {
131			let cmp = version_compare(a, b);
132			if ascending {
133				cmp
134			} else {
135				cmp.reverse()
136			}
137		});
138
139		// Return original versions in sorted order
140		Ok(normalized
141			.into_iter()
142			.map(|(_, idx)| versions[idx].to_string())
143			.collect())
144	}
145}
146
147#[cfg(test)]
148mod tests {
149	use super::*;
150
151	#[test]
152	fn test_basic_constraint() {
153		let c = SingleConstraint::new(Operator::Ge, "1.0.0.0");
154		assert_eq!(c.operator(), Operator::Ge);
155		assert_eq!(c.version(), "1.0.0.0");
156	}
157
158	#[test]
159	fn test_match_all() {
160		let all = MatchAllConstraint::new();
161		let c = SingleConstraint::new(Operator::Eq, "1.0.0");
162		assert!(all.matches(&c));
163	}
164
165	#[test]
166	fn test_match_none() {
167		let none = MatchNoneConstraint::new();
168		let c = SingleConstraint::new(Operator::Eq, "1.0.0");
169		assert!(!none.matches(&c));
170	}
171}