diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml index 922f3ac..2ef71f2 100644 --- a/crates/model/Cargo.toml +++ b/crates/model/Cargo.toml @@ -6,4 +6,5 @@ edition = "2018" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -url = {version = "2.2.2", features = ["serde"]} \ No newline at end of file +url = {version = "2.2.2", features = ["serde"]} +addonscript-versioning = { path = "../versioning" } \ No newline at end of file diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index f4a3a55..3fd3a85 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use addonscript_versioning::{Version, VersionRestriction}; use serde::{Deserialize, Serialize}; pub use self::{ @@ -16,7 +17,7 @@ pub struct Manifest { pub addonscript: AddonScript, pub id: String, pub namespace: String, - pub version: String, + pub version: Version, #[serde(skip_serializing_if = "Option::is_none")] pub files: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -62,7 +63,7 @@ pub struct Relation { pub id: String, #[serde(skip_serializing_if = "Option::is_none")] pub namespace: Option, - pub version: String, + pub version: VersionRestriction, #[serde(skip_serializing_if = "Option::is_none")] pub repositories: Option>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/versioning/Cargo.toml b/crates/versioning/Cargo.toml index 3222b55..232064d 100644 --- a/crates/versioning/Cargo.toml +++ b/crates/versioning/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2018" [dependencies] -thiserror = "1.0.24" \ No newline at end of file +thiserror = "1.0.24" +semver = "1.0" +serde = "1.0" \ No newline at end of file diff --git a/crates/versioning/src/errors.rs b/crates/versioning/src/errors.rs index 02b08c6..d958b3e 100644 --- a/crates/versioning/src/errors.rs +++ b/crates/versioning/src/errors.rs @@ -11,5 +11,9 @@ pub enum Error { IllegalVersionRange, #[error("Version range overlap")] RangeOverlap, + #[error("Version contains invalid characters")] + InvalidVersion, + #[error("SemVer error: {0}")] + Semver(#[from] semver::Error), } pub type Result = std::result::Result; diff --git a/crates/versioning/src/lib.rs b/crates/versioning/src/lib.rs index b7209e2..222508a 100644 --- a/crates/versioning/src/lib.rs +++ b/crates/versioning/src/lib.rs @@ -1,6 +1,10 @@ pub mod errors; pub mod maven; +mod serde; mod util; +pub mod version; + +pub use version::{Version, VersionRestriction}; #[cfg(test)] mod tests { diff --git a/crates/versioning/src/maven/restriction.rs b/crates/versioning/src/maven/restriction.rs index 1fb30d0..123df14 100644 --- a/crates/versioning/src/maven/restriction.rs +++ b/crates/versioning/src/maven/restriction.rs @@ -4,12 +4,12 @@ use crate::{ }; use super::version::{ComparableVersion, VersionOption}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VersionRange { pub(crate) restrictions: Vec, pub(crate) recommended_version: Option, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Restriction { pub lower_bound: Option, pub lower_bound_inclusive: bool, diff --git a/crates/versioning/src/serde.rs b/crates/versioning/src/serde.rs new file mode 100644 index 0000000..6f0eead --- /dev/null +++ b/crates/versioning/src/serde.rs @@ -0,0 +1,68 @@ +use ::serde::{Deserialize, Deserializer, Serialize}; +use serde::de::Visitor; + +use crate::{Version, VersionRestriction}; + +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl Serialize for VersionRestriction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = Version; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an AddonScript version") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse::() + .map_err(|e| E::custom(format!("invalid version: {}", e))) + } + } + deserializer.deserialize_str(Vis) + } +} + +impl<'de> Deserialize<'de> for VersionRestriction { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = VersionRestriction; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an AddonScript version restriction") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse::() + .map_err(|e| E::custom(format!("invalid version restriction: {}", e))) + } + } + deserializer.deserialize_str(Vis) + } +} diff --git a/crates/versioning/src/version.rs b/crates/versioning/src/version.rs new file mode 100644 index 0000000..64e7719 --- /dev/null +++ b/crates/versioning/src/version.rs @@ -0,0 +1,141 @@ +use std::str::FromStr; + +use semver::VersionReq; + +use crate::{ + errors::Error, + maven::{restriction::VersionRange, version::ComparableVersion}, +}; + +#[derive(Clone, Eq, Debug)] +pub struct Version { + raw: String, + maven: ComparableVersion, + semver: Option, +} + +#[derive(Clone, Debug)] +pub struct VersionRestriction { + raw: String, + exact: Option, + restriction: VersionRestrictionInner, +} + +#[derive(Clone, Debug)] +enum VersionRestrictionInner { + Semver(VersionReq), + Maven(VersionRange), +} + +impl VersionRestriction { + pub fn parse(raw: String) -> Result { + if !raw.is_ascii() { + return Err(Error::IllegalVersionRange); + } + let trimmed = raw.trim().to_string(); + let is_semver = + trimmed.starts_with('=') || trimmed.starts_with('<') || trimmed.starts_with('>'); + let mut exact: Option = None; + + let inner = if is_semver { + let semver = VersionReq::parse(trimmed.as_str())?; + VersionRestrictionInner::Semver(semver) + } else { + let maven = VersionRange::parse(trimmed)?; + if maven.restrictions.is_empty() { + let recomm = maven.get_recommended().ok_or(Error::IllegalVersionRange)?; + exact = Some(Version::parse(recomm.to_string())?); + } + + VersionRestrictionInner::Maven(maven) + }; + Ok(VersionRestriction { + raw, + exact, + restriction: inner, + }) + } + + pub fn contains_version(&self, version: &Version) -> bool { + match &self.restriction { + VersionRestrictionInner::Semver(semver) => { + if let Some(sv) = &version.semver { + semver.matches(sv) + } else { + false + } + } + VersionRestrictionInner::Maven(maven) => maven.contains_version(&version.maven), + } + } + + pub fn is_exact_version(&self) -> bool { + self.exact.is_some() + } + + pub fn get_exact_version(&self) -> Option { + self.exact.clone() + } +} + +impl FromStr for VersionRestriction { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::parse(s.to_string()) + } +} + +impl ToString for VersionRestriction { + fn to_string(&self) -> String { + self.raw.clone() + } +} + +impl PartialEq for Version { + fn eq(&self, other: &Self) -> bool { + self.maven == other.maven + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + self.maven.partial_cmp(&other.maven) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.maven.cmp(&other.maven) + } +} + +impl Version { + pub fn parse(raw: String) -> Result { + if raw.contains(char::is_whitespace) || !raw.is_ascii() { + return Err(Error::InvalidVersion); + } + let maven = ComparableVersion::parse(raw.clone())?; + let semver = semver::Version::parse(raw.as_str()).ok(); + + Ok(Self { raw, maven, semver }) + } + + pub fn is_semver(&self) -> bool { + self.semver.is_some() + } +} + +impl FromStr for Version { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::parse(s.to_string()) + } +} + +impl ToString for Version { + fn to_string(&self) -> String { + self.raw.clone() + } +}