pop_common/
polkadot_sdk.rs

1// SPDX-License-Identifier: GPL-3.0
2
3//! Parses and identifies the latest version tags based on semantic or Polkadot SDK versioning.
4
5use crate::SortedSlice;
6use regex::Regex;
7use std::{cmp::Reverse, sync::LazyLock};
8
9// Regex for `polkadot-stableYYMM` and `polkadot-stableYYMM-X`
10static STABLE: LazyLock<Regex> = LazyLock::new(|| {
11	Regex::new(
12		r"(polkadot-(parachain-)?)?stable(?P<year>\d{2})(?P<month>\d{2})(-(?P<patch>\d+))?(-rc\d+)?",
13	)
14	.expect("Valid regex")
15});
16// Regex for v{major}.{minor}.{patch} format
17static VERSION: LazyLock<Regex> = LazyLock::new(|| {
18	Regex::new(r"v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc\d+)?").expect("Valid regex")
19});
20
21/// A tuple of version numbers.
22pub type Version = (u32, u32, u32);
23
24/// Identifies the latest tag from a list of tags, prioritizing those in a `stableYYMM-X` format.
25/// Prerelease versions are omitted.
26///
27/// # Arguments
28/// * `tags` - A vector of tags to parse and evaluate.
29pub fn parse_latest_tag(tags: &[impl AsRef<str>]) -> Option<&str> {
30	match parse_latest_stable_tag(tags) {
31		Some(last_stable_tag) => Some(last_stable_tag),
32		None => parse_latest_semantic_version(tags),
33	}
34}
35
36/// Identifies the latest `stableYYMM-X` release tag. Prerelease versions are omitted.
37fn parse_latest_stable_tag(tags: &[impl AsRef<str>]) -> Option<&str> {
38	tags.iter()
39		.filter_map(|tag| parse_stable_version(tag.as_ref()).map(|version| (tag, version)))
40		.max_by(|a, b| {
41			let (_, (year_a, month_a, patch_a)) = a;
42			let (_, (year_b, month_b, patch_b)) = b;
43			// Compare by year, then by month, then by patch number
44			year_a
45				.cmp(year_b)
46				.then_with(|| month_a.cmp(month_b))
47				.then_with(|| patch_a.cmp(patch_b))
48		})
49		.map(|(tag, _)| tag.as_ref())
50}
51
52/// Identifies the latest version based on semantic versioning - e.g. `v1.2.3-rc`. Prerelease
53/// versions are omitted.
54///
55/// # Arguments
56/// * `items` - A vector of items to parse and evaluate.
57pub fn parse_latest_semantic_version(items: &[impl AsRef<str>]) -> Option<&str> {
58	items
59		.iter()
60		.filter_map(|tag| parse_semantic_version(tag.as_ref()).map(|version| (tag, version)))
61		.max_by_key(|&(_, version)| version)
62		.map(|(tag, _)| tag.as_ref())
63}
64
65/// Parses a semantic version - e.g. `v1.2.3-rc`. Prerelease versions are omitted.
66///
67/// # Arguments
68/// * `value` - The value to parse and evaluate.
69pub fn parse_semantic_version(value: impl AsRef<str>) -> Option<Version> {
70	// Skip the pre-release label
71	let value = value.as_ref();
72	if value.contains("-rc") {
73		return None;
74	}
75	VERSION.captures(value).and_then(|v| {
76		let major = v.name("major")?.as_str().parse::<u32>().ok()?;
77		let minor = v.name("minor")?.as_str().parse::<u32>().ok()?;
78		let patch = v.name("patch")?.as_str().parse::<u32>().ok()?;
79		Some((major, minor, patch))
80	})
81}
82
83/// Parses a stable version - e.g. `stable2503-1`. Prerelease versions are omitted.
84///
85/// # Arguments
86/// * `value` - The value to parse and evaluate.
87pub fn parse_stable_version(value: &str) -> Option<Version> {
88	// Skip the pre-release label
89	if value.contains("-rc") {
90		return None;
91	}
92	STABLE.captures(value).and_then(|v| {
93		let year = v.name("year")?.as_str().parse::<u32>().ok()?;
94		let month = v.name("month")?.as_str().parse::<u32>().ok()?;
95		let patch = v.name("patch").and_then(|m| m.as_str().parse::<u32>().ok()).unwrap_or(0);
96		Some((year, month, patch))
97	})
98}
99
100/// Parses a version - e.g. `v1.2.3-rc` or `stableYYMM-X`, prioritizing those in a `stableYYMM-X`
101/// format. Prerelease versions are omitted.
102///
103/// # Arguments
104/// * `value` - The value to parse and evaluate.
105pub fn parse_version(value: &str) -> Option<Version> {
106	match parse_stable_version(value) {
107		Some(stable_version) => Some(stable_version),
108		None => parse_semantic_version(value),
109	}
110}
111
112/// Sorts the provided versions using semantic versioning, with the latest version first. Prerelease
113/// versions are omitted.
114///
115/// # Arguments
116/// * `versions` - The versions to sort.
117pub fn sort_by_latest_semantic_version<T: AsRef<str>>(versions: &mut [T]) -> SortedSlice<T> {
118	SortedSlice::by_key(versions, |tag| {
119		parse_semantic_version(tag.as_ref())
120			.map(|version| Reverse(Some(version)))
121			.unwrap_or(Reverse(None))
122	})
123}
124
125/// Sorts the provided versions using `stableYYMM-X` versioning, with the latest version first.
126/// Prerelease versions are omitted.
127///
128/// # Arguments
129/// * `versions` - The versions to sort.
130pub fn sort_by_latest_stable_version<T: AsRef<str>>(versions: &mut [T]) -> SortedSlice<T> {
131	SortedSlice::by_key(versions, |tag| {
132		parse_stable_version(tag.as_ref())
133			.map(|version| Reverse(Some(version)))
134			.unwrap_or(Reverse(None))
135	})
136}
137
138/// Sorts the provided versions using `stableYYMM-X` and semver versioning, with the latest version
139/// first. Prerelease versions are omitted.
140///
141/// # Arguments
142/// * `versions` - The versions to sort.
143pub fn sort_by_latest_version<T: AsRef<str>>(versions: &mut [T]) -> SortedSlice<T> {
144	SortedSlice::by_key(versions, |tag| {
145		parse_version(tag.as_ref())
146			.map(|version| Reverse(Some(version)))
147			.unwrap_or(Reverse(None))
148	})
149}
150
151#[cfg(test)]
152mod tests {
153	use super::*;
154
155	#[test]
156	fn parse_latest_tag_works() {
157		let mut tags = vec![];
158		assert_eq!(parse_latest_tag(&tags), None);
159		tags = vec![
160			"polkadot-stable2409",
161			"polkadot-stable2409-1",
162			"polkadot-stable2407",
163			"polkadot-v1.10.0",
164			"polkadot-v1.11.0",
165			"polkadot-v1.12.0",
166			"polkadot-v1.7.0",
167			"polkadot-v1.8.0",
168			"polkadot-v1.9.0",
169			"v1.15.1-rc2",
170		];
171		assert_eq!(parse_latest_tag(&tags), Some("polkadot-stable2409-1"));
172	}
173
174	#[test]
175	fn parse_stable_format_works() {
176		let mut tags = vec![];
177		assert_eq!(parse_latest_stable_tag(&tags), None);
178		tags = vec!["polkadot-stable2407", "polkadot-stable2408"];
179		assert_eq!(parse_latest_stable_tag(&tags), Some("polkadot-stable2408"));
180		tags = vec!["polkadot-stable2407", "polkadot-stable2501"];
181		assert_eq!(parse_latest_stable_tag(&tags), Some("polkadot-stable2501"));
182		// Skip the pre-release label
183		tags = vec!["polkadot-stable2407", "polkadot-stable2407-1", "polkadot-stable2407-1-rc1"];
184		assert_eq!(parse_latest_stable_tag(&tags), Some("polkadot-stable2407-1"));
185	}
186
187	#[test]
188	fn parse_latest_semantic_version_works() {
189		let mut tags: Vec<&str> = vec![];
190		assert_eq!(parse_latest_semantic_version(&tags), None);
191		tags = vec![
192			"polkadot-v1.10.0",
193			"polkadot-v1.11.0",
194			"polkadot-v1.12.0",
195			"polkadot-v1.7.0",
196			"polkadot-v1.8.0",
197			"polkadot-v1.9.0",
198		];
199		assert_eq!(parse_latest_semantic_version(&tags), Some("polkadot-v1.12.0"));
200		tags = vec!["v1.0.0", "v2.0.0", "v3.0.0"];
201		assert_eq!(parse_latest_semantic_version(&tags), Some("v3.0.0"));
202		// Skip the pre-release label
203		tags = vec!["polkadot-v1.12.0", "v1.15.1-rc2"];
204		assert_eq!(parse_latest_semantic_version(&tags), Some("polkadot-v1.12.0"));
205	}
206
207	#[test]
208	fn parse_version_works() {
209		for (tag, expected) in [
210			("polkadot-stable2409", Some((24, 9, 0))),
211			("polkadot-stable2409-1", Some((24, 9, 1))),
212			("polkadot-v1.18.0", Some((1, 18, 0))),
213			("polkadot-v1.18.1", Some((1, 18, 1))),
214			("v1.15.1", Some((1, 15, 1))),
215			("v1.15.1-rc2", None),
216		] {
217			assert_eq!(parse_version(tag), expected);
218		}
219	}
220
221	#[test]
222	fn sort_by_latest_semantic_version_works() {
223		assert_eq!(
224			sort_by_latest_semantic_version(
225				[
226					"polkadot-v1.10.0",
227					"polkadot-v1.11.0",
228					"v1.17.0",
229					"polkadot-v1.12.0",
230					"polkadot-v1.7.0",
231					"v1.18.0",
232					"polkadot-v1.8.0",
233					"polkadot-v1.9.0",
234					"v1.18.1",
235				]
236				.as_mut_slice()
237			)
238			.0,
239			[
240				"v1.18.1",
241				"v1.18.0",
242				"v1.17.0",
243				"polkadot-v1.12.0",
244				"polkadot-v1.11.0",
245				"polkadot-v1.10.0",
246				"polkadot-v1.9.0",
247				"polkadot-v1.8.0",
248				"polkadot-v1.7.0",
249			]
250		);
251	}
252
253	#[test]
254	fn sort_by_latest_stable_version_works() {
255		assert_eq!(
256			sort_by_latest_stable_version(
257				[
258					"polkadot-stable2409",
259					"polkadot-stable2409-1",
260					"polkadot-stable2407",
261					"polkadot-stable2503",
262					"polkadot-stable2503-1"
263				]
264				.as_mut_slice()
265			)
266			.0,
267			[
268				"polkadot-stable2503-1",
269				"polkadot-stable2503",
270				"polkadot-stable2409-1",
271				"polkadot-stable2409",
272				"polkadot-stable2407",
273			]
274		);
275	}
276
277	#[test]
278	fn sort_by_latest_version_works() {
279		assert_eq!(
280			sort_by_latest_version(
281				[
282					"polkadot-v1.10.0",
283					"polkadot-v1.11.0",
284					"v1.17.0",
285					"polkadot-v1.12.0",
286					"polkadot-v1.7.0",
287					"v1.18.0",
288					"polkadot-v1.8.0",
289					"polkadot-v1.9.0",
290					"v1.18.1",
291					"polkadot-stable2409",
292					"polkadot-stable2409-1",
293					"polkadot-stable2407",
294					"polkadot-stable2503",
295					"polkadot-stable2503-1"
296				]
297				.as_mut_slice()
298			)
299			.0,
300			[
301				"polkadot-stable2503-1",
302				"polkadot-stable2503",
303				"polkadot-stable2409-1",
304				"polkadot-stable2409",
305				"polkadot-stable2407",
306				"v1.18.1",
307				"v1.18.0",
308				"v1.17.0",
309				"polkadot-v1.12.0",
310				"polkadot-v1.11.0",
311				"polkadot-v1.10.0",
312				"polkadot-v1.9.0",
313				"polkadot-v1.8.0",
314				"polkadot-v1.7.0",
315			]
316		);
317	}
318}