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