Skip to main content

miden_package_registry/
version_requirement.rs

1use core::fmt;
2
3use miden_assembly_syntax::debuginfo::Span;
4#[cfg(feature = "arbitrary")]
5use miden_core::utils::hash_string_to_word;
6#[cfg(feature = "arbitrary")]
7use proptest::prelude::*;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use super::*;
12use crate::Word;
13
14/// Represents a requirement on a specific version (or versions) of a dependency.
15#[derive(Debug, Clone)]
16#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
17pub enum VersionRequirement {
18    /// A semantic versioning constraint, e.g. `~> 0.1`
19    ///
20    /// In general, this is meant to indicate that any version of a package that satisfies the
21    /// version constraint can be used to resolve the dependency.
22    ///
23    /// This form of constraint also permits us to compile a dependency from source, so long as
24    /// the semantic versioning constraint is satisfied.
25    Semantic(Span<VersionReq>),
26    /// The most precise and onerous form of versioning constraint.
27    ///
28    /// This requires that the dependency's package digest exactly matches the one provided here.
29    ///
30    /// Digest constraints also effectively require that the dependency already be compiled to a
31    /// Miden package, as digests are derived from the MAST of a compiled package. This means that
32    /// when the dependency is resolved, we must be able to find a `.masp` file with the expected
33    /// digest.
34    Digest(Span<Word>),
35    /// Requires an exact assembled package version, including both semantic version and digest.
36    Exact(Version),
37}
38
39impl VersionRequirement {
40    /// Returns true if this version requirement is a semantic versioning requirement
41    pub fn is_semantic_version(&self) -> bool {
42        matches!(self, Self::Semantic(_))
43    }
44
45    /// Returns true if this version requirement requires an exact digest match
46    pub fn is_digest(&self) -> bool {
47        matches!(self, Self::Digest(_))
48    }
49
50    /// Returns true if this version requirement requires an exact assembled version match.
51    pub fn is_exact(&self) -> bool {
52        matches!(self, Self::Exact(_))
53    }
54}
55
56impl Eq for VersionRequirement {}
57
58impl PartialEq for VersionRequirement {
59    fn eq(&self, other: &Self) -> bool {
60        match (self, other) {
61            (Self::Exact(l), Self::Exact(r)) => l == r,
62            (Self::Digest(l), Self::Digest(r)) => l.into_inner() == r.into_inner(),
63            (Self::Semantic(l), Self::Semantic(r)) => l == r,
64            (Self::Semantic(_) | Self::Exact(_), Self::Digest(_))
65            | (Self::Semantic(_), Self::Exact(_))
66            | (Self::Digest(_), Self::Semantic(_) | Self::Exact(_))
67            | (Self::Exact(_), Self::Semantic(_)) => false,
68        }
69    }
70}
71
72impl fmt::Display for VersionRequirement {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::Semantic(v) => fmt::Display::fmt(v, f),
76            Self::Digest(word) => fmt::Display::fmt(word, f),
77            Self::Exact(version) => {
78                assert!(
79                    version.digest.is_some(),
80                    "exact requirements must include an artifact digest"
81                );
82                write!(f, "{version}")
83            },
84        }
85    }
86}
87
88impl From<VersionReq> for VersionRequirement {
89    fn from(version: VersionReq) -> Self {
90        Self::Semantic(Span::unknown(version))
91    }
92}
93
94impl From<Word> for VersionRequirement {
95    fn from(digest: Word) -> Self {
96        Self::Digest(Span::unknown(digest))
97    }
98}
99
100impl From<Version> for VersionRequirement {
101    fn from(value: Version) -> Self {
102        if value.digest.is_none() {
103            Self::Semantic(Span::unknown(format!("={}", value.version).parse().unwrap()))
104        } else {
105            Self::Exact(value)
106        }
107    }
108}
109
110#[cfg(feature = "serde")]
111impl Serialize for VersionRequirement {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: serde::Serializer,
115    {
116        use alloc::string::ToString;
117        serializer.serialize_str(&self.to_string())
118    }
119}
120
121#[cfg(feature = "serde")]
122impl<'de> Deserialize<'de> for VersionRequirement {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: serde::Deserializer<'de>,
126    {
127        use core::str::FromStr;
128
129        let value = <String as Deserialize>::deserialize(deserializer)?;
130
131        if value == "*" {
132            return Ok(Self::from(VersionReq::STAR));
133        }
134
135        if let Some((version, digest)) = value.split_once('#') {
136            let version = version.parse::<SemVer>().map_err(serde::de::Error::custom)?;
137            let digest = Word::parse(digest).map_err(serde::de::Error::custom)?;
138            return Ok(Self::Exact(Version::new(version, digest)));
139        }
140
141        if let Ok(digest) = Word::parse(&value) {
142            return Ok(Self::from(digest));
143        }
144
145        let requirement = VersionReq::from_str(&value).map_err(serde::de::Error::custom)?;
146        Ok(Self::from(requirement))
147    }
148}
149
150#[cfg(feature = "arbitrary")]
151impl Arbitrary for VersionRequirement {
152    type Parameters = ();
153    type Strategy = BoxedStrategy<Self>;
154
155    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
156        let semantic =
157            (0u64..=4, 0u64..=8, 0u64..=16, 0u8..=2).prop_map(|(major, minor, patch, kind)| {
158                let req = match kind {
159                    0 => format!("^{major}.{minor}.{patch}"),
160                    1 => format!("~{major}.{minor}.{patch}"),
161                    _ => format!("={major}.{minor}.{patch}"),
162                }
163                .parse::<VersionReq>()
164                .expect("generated version requirements are valid");
165
166                Self::Semantic(Span::unknown(req))
167            });
168
169        let digest =
170            proptest::collection::vec(proptest::char::range('a', 'z'), 1..16).prop_map(|chars| {
171                let material = chars.into_iter().collect::<String>();
172                let digest = hash_string_to_word(material.as_str());
173                Self::Digest(Span::unknown(digest))
174            });
175
176        let exact = any::<Version>()
177            .prop_filter("exact requirements must include a digest", |version| {
178                version.digest.is_some()
179            })
180            .prop_map(Self::Exact);
181
182        proptest::prop_oneof![Just(Self::from(VersionReq::STAR)), semantic, digest, exact,].boxed()
183    }
184}