Skip to main content

miden_package_registry/
version.rs

1use core::{borrow::Borrow, fmt, str::FromStr};
2
3pub use miden_assembly_syntax::semver::{Error as SemVerError, Version as SemVer};
4use miden_core::Word;
5#[cfg(feature = "arbitrary")]
6use proptest::prelude::*;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10use super::VersionRequirement;
11
12/// The error type raised when attempting to parse a [Version] from a string.
13#[derive(Debug, thiserror::Error)]
14pub enum InvalidVersionError {
15    #[error("invalid digest: {0}")]
16    Digest(&'static str),
17    #[error("invalid semantic version: {0}")]
18    Version(SemVerError),
19}
20
21#[cfg(feature = "arbitrary")]
22impl Arbitrary for InvalidVersionError {
23    type Parameters = ();
24    type Strategy = BoxedStrategy<Self>;
25
26    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
27        any::<bool>()
28            .prop_map(|use_digest| {
29                if use_digest {
30                    Self::Digest("invalid digest")
31                } else {
32                    Self::Version("not-a-version".parse::<SemVer>().unwrap_err())
33                }
34            })
35            .boxed()
36    }
37}
38
39/// The representation of versioning information associated with packages in the package index.
40///
41/// This type provides the means by which dependency resolution can satisfy versioning constraints
42/// on packages using either semantic version constraints or explicit package commitments
43/// simultaneously.
44///
45/// All packages have an associated semantic version. Packages which have been assembled to MAST,
46/// also have an associated content digest. However, for the purposes of indexing and dependency
47/// resolution, we cannot assume that all packages have a content digest (as they may not have been
48/// assembled yet), and so this type is used to represent versions within the index/resolver so that
49/// it can:
50///
51/// * Satisfy requirements for a package that has a specific digest
52/// * Record the exact published identity of a canonical package artifact as `semver#digest`
53/// * Provide a total ordering for package versions that may or may not include a specific digest
54#[derive(Debug, Clone, Eq, PartialEq)]
55pub struct Version {
56    /// The semantic version information
57    ///
58    /// This is the canonical human-facing version for a package.
59    pub version: SemVer,
60    /// The content digest for this version, if known.
61    ///
62    /// This is the most precise version for a package, and uniquely identifies the canonical
63    /// published artifact associated with a semantic version.
64    pub digest: Option<Word>,
65}
66
67#[cfg(feature = "serde")]
68impl Serialize for Version {
69    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: serde::Serializer,
72    {
73        use alloc::string::ToString;
74
75        serializer.serialize_str(&self.to_string())
76    }
77}
78
79#[cfg(feature = "serde")]
80impl<'de> Deserialize<'de> for Version {
81    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82    where
83        D: serde::Deserializer<'de>,
84    {
85        let value = <alloc::string::String as Deserialize>::deserialize(deserializer)?;
86        value.parse().map_err(serde::de::Error::custom)
87    }
88}
89
90impl Version {
91    /// Construct a [Version] from its component parts.
92    pub fn new(version: SemVer, digest: Word) -> Self {
93        Self { version, digest: Some(digest) }
94    }
95
96    /// Get a [Version] without an attached digest for comparison purposes
97    pub fn without_digest(&self) -> Self {
98        Self {
99            version: self.version.clone(),
100            digest: None,
101        }
102    }
103
104    /// Get a [core::ops::Range] which can be used to select all available versions with the same
105    /// semantic version, but with possibly-differing digests
106    pub fn as_range(&self) -> core::ops::Range<Version> {
107        let start = self.without_digest();
108        let mut end = start.clone();
109        end.version.patch += 1;
110
111        start..end
112    }
113
114    /// Returns true if `self` and `other` are equivalent with regards to semantic versioning
115    pub fn is_semantically_equivalent(&self, other: &Self) -> bool {
116        self.version.cmp_precedence(&other.version).is_eq()
117    }
118
119    /// Check if this version satisfies the given `requirement`.
120    ///
121    /// Version requirements are expressed as either a semantic version constraint OR a specific
122    /// content digest.
123    pub fn satisfies(&self, requirement: &VersionRequirement) -> bool {
124        match requirement {
125            VersionRequirement::Semantic(req) => req.matches(&self.version),
126            VersionRequirement::Digest(req) => {
127                self.digest.as_ref().is_some_and(|digest| req.into_inner() == *digest)
128            },
129            VersionRequirement::Exact(req) => self == req,
130        }
131    }
132}
133
134impl FromStr for Version {
135    type Err = InvalidVersionError;
136    fn from_str(s: &str) -> Result<Self, Self::Err> {
137        match s.split_once('#') {
138            Some((v, digest)) => {
139                let v = v.parse::<SemVer>().map_err(InvalidVersionError::Version)?;
140                let digest = Word::parse(digest).map_err(InvalidVersionError::Digest)?;
141                Ok(Self::new(v, digest))
142            },
143            None => {
144                let v = s.parse::<SemVer>().map_err(InvalidVersionError::Version)?;
145                Ok(Self::from(v))
146            },
147        }
148    }
149}
150
151impl From<SemVer> for Version {
152    fn from(version: SemVer) -> Self {
153        Self { version, digest: None }
154    }
155}
156
157impl From<(SemVer, Word)> for Version {
158    fn from(version: (SemVer, Word)) -> Self {
159        let (version, word) = version;
160        Self { version, digest: Some(word) }
161    }
162}
163
164impl Borrow<SemVer> for Version {
165    #[inline(always)]
166    fn borrow(&self) -> &SemVer {
167        &self.version
168    }
169}
170
171impl fmt::Display for Version {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        if let Some(digest) = self.digest.as_ref() {
174            write!(f, "{}#{digest}", self.version)
175        } else {
176            fmt::Display::fmt(&self.version, f)
177        }
178    }
179}
180
181impl PartialOrd for Version {
182    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl Ord for Version {
188    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
189        use core::cmp::Ordering;
190        self.version.cmp_precedence(&other.version).then_with(|| {
191            match (self.digest.as_ref(), other.digest.as_ref()) {
192                (None, None) => Ordering::Equal,
193                (Some(l), Some(r)) => l.cmp(r),
194                (None, Some(_)) => Ordering::Less,
195                (Some(_), None) => Ordering::Greater,
196            }
197        })
198    }
199}