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::borrow::Borrow;
13use std::collections::{BTreeMap, BTreeSet, HashMap};
14use std::io::{Read, Write};
15
16/// A map that stores values indexed by semantic versions with support for alternate lookups.
17///
18/// The `VersionMap` maintains a primary mapping from versions to values, and a secondary
19/// mapping that groups versions by their "alternate" keys for fallback lookups.
20///
21/// # Alternate Lookup Logic
22///
23/// - For major versions > 0: alternate is `major.*.*`
24/// - For minor versions > 0 (when major is 0): alternate is `0.minor.*`
25/// - Otherwise: alternate is `0.0.patch`
26/// - Pre-release versions have no alternates
27///
28/// # Example
29///
30/// ```rust
31/// use semver::Version;
32/// # use wasm_component_semver::VersionMap;
33///
34/// let mut map = VersionMap::new();
35/// map.insert(Version::new(1, 0, 1), "v1.0.1");
36/// map.insert(Version::new(1, 2, 0), "v1.2.0");
37///
38/// // Exact lookups
39/// assert_eq!(map.get_exact(&Version::new(1, 0, 1)), Some(&"v1.0.1"));
40///
41/// // Alternate lookups (finds latest patch for major version 1)
42/// assert_eq!(map.get(&Version::new(1, 0, 0)), Some(&"v1.2.0"));
43/// ```
44#[derive(Clone, Derivative, Debug)]
45#[derivative(Default(bound = ""))]
46pub struct VersionMap<T> {
47    /// Primary storage mapping versions to values
48    versions: BTreeMap<WrappedVersion, T>,
49    /// Secondary mapping for alternate version lookups
50    alternates: HashMap<Version, BTreeSet<WrappedVersion>>,
51}
52
53impl<T> VersionMap<T> {
54    /// Creates a new empty `VersionMap`.
55    pub fn new() -> Self {
56        Self {
57            versions: BTreeMap::new(),
58            alternates: HashMap::new(),
59        }
60    }
61
62    fn from_versions(versions: BTreeMap<WrappedVersion, T>) -> Self {
63        let mut alternates: HashMap<Version, BTreeSet<WrappedVersion>> = HashMap::new();
64
65        for (version, _) in &versions {
66            if let Some(alternate) = version_alternate(&version.inner) {
67                alternates
68                    .entry(alternate)
69                    .or_default()
70                    .insert(version.clone());
71            }
72        }
73
74        Self {
75            versions,
76            alternates,
77        }
78    }
79
80    /// Attempts to insert a version-value pair, returning an error if the version already exists.
81    pub fn try_insert(&mut self, version: Version, value: T) -> Result<(), (Version, T)> {
82        let version: WrappedVersion = version.into();
83
84        if self.versions.contains_key(&version) {
85            return Err((version.into(), value));
86        }
87
88        if let Some(alternate) = version_alternate(&version.inner) {
89            self.alternates
90                .entry(alternate)
91                .or_default()
92                .insert(version.clone());
93        }
94
95        self.versions.insert(version, value);
96
97        Ok(())
98    }
99
100    /// Inserts a version-value pair, returning the previous value if the version existed.
101    ///
102    /// Updates the alternates mapping appropriately.
103    pub fn insert(&mut self, version: Version, value: T) -> Option<T> {
104        let version: WrappedVersion = version.into();
105
106        if let Some(alternate) = version_alternate(&version.inner) {
107            self.alternates
108                .entry(alternate)
109                .or_default()
110                .insert(version.clone());
111        }
112
113        self.versions.insert(version, value)
114    }
115
116    /// Gets a value by version, using alternate lookup if exact match is not found.
117    /// # Examples
118    ///
119    /// ```rust
120    /// use semver::Version;
121    /// # use wasm_component_semver::VersionMap;
122    ///
123    /// let mut map = VersionMap::new();
124    /// map.insert(Version::new(0, 0, 9), "v0.0.9");
125    /// map.insert(Version::new(0, 1, 1), "v0.1.1");
126    /// map.insert(Version::new(1, 2, 1), "v1.2.1");
127    ///
128    /// // Get latest patch
129    /// assert_eq!(map.get(&Version::new(0, 0, 9)), Some(&"v0.0.9"));
130    ///
131    /// // Get latest minor
132    /// assert_eq!(map.get(&Version::new(0, 1, 0)), Some(&"v0.1.1"));
133    ///
134    /// // Get latest major
135    /// assert_eq!(map.get(&Version::new(1, 0, 0)), Some(&"v1.2.1"));
136    pub fn get(&self, version: &Version) -> Option<&T> {
137        if version.build.is_empty() {
138            let maybe_value = version_alternate(version)
139                .as_ref()
140                .and_then(|alternate| self.alternates.get(alternate))
141                .and_then(|version_set| version_set.last())
142                .and_then(|version| self.versions.get(version));
143
144            if maybe_value.is_some() {
145                return maybe_value;
146            }
147        }
148
149        self.versions.get(version)
150    }
151
152    /// Like `get`, but returns the resolved version and value as a tuple.
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// use semver::Version;
158    /// # use wasm_component_semver::VersionMap;
159    ///
160    /// let mut map = VersionMap::new();
161    /// map.insert(Version::new(0, 0, 9), "v0.0.9");
162    /// map.insert(Version::new(0, 1, 1), "v0.1.1");
163    /// map.insert(Version::new(1, 2, 1), "v1.2.1");
164    ///
165    /// // Get latest patch
166    /// assert_eq!(map.get_version(&Version::new(0, 0, 9)), Some((&Version::new(0, 0, 9), &"v0.0.9")));
167    ///
168    /// // Get latest minor
169    /// assert_eq!(map.get_version(&Version::new(0, 1, 0)), Some((&Version::new(0, 1, 1), &"v0.1.1")));
170    ///
171    /// // Get latest major
172    /// assert_eq!(map.get_version(&Version::new(1, 0, 0)), Some((&Version::new(1, 2, 1), &"v1.2.1")));
173    pub fn get_version(&self, version: &Version) -> Option<(&Version, &T)> {
174        if version.build.is_empty() {
175            let maybe_key_value = version_alternate(version)
176                .as_ref()
177                .and_then(|alternate| self.alternates.get(alternate))
178                .and_then(|version_set| version_set.last())
179                .and_then(|version| self.versions.get_key_value(version));
180
181            if maybe_key_value.is_some() {
182                return maybe_key_value.map(|(k, v)| (k.borrow(), v));
183            }
184        }
185
186        self.versions
187            .get_key_value(version)
188            .map(|(k, v)| (k.borrow(), v))
189    }
190
191    /// Gets a value by version or returns the latest version if no specific version is provided.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use semver::Version;
197    /// # use wasm_component_semver::VersionMap;
198    ///
199    /// let mut map = VersionMap::new();
200    /// map.insert(Version::new(0, 0, 9), "v0.0.9");
201    /// map.insert(Version::new(0, 1, 0), "v0.1.0");
202    /// map.insert(Version::new(0, 1, 1), "v0.1.1");
203    /// map.insert(Version::new(0, 5, 1), "v0.5.1");
204    /// map.insert(Version::new(1, 0, 0), "v1.0.0");
205    /// map.insert(Version::new(1, 2, 0), "v1.2.0");
206    ///
207    /// // Get latest patch
208    /// assert_eq!(map.get_or_latest(Some(&Version::new(0, 0, 9))), Some(&"v0.0.9"));
209    ///
210    /// // Get latest minor
211    /// assert_eq!(map.get_or_latest(Some(&Version::new(0, 1, 0))), Some(&"v0.1.1"));
212    ///
213    /// // Get latest major
214    /// assert_eq!(map.get_or_latest(Some(&Version::new(1, 0, 0))), Some(&"v1.2.0"));
215    ///
216    /// // Get the latest version
217    /// assert_eq!(map.get_or_latest(None), Some(&"v1.2.0"));
218    /// ```
219    pub fn get_or_latest(&self, version: Option<&Version>) -> Option<&T> {
220        match version {
221            Some(v) => self.get(v),
222            None => self.get_latest().map(|(_, value)| value),
223        }
224    }
225
226    /// Gets a value by version or returns the latest version and its associated value
227    /// if no specific version is provided.
228    ///
229    /// # Examples
230    ///
231    /// ```rust
232    /// use semver::Version;
233    /// # use wasm_component_semver::VersionMap;
234    ///
235    /// let mut map = VersionMap::new();
236    /// map.insert(Version::new(0, 0, 9), "v0.0.9");
237    /// map.insert(Version::new(0, 1, 0), "v0.1.0");
238    /// map.insert(Version::new(0, 1, 1), "v0.1.1");
239    /// map.insert(Version::new(0, 5, 1), "v0.5.1");
240    /// map.insert(Version::new(1, 0, 0), "v1.0.0");
241    /// map.insert(Version::new(1, 2, 0), "v1.2.0");
242    ///
243    /// // Get latest patch
244    /// assert_eq!(map.get_or_latest_version(Some(&Version::new(0, 0, 9))), Some((&Version::new(0, 0, 9), &"v0.0.9")));
245    ///
246    /// // Get latest minor
247    /// assert_eq!(map.get_or_latest_version(Some(&Version::new(0, 1, 0))), Some((&Version::new(0, 1, 1), &"v0.1.1")));
248    ///
249    /// // Get latest major
250    /// assert_eq!(map.get_or_latest_version(Some(&Version::new(1, 0, 0))), Some((&Version::new(1, 2, 0), &"v1.2.0")));
251    ///
252    /// // Get the latest version
253    /// assert_eq!(map.get_or_latest_version(None), Some((&Version::new(1, 2, 0), &"v1.2.0")));
254    /// ```
255    pub fn get_or_latest_version(&self, version: Option<&Version>) -> Option<(&Version, &T)> {
256        match version {
257            Some(v) => self.get_version(v),
258            None => self.get_latest(),
259        }
260    }
261
262    /// Returns the latest version and its associated value.
263    pub fn get_latest(&self) -> Option<(&Version, &T)> {
264        self.versions.last_key_value().map(|(k, v)| (k.borrow(), v))
265    }
266
267    /// Gets a value by exact version match only, without alternate lookup.
268    pub fn get_exact(&self, version: &Version) -> Option<&T> {
269        self.versions.get(version)
270    }
271
272    pub fn remove(&mut self, version: &Version) -> Option<T> {
273        if let Some(alternate) = version_alternate(version) {
274            if let Some(set) = self.alternates.get_mut(&alternate) {
275                set.remove(version);
276                if set.is_empty() {
277                    self.alternates.remove(&alternate);
278                }
279            }
280        }
281
282        self.versions.remove(version)
283    }
284}
285
286#[cfg(feature = "borsh")]
287impl<T: borsh::BorshSerialize> borsh::BorshSerialize for VersionMap<T> {
288    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
289        self.versions.serialize(writer)
290    }
291}
292
293#[cfg(feature = "borsh")]
294impl<T: borsh::BorshDeserialize> borsh::BorshDeserialize for VersionMap<T> {
295    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
296        let versions: BTreeMap<WrappedVersion, T> =
297            borsh::BorshDeserialize::deserialize_reader(reader)?;
298        Ok(Self::from_versions(versions))
299    }
300}
301
302#[cfg(feature = "serde")]
303impl<T: serde::Serialize> serde::Serialize for VersionMap<T> {
304    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
305    where
306        S: serde::Serializer,
307    {
308        self.versions.serialize(serializer)
309    }
310}
311
312#[cfg(feature = "serde")]
313impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for VersionMap<T> {
314    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
315    where
316        D: serde::Deserializer<'de>,
317    {
318        let versions: BTreeMap<WrappedVersion, T> = serde::Deserialize::deserialize(deserializer)?;
319        Ok(Self::from_versions(versions))
320    }
321}
322
323/// Computes the alternate version key for fallback lookups.
324///
325/// This function implements the alternate lookup logic:
326/// - Pre-release versions return `None` (no alternates)
327/// - Major versions > 0: return `major.0.0`
328/// - Minor versions > 0 (when major is 0): return `0.minor.0`
329/// - Otherwise: return `0.0.patch`
330fn version_alternate(version: &Version) -> Option<Version> {
331    // Pre-release versions don't have alternates
332    if !version.pre.is_empty() {
333        None
334    } else if version.major > 0 {
335        Some(Version::new(version.major, 0, 0))
336    } else if version.minor > 0 {
337        Some(Version::new(0, version.minor, 0))
338    } else {
339        Some(Version::new(0, 0, version.patch))
340    }
341}
342
343#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
344#[repr(transparent)]
345struct WrappedVersion {
346    inner: Version,
347}
348
349impl Borrow<Version> for WrappedVersion {
350    fn borrow(&self) -> &Version {
351        &self.inner
352    }
353}
354
355impl From<Version> for WrappedVersion {
356    fn from(version: Version) -> Self {
357        Self { inner: version }
358    }
359}
360
361impl From<WrappedVersion> for Version {
362    fn from(wrapped: WrappedVersion) -> Self {
363        wrapped.inner
364    }
365}
366
367#[cfg(feature = "borsh")]
368impl borsh::BorshSerialize for WrappedVersion {
369    fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
370        let s = self.inner.to_string();
371        borsh::BorshSerialize::serialize(&s, writer)
372    }
373}
374
375#[cfg(feature = "borsh")]
376impl borsh::BorshDeserialize for WrappedVersion {
377    fn deserialize_reader<R: Read>(reader: &mut R) -> std::io::Result<Self> {
378        let s: String = borsh::BorshDeserialize::deserialize_reader(reader)?;
379
380        let version = Version::parse(&s).map_err(|_| {
381            std::io::Error::new(
382                std::io::ErrorKind::InvalidData,
383                "Failed to parse version from string",
384            )
385        })?;
386
387        Ok(Self { inner: version })
388    }
389}
390
391#[cfg(feature = "serde")]
392impl serde::Serialize for WrappedVersion {
393    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
394    where
395        S: serde::Serializer,
396    {
397        self.inner.serialize(serializer)
398    }
399}
400
401#[cfg(feature = "serde")]
402impl<'de> serde::Deserialize<'de> for WrappedVersion {
403    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
404    where
405        D: serde::Deserializer<'de>,
406    {
407        Ok(Self {
408            inner: serde::Deserialize::deserialize(deserializer)?,
409        })
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use semver::Version;
417
418    #[test]
419    fn test_version_map_basic_operations() {
420        let mut map = VersionMap::new();
421
422        let version0 = Version::new(0, 4, 2);
423        let version1 = Version::new(1, 0, 0);
424        let version2 = Version::new(1, 0, 1);
425        let version3 = Version::new(2, 0, 0);
426
427        // Test insertions
428        assert!(map.try_insert(version0.clone(), "value0").is_ok());
429        assert!(map.try_insert(version1.clone(), "value1").is_ok());
430        assert!(map.try_insert(version2.clone(), "value2").is_ok());
431        assert!(map.try_insert(version3.clone(), "value3").is_ok());
432
433        // Test duplicate insertion
434        assert!(map.try_insert(version1.clone(), "duplicate").is_err());
435    }
436
437    #[test]
438    fn test_version_map_alternate_zero() {
439        let mut map = VersionMap::new();
440
441        let version0 = Version::new(0, 0, 3);
442        let version1 = Version::new(0, 3, 3);
443
444        map.try_insert(version0.clone(), "value0").unwrap();
445        map.try_insert(version1.clone(), "value1").unwrap();
446
447        assert_eq!(map.get_version(&Version::new(0, 0, 1)), None);
448
449        assert_eq!(
450            map.get_version(&Version::new(0, 0, 3)),
451            Some((&version0, &"value0"))
452        );
453
454        assert_eq!(
455            map.get_version(&Version::new(0, 3, 0)),
456            Some((&version1, &"value1"))
457        );
458    }
459
460    #[test]
461    fn test_version_map_alternate_lookups() {
462        let mut map = VersionMap::new();
463
464        let version0 = Version::new(0, 4, 2);
465        let version1 = Version::new(1, 0, 0);
466        let version2 = Version::new(1, 0, 1);
467        let version3 = Version::new(2, 0, 0);
468
469        map.try_insert(version0.clone(), "value0").unwrap();
470        map.try_insert(version1.clone(), "value1").unwrap();
471        map.try_insert(version2.clone(), "value2").unwrap();
472        map.try_insert(version3.clone(), "value3").unwrap();
473
474        // Test exact matches
475        assert_eq!(map.get(&version0), Some(&"value0"));
476        assert_eq!(map.get(&version2), Some(&"value2"));
477        assert_eq!(map.get(&version3), Some(&"value3"));
478
479        // Test alternate matches (should get latest in group)
480        assert_eq!(map.get(&version1), Some(&"value2")); // 1.0.0 -> latest in 1.x.x group
481        assert_eq!(map.get(&Version::new(0, 4, 1)), Some(&"value0")); // 0.4.1 -> latest in 0.4.x group
482        assert_eq!(map.get(&Version::new(1, 1, 0)), Some(&"value2")); // 1.1.0 -> latest in 1.x.x group
483        assert_eq!(map.get(&Version::new(2, 0, 4)), Some(&"value3")); // 2.0.4 -> latest in 2.x.x group
484
485        // Test alternate matches with get_version
486        assert_eq!(map.get_version(&version1), Some((&version2, &"value2"))); // 1.0.0 -> latest in 1.x.x group
487        assert_eq!(
488            map.get_version(&Version::new(0, 4, 1)),
489            Some((&version0, &"value0"))
490        ); // 0.4.1 -> latest in 0.4.x group
491        assert_eq!(
492            map.get_version(&Version::new(1, 1, 0)),
493            Some((&version2, &"value2"))
494        ); // 1.1.0 -> latest in 1.x.x group
495        assert_eq!(
496            map.get_version(&Version::new(2, 0, 4)),
497            Some((&version3, &"value3"))
498        ); // 2.0.4 -> latest in 2.x.x group
499
500        // Test non-existent versions
501        assert_eq!(map.get(&Version::new(0, 1, 0)), None);
502        assert_eq!(map.get(&Version::new(3, 0, 0)), None);
503
504        // Test exact lookups
505        assert_eq!(map.get_exact(&version1), Some(&"value1"));
506        assert_eq!(map.get_exact(&Version::new(1, 1, 0)), None); // No exact match
507    }
508
509    #[test]
510    fn test_version_map_latest_operations() {
511        let mut map = VersionMap::new();
512
513        assert_eq!(map.get_latest(), None);
514        assert_eq!(map.get_or_latest(None), None);
515
516        map.insert(Version::new(1, 0, 0), "v1.0.0");
517        map.insert(Version::new(2, 0, 0), "v2.0.0");
518        map.insert(Version::new(0, 1, 0), "v0.1.0");
519
520        assert_eq!(map.get_latest(), Some((&Version::new(2, 0, 0), &"v2.0.0")));
521        assert_eq!(map.get_or_latest(None), Some(&"v2.0.0"));
522        assert_eq!(
523            map.get_or_latest(Some(&Version::new(1, 0, 0))),
524            Some(&"v1.0.0")
525        );
526    }
527
528    #[test]
529    fn test_version_map_insert_and_removal() {
530        let mut map = VersionMap::new();
531
532        let v1 = Version::new(1, 0, 0);
533        let v2 = Version::new(1, 0, 1);
534
535        map.insert(v1.clone(), "v1");
536        map.insert(v2.clone(), "v2");
537
538        assert_eq!(map.remove(&v1), Some("v1"));
539        assert_eq!(map.remove(&v1), None); // Already removed
540    }
541
542    #[test]
543    fn test_version_alternate_function() {
544        // Pre-release versions have no alternates
545        let pre = Version::parse("1.0.0-alpha").unwrap();
546        assert_eq!(version_alternate(&pre), None);
547
548        // Major versions > 0
549        assert_eq!(
550            version_alternate(&Version::new(1, 2, 3)),
551            Some(Version::new(1, 0, 0))
552        );
553        assert_eq!(
554            version_alternate(&Version::new(2, 5, 1)),
555            Some(Version::new(2, 0, 0))
556        );
557
558        // Minor versions > 0 (when major is 0)
559        assert_eq!(
560            version_alternate(&Version::new(0, 1, 5)),
561            Some(Version::new(0, 1, 0))
562        );
563        assert_eq!(
564            version_alternate(&Version::new(0, 3, 2)),
565            Some(Version::new(0, 3, 0))
566        );
567
568        // Patch versions (when major and minor are 0)
569        assert_eq!(
570            version_alternate(&Version::new(0, 0, 1)),
571            Some(Version::new(0, 0, 1))
572        );
573        assert_eq!(
574            version_alternate(&Version::new(0, 0, 5)),
575            Some(Version::new(0, 0, 5))
576        );
577    }
578
579    #[test]
580    #[cfg(feature = "borsh")]
581    fn test_borsh_serialize_deserialize() {
582        use borsh::{BorshDeserialize, BorshSerialize};
583
584        let mut map = VersionMap::new();
585        map.insert(Version::new(1, 0, 0), "v1.0.0");
586        map.insert(Version::new(2, 0, 0), "v2.0.0");
587
588        // Serialize
589        let mut buffer = Vec::new();
590        map.serialize(&mut buffer).unwrap();
591
592        // Deserialize
593        let deserialized_map: VersionMap<String> =
594            BorshDeserialize::deserialize_reader(&mut &buffer[..]).unwrap();
595
596        assert_eq!(
597            deserialized_map.get(&Version::new(1, 0, 0)),
598            Some(&"v1.0.0".to_string())
599        );
600
601        assert_eq!(
602            deserialized_map.get(&Version::new(2, 0, 0)),
603            Some(&"v2.0.0".to_string())
604        );
605    }
606
607    #[test]
608    #[cfg(feature = "serde")]
609    fn test_serde_serialize_deserialize() {
610        let mut map = VersionMap::new();
611        map.insert(Version::new(1, 0, 0), "v1.0.0");
612        map.insert(Version::new(2, 0, 0), "v2.0.0");
613
614        // Serialize
615        let serialized = serde_json::to_string(&map).unwrap();
616
617        // Deserialize
618        let deserialized_map: VersionMap<String> = serde_json::from_str(&serialized).unwrap();
619
620        assert_eq!(
621            deserialized_map.get(&Version::new(1, 0, 0)),
622            Some(&"v1.0.0".to_string())
623        );
624
625        assert_eq!(
626            deserialized_map.get(&Version::new(2, 0, 0)),
627            Some(&"v2.0.0".to_string())
628        );
629    }
630}