1use crate::Version;
11use serde::{Deserialize, Serialize};
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "kind", rename_all = "kebab-case")]
18pub enum ConstraintSpec {
19 Exact(Version),
21 Range {
23 lower_inclusive: Version,
24 upper_exclusive: Version,
25 },
26 Tilde(Version),
29 Caret(Version),
33 GreaterEqual(Version),
35 Greater(Version),
37 LessEqual(Version),
39 Less(Version),
41 Any,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct CompoundConstraint {
54 pub combinator: Combinator,
55 pub atoms: Vec<ConstraintSpec>,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub enum Combinator {
61 Or,
63 And,
66}
67
68impl CompoundConstraint {
69 #[must_use]
70 pub fn matches(&self, v: &Version) -> bool {
71 match self.combinator {
72 Combinator::Or => self.atoms.iter().any(|a| match_constraint(a, v)),
73 Combinator::And => self.atoms.iter().all(|a| match_constraint(a, v)),
74 }
75 }
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct VersionConstraint {
83 pub spec: ConstraintSpec,
84 #[serde(default)]
88 pub native_syntax: Option<String>,
89}
90
91impl VersionConstraint {
92 #[must_use]
95 pub const fn from_spec(spec: ConstraintSpec) -> Self {
96 Self {
97 spec,
98 native_syntax: None,
99 }
100 }
101
102 #[must_use]
105 pub fn matches(&self, v: &Version) -> bool {
106 match_constraint(&self.spec, v)
107 }
108}
109
110fn match_constraint(c: &ConstraintSpec, v: &Version) -> bool {
111 match c {
112 ConstraintSpec::Exact(target) => v == target,
113 ConstraintSpec::Range {
114 lower_inclusive,
115 upper_exclusive,
116 } => v >= lower_inclusive && v < upper_exclusive,
117 ConstraintSpec::Tilde(base) => {
118 v >= base && v < &Version::new(base.major, base.minor + 1, 0)
120 }
121 ConstraintSpec::Caret(base) => {
122 if base.major > 0 {
126 v >= base && v < &Version::new(base.major + 1, 0, 0)
127 } else if base.minor > 0 {
128 v >= base && v < &Version::new(0, base.minor + 1, 0)
129 } else {
130 v >= base && v < &Version::new(0, 0, base.patch + 1)
131 }
132 }
133 ConstraintSpec::GreaterEqual(target) => v >= target,
134 ConstraintSpec::Greater(target) => v > target,
135 ConstraintSpec::LessEqual(target) => v <= target,
136 ConstraintSpec::Less(target) => v < target,
137 ConstraintSpec::Any => true,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn exact_matches_only_exact_version() {
147 let c = VersionConstraint::from_spec(ConstraintSpec::Exact(Version::new(1, 2, 3)));
148 assert!(c.matches(&Version::new(1, 2, 3)));
149 assert!(!c.matches(&Version::new(1, 2, 4)));
150 }
151
152 #[test]
153 fn caret_major_above_zero() {
154 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(1, 2, 3)));
155 assert!(c.matches(&Version::new(1, 2, 3)));
156 assert!(c.matches(&Version::new(1, 99, 0)));
157 assert!(!c.matches(&Version::new(2, 0, 0)));
158 assert!(!c.matches(&Version::new(1, 2, 2)));
159 }
160
161 #[test]
162 fn caret_major_zero_minor_above_zero() {
163 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 2, 3)));
165 assert!(c.matches(&Version::new(0, 2, 3)));
166 assert!(c.matches(&Version::new(0, 2, 99)));
167 assert!(!c.matches(&Version::new(0, 3, 0)));
168 }
169
170 #[test]
171 fn caret_major_and_minor_zero() {
172 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 0, 3)));
174 assert!(c.matches(&Version::new(0, 0, 3)));
175 assert!(!c.matches(&Version::new(0, 0, 4)));
176 }
177
178 #[test]
179 fn tilde_only_allows_patch_changes() {
180 let c = VersionConstraint::from_spec(ConstraintSpec::Tilde(Version::new(1, 2, 3)));
182 assert!(c.matches(&Version::new(1, 2, 3)));
183 assert!(c.matches(&Version::new(1, 2, 99)));
184 assert!(!c.matches(&Version::new(1, 3, 0)));
185 }
186
187 #[test]
188 fn range_inclusive_lower_exclusive_upper() {
189 let c = VersionConstraint::from_spec(ConstraintSpec::Range {
190 lower_inclusive: Version::new(1, 2, 3),
191 upper_exclusive: Version::new(2, 0, 0),
192 });
193 assert!(c.matches(&Version::new(1, 2, 3)));
194 assert!(c.matches(&Version::new(1, 99, 99)));
195 assert!(!c.matches(&Version::new(2, 0, 0)));
196 assert!(!c.matches(&Version::new(1, 2, 2)));
197 }
198
199 #[test]
200 fn open_bounds() {
201 let ge = VersionConstraint::from_spec(ConstraintSpec::GreaterEqual(Version::new(1, 0, 0)));
202 let gt = VersionConstraint::from_spec(ConstraintSpec::Greater(Version::new(1, 0, 0)));
203 let le = VersionConstraint::from_spec(ConstraintSpec::LessEqual(Version::new(1, 0, 0)));
204 let lt = VersionConstraint::from_spec(ConstraintSpec::Less(Version::new(1, 0, 0)));
205 assert!(ge.matches(&Version::new(1, 0, 0)));
206 assert!(!gt.matches(&Version::new(1, 0, 0)));
207 assert!(le.matches(&Version::new(1, 0, 0)));
208 assert!(!lt.matches(&Version::new(1, 0, 0)));
209 }
210
211 #[test]
212 fn any_matches_everything() {
213 let c = VersionConstraint::from_spec(ConstraintSpec::Any);
214 assert!(c.matches(&Version::new(0, 0, 0)));
215 assert!(c.matches(&Version::new(999, 999, 999)));
216 }
217
218 #[test]
219 fn disjunction_via_compound() {
220 let c = CompoundConstraint {
221 combinator: Combinator::Or,
222 atoms: vec![
223 ConstraintSpec::Caret(Version::new(1, 0, 0)),
224 ConstraintSpec::Caret(Version::new(2, 0, 0)),
225 ],
226 };
227 assert!(c.matches(&Version::new(1, 5, 0)));
228 assert!(c.matches(&Version::new(2, 5, 0)));
229 assert!(!c.matches(&Version::new(3, 0, 0)));
230 }
231
232 #[test]
233 fn conjunction_via_compound() {
234 let c = CompoundConstraint {
235 combinator: Combinator::And,
236 atoms: vec![
237 ConstraintSpec::GreaterEqual(Version::new(1, 2, 0)),
238 ConstraintSpec::Less(Version::new(2, 0, 0)),
239 ],
240 };
241 assert!(c.matches(&Version::new(1, 5, 0)));
242 assert!(!c.matches(&Version::new(1, 1, 0)));
243 assert!(!c.matches(&Version::new(2, 0, 0)));
244 }
245
246 #[test]
247 fn round_trip_through_serde() {
248 let c = VersionConstraint {
249 spec: ConstraintSpec::Caret(Version::new(1, 2, 3)),
250 native_syntax: Some("^1.2.3".to_string()),
251 };
252 let j = serde_json::to_string(&c).unwrap();
253 let parsed: VersionConstraint = serde_json::from_str(&j).unwrap();
254 assert_eq!(c, parsed);
255 }
256}