summaryrefslogtreecommitdiff
path: root/crates/typst-syntax
diff options
context:
space:
mode:
authorClemens Koza <clemens.koza@gmx.at>2024-06-22 10:41:19 +0200
committerGitHub <noreply@github.com>2024-06-22 08:41:19 +0000
commit3d3489fbaef7524655f5c911e0bf6dec7394f90e (patch)
tree6b011e024d438f39777dd9ba4f9eba9600cb06f6 /crates/typst-syntax
parentddce645ef006ac86109a7dfe8cc9d3565598fbf0 (diff)
Allow "imprecise" bounds for the compiler version (#4394)
Diffstat (limited to 'crates/typst-syntax')
-rw-r--r--crates/typst-syntax/src/package.rs154
1 files changed, 152 insertions, 2 deletions
diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs
index 138b39ca..911af6d4 100644
--- a/crates/typst-syntax/src/package.rs
+++ b/crates/typst-syntax/src/package.rs
@@ -40,7 +40,7 @@ pub struct PackageInfo {
/// The path of the entrypoint into the package.
pub entrypoint: EcoString,
/// The minimum required compiler version for the package.
- pub compiler: Option<PackageVersion>,
+ pub compiler: Option<VersionBound>,
}
impl PackageManifest {
@@ -62,7 +62,7 @@ impl PackageManifest {
if let Some(required) = self.package.compiler {
let current = PackageVersion::compiler();
- if current < required {
+ if !current.matches_ge(&required) {
return Err(eco_format!(
"package requires typst {required} or newer \
(current version is {current})"
@@ -214,6 +214,62 @@ impl PackageVersion {
patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
}
}
+
+ /// Performs an `==` match with the given version bound. Version elements
+ /// missing in the bound are ignored.
+ pub fn matches_eq(&self, bound: &VersionBound) -> bool {
+ self.major == bound.major
+ && bound.minor.map_or(true, |minor| self.minor == minor)
+ && bound.patch.map_or(true, |patch| self.patch == patch)
+ }
+
+ /// Performs a `>` match with the given version bound. The match only
+ /// succeeds if some version element in the bound is actually greater than
+ /// that of the version.
+ pub fn matches_gt(&self, bound: &VersionBound) -> bool {
+ if self.major != bound.major {
+ return self.major > bound.major;
+ }
+ let Some(minor) = bound.minor else { return false };
+ if self.minor != minor {
+ return self.minor > minor;
+ }
+ let Some(patch) = bound.patch else { return false };
+ if self.patch != patch {
+ return self.patch > patch;
+ }
+ false
+ }
+
+ /// Performs a `<` match with the given version bound. The match only
+ /// succeeds if some version element in the bound is actually less than that
+ /// of the version.
+ pub fn matches_lt(&self, bound: &VersionBound) -> bool {
+ if self.major != bound.major {
+ return self.major < bound.major;
+ }
+ let Some(minor) = bound.minor else { return false };
+ if self.minor != minor {
+ return self.minor < minor;
+ }
+ let Some(patch) = bound.patch else { return false };
+ if self.patch != patch {
+ return self.patch < patch;
+ }
+ false
+ }
+
+ /// Performs a `>=` match with the given versions. The match succeeds when
+ /// either a `==` or `>` match does.
+ pub fn matches_ge(&self, bound: &VersionBound) -> bool {
+ self.matches_eq(bound) || self.matches_gt(bound)
+ }
+
+ /// Performs a `<=` match with the given versions. The match succeeds when
+ /// either a `==` or `<` match does.
+ pub fn matches_le(&self, bound: &VersionBound) -> bool {
+ self.matches_eq(bound) || self.matches_lt(bound)
+ }
}
impl FromStr for PackageVersion {
@@ -265,3 +321,97 @@ impl<'de> Deserialize<'de> for PackageVersion {
string.parse().map_err(serde::de::Error::custom)
}
}
+
+/// A version bound for compatibility specification.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct VersionBound {
+ /// The bounds's major version.
+ pub major: u32,
+ /// The bounds's minor version.
+ pub minor: Option<u32>,
+ /// The bounds's patch version. Can only be present if minor is too.
+ pub patch: Option<u32>,
+}
+
+impl FromStr for VersionBound {
+ type Err = EcoString;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut parts = s.split('.');
+ let mut next = |kind| {
+ if let Some(part) = parts.next() {
+ part.parse::<u32>().map(Some).map_err(|_| {
+ eco_format!("`{part}` is not a valid {kind} version bound")
+ })
+ } else {
+ Ok(None)
+ }
+ };
+
+ let major = next("major")?
+ .ok_or_else(|| eco_format!("version bound is missing major version"))?;
+ let minor = next("minor")?;
+ let patch = next("patch")?;
+ if let Some(rest) = parts.next() {
+ Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?;
+ }
+
+ Ok(Self { major, minor, patch })
+ }
+}
+
+impl Debug for VersionBound {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ Display::fmt(self, f)
+ }
+}
+
+impl Display for VersionBound {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{}", self.major)?;
+ if let Some(minor) = self.minor {
+ write!(f, ".{minor}")?;
+ }
+ if let Some(patch) = self.patch {
+ write!(f, ".{patch}")?;
+ }
+ Ok(())
+ }
+}
+
+impl Serialize for VersionBound {
+ fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
+ s.collect_str(self)
+ }
+}
+
+impl<'de> Deserialize<'de> for VersionBound {
+ fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
+ let string = EcoString::deserialize(d)?;
+ string.parse().map_err(serde::de::Error::custom)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use super::*;
+
+ #[test]
+ fn version_version_match() {
+ let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap();
+
+ assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap()));
+ assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap()));
+ assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap()));
+
+ assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap()));
+ assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap()));
+ assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap()));
+
+ assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap()));
+ assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
+ assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
+ }
+}