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}