1use std::fmt::Display;
2
3#[cfg(feature = "schema")]
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
9#[cfg_attr(feature = "schema", derive(JsonSchema))]
10pub enum VersionPattern {
11 Single(String),
13 Latest(Option<String>),
15 Before(String),
17 After(String),
19 Range(String, String),
21 Any,
23}
24
25impl VersionPattern {
26 pub fn get_matches(&self, versions: &[String]) -> Vec<String> {
28 match self {
29 Self::Single(version) => match versions.contains(version) {
30 true => vec![version.to_string()],
31 false => vec![],
32 },
33 Self::Latest(found) => match found {
34 Some(found) => vec![found.clone()],
35 None => match versions.get(versions.len()).cloned() {
36 Some(version) => vec![version],
37 None => vec![],
38 },
39 },
40 Self::Before(version) => match versions.iter().position(|e| e == version) {
41 Some(pos) => versions[..=pos].to_vec(),
42 None => vec![],
43 },
44 Self::After(version) => match versions.iter().position(|e| e == version) {
45 Some(pos) => versions[pos..].to_vec(),
46 None => vec![],
47 },
48 Self::Range(start, end) => match versions.iter().position(|e| e == start) {
49 Some(start_pos) => match versions.iter().position(|e| e == end) {
50 Some(end_pos) => versions[start_pos..=end_pos].to_vec(),
51 None => vec![],
52 },
53 None => vec![],
54 },
55 Self::Any => versions.to_vec(),
56 }
57 }
58
59 pub fn get_match(&self, versions: &[String]) -> Option<String> {
61 self.get_matches(versions).last().cloned()
62 }
63
64 pub fn matches_single(&self, version: &str, versions: &[String]) -> bool {
68 match self {
69 Self::Single(vers) => version == vers,
70 Self::Latest(cached) => match cached {
71 Some(vers) => version == vers,
72 None => {
73 if let Some(latest) = versions.last() {
74 version == latest
75 } else {
76 false
77 }
78 }
79 },
80 Self::Before(vers) => {
81 if let Some(vers_pos) = versions.iter().position(|x| x == vers) {
82 if let Some(version_pos) = versions.iter().position(|x| x == version) {
83 version_pos <= vers_pos
84 } else {
85 false
86 }
87 } else {
88 false
89 }
90 }
91 Self::After(vers) => {
92 if let Some(vers_pos) = versions.iter().position(|x| x == vers) {
93 if let Some(version_pos) = versions.iter().position(|x| x == version) {
94 version_pos >= vers_pos
95 } else {
96 false
97 }
98 } else {
99 false
100 }
101 }
102 Self::Range(start, end) => {
103 if let Some(start_pos) = versions.iter().position(|x| x == start) {
104 if let Some(end_pos) = versions.iter().position(|x| x == end) {
105 if let Some(version_pos) = versions.iter().position(|x| x == version) {
106 (version_pos >= start_pos) && (version_pos <= end_pos)
107 } else {
108 false
109 }
110 } else {
111 false
112 }
113 } else {
114 false
115 }
116 }
117 Self::Any => versions.contains(&version.to_string()),
118 }
119 }
120
121 pub fn matches_info(&self, version_info: &VersionInfo) -> bool {
123 self.matches_single(&version_info.version, &version_info.versions)
124 }
125
126 pub fn match_union(&self, other: &Self, versions: &[String]) -> Vec<String> {
128 self.get_matches(versions)
129 .iter()
130 .zip(other.get_matches(versions))
131 .filter_map(
132 |(left, right)| {
133 if *left == right {
134 Some(right)
135 } else {
136 None
137 }
138 },
139 )
140 .collect()
141 }
142
143 pub fn from(text: &str) -> Self {
145 match text {
146 "latest" => Self::Latest(None),
147 "*" => Self::Any,
148 text => {
149 if let Some(last) = text.chars().last() {
150 if !text.chars().nth(text.len() - 2).is_some_and(|x| x == '\\') {
152 match last {
153 '-' => return Self::Before(text[..text.len() - 1].to_string()),
154 '+' => return Self::After(text[..text.len() - 1].to_string()),
155 _ => {}
156 }
157 }
158 }
159
160 let range_split: Vec<_> = text.split("..").collect();
161 if range_split.len() == 2 {
162 let start = range_split
163 .first()
164 .expect("First element in range split should exist");
165 if !start.ends_with('\\') {
167 let end = range_split
168 .get(1)
169 .expect("Second element in range split should exist");
170 return Self::Range(start.to_string(), end.to_string());
171 }
172 }
173
174 Self::Single(text.replace('\\', ""))
175 }
176 }
177 }
178
179 #[cfg(test)]
181 pub fn validate(text: &str) -> bool {
182 if text.contains('*') || text.contains("..") || text == "latest" {
183 return false;
184 }
185 if let Some(last) = text.chars().last() {
186 if last == '-' || last == '+' {
187 return false;
188 }
189 }
190 true
191 }
192}
193
194impl Display for VersionPattern {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(
197 f,
198 "{}",
199 match self {
200 Self::Single(version) => version.to_string(),
201 Self::Latest(..) => "latest".into(),
202 Self::Before(version) => version.to_string() + "-",
203 Self::After(version) => version.to_string() + "+",
204 Self::Range(start, end) => start.to_string() + ".." + end,
205 Self::Any => "*".into(),
206 }
207 )
208 }
209}
210
211impl<'de> Deserialize<'de> for VersionPattern {
212 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213 where
214 D: serde::Deserializer<'de>,
215 {
216 let string = String::deserialize(deserializer)?;
217 Ok(Self::from(&string))
218 }
219}
220
221impl Serialize for VersionPattern {
222 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
223 where
224 S: serde::Serializer,
225 {
226 let ser = self.to_string().serialize(serializer)?;
227 Ok(ser)
228 }
229}
230
231#[derive(Debug, Default, Deserialize, Serialize, Clone)]
233pub struct VersionInfo {
234 pub version: String,
236 pub versions: Vec<String>,
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_version_pattern() {
246 let versions = vec![
247 "1.16.5".to_string(),
248 "1.17".to_string(),
249 "1.18".to_string(),
250 "1.19.3".to_string(),
251 ];
252
253 assert_eq!(
254 VersionPattern::Single("1.19.3".into()).get_match(&versions),
255 Some("1.19.3".into())
256 );
257 assert_eq!(
258 VersionPattern::Single("1.18".into()).get_match(&versions),
259 Some("1.18".into())
260 );
261 assert_eq!(
262 VersionPattern::Single(String::new()).get_match(&versions),
263 None
264 );
265 assert_eq!(
266 VersionPattern::Before("1.18".into()).get_match(&versions),
267 Some("1.18".into())
268 );
269 assert_eq!(
270 VersionPattern::After("1.16.5".into()).get_match(&versions),
271 Some("1.19.3".into())
272 );
273
274 assert_eq!(
275 VersionPattern::Before("1.17".into()).get_matches(&versions),
276 vec!["1.16.5".to_string(), "1.17".to_string()]
277 );
278 assert_eq!(
279 VersionPattern::After("1.17".into()).get_matches(&versions),
280 vec!["1.17".to_string(), "1.18".to_string(), "1.19.3".to_string()]
281 );
282 assert_eq!(
283 VersionPattern::Range("1.16.5".into(), "1.18".into()).get_matches(&versions),
284 vec!["1.16.5".to_string(), "1.17".to_string(), "1.18".to_string()]
285 );
286
287 assert!(VersionPattern::Before("1.18".into()).matches_single("1.16.5", &versions));
288 assert!(VersionPattern::After("1.18".into()).matches_single("1.19.3", &versions));
289 assert!(VersionPattern::Latest(None).matches_single("1.19.3", &versions));
290 }
291
292 #[test]
293 fn test_version_pattern_parse() {
294 assert_eq!(
295 VersionPattern::from("+1.19.5"),
296 VersionPattern::Single("+1.19.5".into())
297 );
298 assert_eq!(VersionPattern::from("latest"), VersionPattern::Latest(None));
299 assert_eq!(
300 VersionPattern::from("1.19.5-"),
301 VersionPattern::Before("1.19.5".into())
302 );
303 assert_eq!(
304 VersionPattern::from("1.19.5+"),
305 VersionPattern::After("1.19.5".into())
306 );
307 assert_eq!(
308 VersionPattern::from("1.17.1..1.19.3"),
309 VersionPattern::Range("1.17.1".into(), "1.19.3".into())
310 );
311 }
312
313 #[test]
314 fn test_version_pattern_parse_escape() {
315 assert_eq!(
316 VersionPattern::from("1.19.5\\+"),
317 VersionPattern::Single("1.19.5+".into())
318 );
319 assert_eq!(
320 VersionPattern::from("1.17.1\\..1.19.3"),
321 VersionPattern::Single("1.17.1..1.19.3".into())
322 );
323 }
324
325 #[test]
326 fn test_version_pattern_validation() {
327 assert!(VersionPattern::validate("hello"));
328 assert!(!VersionPattern::validate("latest"));
329 assert!(!VersionPattern::validate("foo-"));
330 assert!(!VersionPattern::validate("foo+"));
331 assert!(!VersionPattern::validate("f*o"));
332 assert!(!VersionPattern::validate("f..o"));
333 }
334}