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 { lower_inclusive: Version, upper_exclusive: Version },
23 Tilde(Version),
26 Caret(Version),
30 GreaterEqual(Version),
32 Greater(Version),
34 LessEqual(Version),
36 Less(Version),
38 Any,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CompoundConstraint {
51 pub combinator: Combinator,
52 pub atoms: Vec<ConstraintSpec>,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "kebab-case")]
57pub enum Combinator {
58 Or,
60 And,
63}
64
65impl CompoundConstraint {
66 #[must_use]
67 pub fn matches(&self, v: &Version) -> bool {
68 match self.combinator {
69 Combinator::Or => self.atoms.iter().any(|a| match_constraint(a, v)),
70 Combinator::And => self.atoms.iter().all(|a| match_constraint(a, v)),
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub struct VersionConstraint {
80 pub spec: ConstraintSpec,
81 #[serde(default)]
85 pub native_syntax: Option<String>,
86}
87
88impl VersionConstraint {
89 #[must_use]
92 pub const fn from_spec(spec: ConstraintSpec) -> Self {
93 Self {
94 spec,
95 native_syntax: None,
96 }
97 }
98
99 #[must_use]
102 pub fn matches(&self, v: &Version) -> bool {
103 match_constraint(&self.spec, v)
104 }
105}
106
107fn match_constraint(c: &ConstraintSpec, v: &Version) -> bool {
108 match c {
109 ConstraintSpec::Exact(target) => v == target,
110 ConstraintSpec::Range { lower_inclusive, upper_exclusive } => {
111 v >= lower_inclusive && v < upper_exclusive
112 }
113 ConstraintSpec::Tilde(base) => {
114 v >= base
116 && v < &Version::new(base.major, base.minor + 1, 0)
117 }
118 ConstraintSpec::Caret(base) => {
119 if base.major > 0 {
123 v >= base && v < &Version::new(base.major + 1, 0, 0)
124 } else if base.minor > 0 {
125 v >= base && v < &Version::new(0, base.minor + 1, 0)
126 } else {
127 v >= base && v < &Version::new(0, 0, base.patch + 1)
128 }
129 }
130 ConstraintSpec::GreaterEqual(target) => v >= target,
131 ConstraintSpec::Greater(target) => v > target,
132 ConstraintSpec::LessEqual(target) => v <= target,
133 ConstraintSpec::Less(target) => v < target,
134 ConstraintSpec::Any => true,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn exact_matches_only_exact_version() {
144 let c = VersionConstraint::from_spec(ConstraintSpec::Exact(Version::new(1, 2, 3)));
145 assert!(c.matches(&Version::new(1, 2, 3)));
146 assert!(!c.matches(&Version::new(1, 2, 4)));
147 }
148
149 #[test]
150 fn caret_major_above_zero() {
151 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(1, 2, 3)));
152 assert!(c.matches(&Version::new(1, 2, 3)));
153 assert!(c.matches(&Version::new(1, 99, 0)));
154 assert!(!c.matches(&Version::new(2, 0, 0)));
155 assert!(!c.matches(&Version::new(1, 2, 2)));
156 }
157
158 #[test]
159 fn caret_major_zero_minor_above_zero() {
160 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 2, 3)));
162 assert!(c.matches(&Version::new(0, 2, 3)));
163 assert!(c.matches(&Version::new(0, 2, 99)));
164 assert!(!c.matches(&Version::new(0, 3, 0)));
165 }
166
167 #[test]
168 fn caret_major_and_minor_zero() {
169 let c = VersionConstraint::from_spec(ConstraintSpec::Caret(Version::new(0, 0, 3)));
171 assert!(c.matches(&Version::new(0, 0, 3)));
172 assert!(!c.matches(&Version::new(0, 0, 4)));
173 }
174
175 #[test]
176 fn tilde_only_allows_patch_changes() {
177 let c = VersionConstraint::from_spec(ConstraintSpec::Tilde(Version::new(1, 2, 3)));
179 assert!(c.matches(&Version::new(1, 2, 3)));
180 assert!(c.matches(&Version::new(1, 2, 99)));
181 assert!(!c.matches(&Version::new(1, 3, 0)));
182 }
183
184 #[test]
185 fn range_inclusive_lower_exclusive_upper() {
186 let c = VersionConstraint::from_spec(ConstraintSpec::Range {
187 lower_inclusive: Version::new(1, 2, 3),
188 upper_exclusive: Version::new(2, 0, 0),
189 });
190 assert!(c.matches(&Version::new(1, 2, 3)));
191 assert!(c.matches(&Version::new(1, 99, 99)));
192 assert!(!c.matches(&Version::new(2, 0, 0)));
193 assert!(!c.matches(&Version::new(1, 2, 2)));
194 }
195
196 #[test]
197 fn open_bounds() {
198 let ge = VersionConstraint::from_spec(ConstraintSpec::GreaterEqual(Version::new(1, 0, 0)));
199 let gt = VersionConstraint::from_spec(ConstraintSpec::Greater(Version::new(1, 0, 0)));
200 let le = VersionConstraint::from_spec(ConstraintSpec::LessEqual(Version::new(1, 0, 0)));
201 let lt = VersionConstraint::from_spec(ConstraintSpec::Less(Version::new(1, 0, 0)));
202 assert!(ge.matches(&Version::new(1, 0, 0)));
203 assert!(!gt.matches(&Version::new(1, 0, 0)));
204 assert!(le.matches(&Version::new(1, 0, 0)));
205 assert!(!lt.matches(&Version::new(1, 0, 0)));
206 }
207
208 #[test]
209 fn any_matches_everything() {
210 let c = VersionConstraint::from_spec(ConstraintSpec::Any);
211 assert!(c.matches(&Version::new(0, 0, 0)));
212 assert!(c.matches(&Version::new(999, 999, 999)));
213 }
214
215 #[test]
216 fn disjunction_via_compound() {
217 let c = CompoundConstraint {
218 combinator: Combinator::Or,
219 atoms: vec![
220 ConstraintSpec::Caret(Version::new(1, 0, 0)),
221 ConstraintSpec::Caret(Version::new(2, 0, 0)),
222 ],
223 };
224 assert!(c.matches(&Version::new(1, 5, 0)));
225 assert!(c.matches(&Version::new(2, 5, 0)));
226 assert!(!c.matches(&Version::new(3, 0, 0)));
227 }
228
229 #[test]
230 fn conjunction_via_compound() {
231 let c = CompoundConstraint {
232 combinator: Combinator::And,
233 atoms: vec![
234 ConstraintSpec::GreaterEqual(Version::new(1, 2, 0)),
235 ConstraintSpec::Less(Version::new(2, 0, 0)),
236 ],
237 };
238 assert!(c.matches(&Version::new(1, 5, 0)));
239 assert!(!c.matches(&Version::new(1, 1, 0)));
240 assert!(!c.matches(&Version::new(2, 0, 0)));
241 }
242
243 #[test]
244 fn round_trip_through_serde() {
245 let c = VersionConstraint {
246 spec: ConstraintSpec::Caret(Version::new(1, 2, 3)),
247 native_syntax: Some("^1.2.3".to_string()),
248 };
249 let j = serde_json::to_string(&c).unwrap();
250 let parsed: VersionConstraint = serde_json::from_str(&j).unwrap();
251 assert_eq!(c, parsed);
252 }
253}