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