miden_package_registry/
version.rs1use 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#[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#[derive(Debug, Clone, Eq, PartialEq)]
57#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
58pub struct Version {
59 pub version: SemVer,
63 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 pub fn new(version: SemVer, digest: Word) -> Self {
96 Self { version, digest: Some(digest) }
97 }
98
99 pub fn without_digest(&self) -> Self {
101 Self {
102 version: self.version.clone(),
103 digest: None,
104 }
105 }
106
107 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 pub fn is_semantically_equivalent(&self, other: &Self) -> bool {
119 self.version.cmp_precedence(&other.version).is_eq()
120 }
121
122 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}