wasm_component_semver/lib.rs
1//! A specialized map for semantic versions with alternate version lookup support.
2//!
3//! This module best approximates the behavior of WASM component loading in `wasmtime`,
4//! such as in `wasmtime::component::Linker`.
5//!
6//! This module provides `VersionMap<T>`, which stores values indexed by semantic versions
7//! and supports fallback lookups through version alternates (e.g., 1.2.3 can be found
8//! via 1.0.0 if it's the latest patch for major version 1).
9
10use derivative::Derivative;
11use semver::Version;
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13
14/// A map that stores values indexed by semantic versions with support for alternate lookups.
15///
16/// The `VersionMap` maintains a primary mapping from versions to values, and a secondary
17/// mapping that groups versions by their "alternate" keys for fallback lookups.
18///
19/// # Alternate Lookup Logic
20///
21/// - For major versions > 0: alternate is `major.*.*`
22/// - For minor versions > 0 (when major is 0): alternate is `0.minor.*`
23/// - Otherwise: alternate is `0.0.patch`
24/// - Pre-release versions have no alternates
25///
26/// # Example
27///
28/// ```rust
29/// use semver::Version;
30/// # use wasm_component_semver::VersionMap;
31///
32/// let mut map = VersionMap::new();
33/// map.insert(Version::new(1, 0, 1), "v1.0.1");
34/// map.insert(Version::new(1, 2, 0), "v1.2.0");
35///
36/// // Exact lookups
37/// assert_eq!(map.get_exact(&Version::new(1, 0, 1)), Some(&"v1.0.1"));
38///
39/// // Alternate lookups (finds latest patch for major version 1)
40/// assert_eq!(map.get(&Version::new(1, 0, 0)), Some(&"v1.2.0"));
41/// ```
42#[derive(Clone, Derivative, Debug)]
43#[derivative(Default(bound = ""))]
44pub struct VersionMap<T> {
45 /// Primary storage mapping versions to values
46 versions: BTreeMap<Version, T>,
47 /// Secondary mapping for alternate version lookups
48 alternates: HashMap<Version, BTreeSet<Version>>,
49}
50
51impl<T> VersionMap<T> {
52 /// Creates a new empty `VersionMap`.
53 pub fn new() -> Self {
54 Self {
55 versions: BTreeMap::new(),
56 alternates: HashMap::new(),
57 }
58 }
59
60 /// Attempts to insert a version-value pair, returning an error if the version already exists.
61 pub fn try_insert(&mut self, version: Version, value: T) -> Result<(), (Version, T)> {
62 if self.versions.contains_key(&version) {
63 return Err((version, value));
64 }
65
66 if let Some(alternate) = version_alternate(&version) {
67 self.alternates
68 .entry(alternate)
69 .or_default()
70 .insert(version.clone());
71 }
72
73 self.versions.insert(version, value);
74
75 Ok(())
76 }
77
78 /// Inserts a version-value pair, returning the previous value if the version existed.
79 ///
80 /// Updates the alternates mapping appropriately.
81 pub fn insert(&mut self, version: Version, value: T) -> Option<T> {
82 if let Some(alternate) = version_alternate(&version) {
83 self.alternates
84 .entry(alternate)
85 .or_default()
86 .insert(version.clone());
87 }
88
89 self.versions.insert(version, value)
90 }
91
92 /// Gets a value by version, using alternate lookup if exact match is not found.
93 /// # Examples
94 ///
95 /// ```rust
96 /// use semver::Version;
97 /// # use wasm_component_semver::VersionMap;
98 ///
99 /// let mut map = VersionMap::new();
100 /// map.insert(Version::new(0, 0, 9), "v0.0.9");
101 /// map.insert(Version::new(0, 1, 1), "v0.1.1");
102 /// map.insert(Version::new(1, 2, 1), "v1.2.1");
103 ///
104 /// // Get latest patch
105 /// assert_eq!(map.get(&Version::new(0, 0, 9)), Some(&"v0.0.9"));
106 ///
107 /// // Get latest minor
108 /// assert_eq!(map.get(&Version::new(0, 1, 0)), Some(&"v0.1.1"));
109 ///
110 /// // Get latest major
111 /// assert_eq!(map.get(&Version::new(1, 0, 0)), Some(&"v1.2.1"));
112 pub fn get(&self, version: &Version) -> Option<&T> {
113 if version.build.is_empty() {
114 let maybe_value = version_alternate(version)
115 .as_ref()
116 .and_then(|alternate| self.alternates.get(alternate))
117 .and_then(|version_set| version_set.last())
118 .and_then(|version| self.versions.get(version));
119
120 if maybe_value.is_some() {
121 return maybe_value;
122 }
123 }
124
125 self.versions.get(version)
126 }
127
128 /// Like `get`, but returns the resolved version and value as a tuple.
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use semver::Version;
134 /// # use wasm_component_semver::VersionMap;
135 ///
136 /// let mut map = VersionMap::new();
137 /// map.insert(Version::new(0, 0, 9), "v0.0.9");
138 /// map.insert(Version::new(0, 1, 1), "v0.1.1");
139 /// map.insert(Version::new(1, 2, 1), "v1.2.1");
140 ///
141 /// // Get latest patch
142 /// assert_eq!(map.get_version(&Version::new(0, 0, 9)), Some((&Version::new(0, 0, 9), &"v0.0.9")));
143 ///
144 /// // Get latest minor
145 /// assert_eq!(map.get_version(&Version::new(0, 1, 0)), Some((&Version::new(0, 1, 1), &"v0.1.1")));
146 ///
147 /// // Get latest major
148 /// assert_eq!(map.get_version(&Version::new(1, 0, 0)), Some((&Version::new(1, 2, 1), &"v1.2.1")));
149 pub fn get_version(&self, version: &Version) -> Option<(&Version, &T)> {
150 if version.build.is_empty() {
151 let maybe_key_value = version_alternate(version)
152 .as_ref()
153 .and_then(|alternate| self.alternates.get(alternate))
154 .and_then(|version_set| version_set.last())
155 .and_then(|version| self.versions.get_key_value(version));
156
157 if maybe_key_value.is_some() {
158 return maybe_key_value;
159 }
160 }
161
162 self.versions.get_key_value(version)
163 }
164
165 /// Gets a value by version or returns the latest version if no specific version is provided.
166 ///
167 /// # Examples
168 ///
169 /// ```rust
170 /// use semver::Version;
171 /// # use wasm_component_semver::VersionMap;
172 ///
173 /// let mut map = VersionMap::new();
174 /// map.insert(Version::new(0, 0, 1), "v0.0.9");
175 /// map.insert(Version::new(0, 1, 0), "v0.1.0");
176 /// map.insert(Version::new(0, 1, 1), "v0.1.1");
177 /// map.insert(Version::new(0, 5, 1), "v0.5.1");
178 /// map.insert(Version::new(1, 0, 0), "v1.0.0");
179 /// map.insert(Version::new(1, 2, 0), "v1.2.0");
180 ///
181 /// // Get latest patch
182 /// assert_eq!(map.get_or_latest(Some(&Version::new(0, 0, 1))), Some(&"v0.0.9"));
183 ///
184 /// // Get latest minor
185 /// assert_eq!(map.get_or_latest(Some(&Version::new(0, 1, 0))), Some(&"v0.1.1"));
186 ///
187 /// // Get latest major
188 /// assert_eq!(map.get_or_latest(Some(&Version::new(1, 0, 0))), Some(&"v1.2.0"));
189 ///
190 /// // Get the latest version
191 /// assert_eq!(map.get_or_latest(None), Some(&"v1.2.0"));
192 /// ```
193 pub fn get_or_latest(&self, version: Option<&Version>) -> Option<&T> {
194 match version {
195 Some(v) => self.get(v),
196 None => self.get_latest().map(|(_, value)| value),
197 }
198 }
199
200 /// Returns the latest version and its associated value.
201 pub fn get_latest(&self) -> Option<(&Version, &T)> {
202 self.versions.last_key_value()
203 }
204
205 /// Gets a value by exact version match only, without alternate lookup.
206 pub fn get_exact(&self, version: &Version) -> Option<&T> {
207 self.versions.get(version)
208 }
209
210 pub fn remove(&mut self, version: &Version) -> Option<T> {
211 if let Some(alternate) = version_alternate(version) {
212 if let Some(set) = self.alternates.get_mut(&alternate) {
213 set.remove(version);
214 if set.is_empty() {
215 self.alternates.remove(&alternate);
216 }
217 }
218 }
219
220 self.versions.remove(version)
221 }
222}
223
224/// Computes the alternate version key for fallback lookups.
225///
226/// This function implements the alternate lookup logic:
227/// - Pre-release versions return `None` (no alternates)
228/// - Major versions > 0: return `major.0.0`
229/// - Minor versions > 0 (when major is 0): return `0.minor.0`
230/// - Otherwise: return `0.0.patch`
231fn version_alternate(version: &Version) -> Option<Version> {
232 // Pre-release versions don't have alternates
233 if !version.pre.is_empty() {
234 None
235 } else if version.major > 0 {
236 Some(Version::new(version.major, 0, 0))
237 } else if version.minor > 0 {
238 Some(Version::new(0, version.minor, 0))
239 } else {
240 Some(Version::new(0, 0, version.patch))
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use semver::Version;
248
249 #[test]
250 fn test_version_map_basic_operations() {
251 let mut map = VersionMap::new();
252
253 let version0 = Version::new(0, 4, 2);
254 let version1 = Version::new(1, 0, 0);
255 let version2 = Version::new(1, 0, 1);
256 let version3 = Version::new(2, 0, 0);
257
258 // Test insertions
259 assert!(map.try_insert(version0.clone(), "value0").is_ok());
260 assert!(map.try_insert(version1.clone(), "value1").is_ok());
261 assert!(map.try_insert(version2.clone(), "value2").is_ok());
262 assert!(map.try_insert(version3.clone(), "value3").is_ok());
263
264 // Test duplicate insertion
265 assert!(map.try_insert(version1.clone(), "duplicate").is_err());
266 }
267
268 #[test]
269 fn test_version_map_alternate_zero() {
270 let mut map = VersionMap::new();
271
272 let version0 = Version::new(0, 0, 3);
273 let version1 = Version::new(0, 3, 3);
274
275 map.try_insert(version0.clone(), "value0").unwrap();
276 map.try_insert(version1.clone(), "value1").unwrap();
277
278 assert_eq!(map.get_version(&Version::new(0, 0, 1)), None);
279
280 assert_eq!(
281 map.get_version(&Version::new(0, 0, 3)),
282 Some((&version0, &"value0"))
283 );
284
285 assert_eq!(
286 map.get_version(&Version::new(0, 3, 0)),
287 Some((&version1, &"value1"))
288 );
289 }
290
291 #[test]
292 fn test_version_map_alternate_lookups() {
293 let mut map = VersionMap::new();
294
295 let version0 = Version::new(0, 4, 2);
296 let version1 = Version::new(1, 0, 0);
297 let version2 = Version::new(1, 0, 1);
298 let version3 = Version::new(2, 0, 0);
299
300 map.try_insert(version0.clone(), "value0").unwrap();
301 map.try_insert(version1.clone(), "value1").unwrap();
302 map.try_insert(version2.clone(), "value2").unwrap();
303 map.try_insert(version3.clone(), "value3").unwrap();
304
305 // Test exact matches
306 assert_eq!(map.get(&version0), Some(&"value0"));
307 assert_eq!(map.get(&version2), Some(&"value2"));
308 assert_eq!(map.get(&version3), Some(&"value3"));
309
310 // Test alternate matches (should get latest in group)
311 assert_eq!(map.get(&version1), Some(&"value2")); // 1.0.0 -> latest in 1.x.x group
312 assert_eq!(map.get(&Version::new(0, 4, 1)), Some(&"value0")); // 0.4.1 -> latest in 0.4.x group
313 assert_eq!(map.get(&Version::new(1, 1, 0)), Some(&"value2")); // 1.1.0 -> latest in 1.x.x group
314 assert_eq!(map.get(&Version::new(2, 0, 4)), Some(&"value3")); // 2.0.4 -> latest in 2.x.x group
315
316 // Test alternate matches with get_version
317 assert_eq!(map.get_version(&version1), Some((&version2, &"value2"))); // 1.0.0 -> latest in 1.x.x group
318 assert_eq!(
319 map.get_version(&Version::new(0, 4, 1)),
320 Some((&version0, &"value0"))
321 ); // 0.4.1 -> latest in 0.4.x group
322 assert_eq!(
323 map.get_version(&Version::new(1, 1, 0)),
324 Some((&version2, &"value2"))
325 ); // 1.1.0 -> latest in 1.x.x group
326 assert_eq!(
327 map.get_version(&Version::new(2, 0, 4)),
328 Some((&version3, &"value3"))
329 ); // 2.0.4 -> latest in 2.x.x group
330
331 // Test non-existent versions
332 assert_eq!(map.get(&Version::new(0, 1, 0)), None);
333 assert_eq!(map.get(&Version::new(3, 0, 0)), None);
334
335 // Test exact lookups
336 assert_eq!(map.get_exact(&version1), Some(&"value1"));
337 assert_eq!(map.get_exact(&Version::new(1, 1, 0)), None); // No exact match
338 }
339
340 #[test]
341 fn test_version_map_latest_operations() {
342 let mut map = VersionMap::new();
343
344 assert_eq!(map.get_latest(), None);
345 assert_eq!(map.get_or_latest(None), None);
346
347 map.insert(Version::new(1, 0, 0), "v1.0.0");
348 map.insert(Version::new(2, 0, 0), "v2.0.0");
349 map.insert(Version::new(0, 1, 0), "v0.1.0");
350
351 assert_eq!(map.get_latest(), Some((&Version::new(2, 0, 0), &"v2.0.0")));
352 assert_eq!(map.get_or_latest(None), Some(&"v2.0.0"));
353 assert_eq!(
354 map.get_or_latest(Some(&Version::new(1, 0, 0))),
355 Some(&"v1.0.0")
356 );
357 }
358
359 #[test]
360 fn test_version_map_insert_and_removal() {
361 let mut map = VersionMap::new();
362
363 let v1 = Version::new(1, 0, 0);
364 let v2 = Version::new(1, 0, 1);
365
366 map.insert(v1.clone(), "v1");
367 map.insert(v2.clone(), "v2");
368
369 assert_eq!(map.remove(&v1), Some("v1"));
370 assert_eq!(map.remove(&v1), None); // Already removed
371 }
372
373 #[test]
374 fn test_version_alternate_function() {
375 // Pre-release versions have no alternates
376 let pre = Version::parse("1.0.0-alpha").unwrap();
377 assert_eq!(version_alternate(&pre), None);
378
379 // Major versions > 0
380 assert_eq!(
381 version_alternate(&Version::new(1, 2, 3)),
382 Some(Version::new(1, 0, 0))
383 );
384 assert_eq!(
385 version_alternate(&Version::new(2, 5, 1)),
386 Some(Version::new(2, 0, 0))
387 );
388
389 // Minor versions > 0 (when major is 0)
390 assert_eq!(
391 version_alternate(&Version::new(0, 1, 5)),
392 Some(Version::new(0, 1, 0))
393 );
394 assert_eq!(
395 version_alternate(&Version::new(0, 3, 2)),
396 Some(Version::new(0, 3, 0))
397 );
398
399 // Patch versions (when major and minor are 0)
400 assert_eq!(
401 version_alternate(&Version::new(0, 0, 1)),
402 Some(Version::new(0, 0, 1))
403 );
404 assert_eq!(
405 version_alternate(&Version::new(0, 0, 5)),
406 Some(Version::new(0, 0, 5))
407 );
408 }
409}