From 1f1e8809741e3e20ecf4463757c889963a1c5a4c Mon Sep 17 00:00:00 2001 From: harryxi Date: Thu, 19 Feb 2026 17:38:43 +0800 Subject: [PATCH 1/2] fix support semver in package manifest --- CHANGELOG.md | 3 + .../main/java/org/allaymc/api/pack/Pack.java | 12 +- .../org/allaymc/api/pack/PackManifest.java | 10 +- .../org/allaymc/api/utils/SemVersion.java | 3 +- .../allaymc/api/utils/SemanticVersion.java | 126 ++++++++++++ .../api/utils/SemanticVersionTest.java | 194 ++++++++++++++++++ 6 files changed, 333 insertions(+), 15 deletions(-) create mode 100644 api/src/main/java/org/allaymc/api/utils/SemanticVersion.java create mode 100644 server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d72c6649..c1210d60eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,12 +82,14 @@ Unless otherwise specified, any version comparison below is the comparison of th - End spawn platform creation during teleport to The End. - Added per-chunk POI persistence (LevelDB) and runtime indexing for fast nearest-portal lookup. - Updated the chunk version to 42 (1.21.120). +- (API) Added a semver2.0 implementation in `org.allaymc.api.util.version.SemanticVersion`. ### Changed - (API) Changed `BucketFillSound` and `BucketEmptySound` from `boolean water` to `Type` enum (`WATER`, `LAVA`, `POWDER_SNOW`, `FISH`). - (API) Changed `EntityPhysicsComponent.updateMotion(boolean)` to `updateMotion(LiquidState)` for richer liquid state information. - Improved physics engine motion threshold handling: small forces (e.g. buoyancy) now accumulate across ticks instead of being zeroed out. +- (API) The return type of Pack.getVersion() is changed from `SemVersion` to `SemanticVersion` to support semver in package manifest. ### Fixed @@ -95,6 +97,7 @@ Unless otherwise specified, any version comparison below is the comparison of th - Fixed door collision shapes not changing based on block state. Doors now correctly compute their collision and selection shapes based on cardinal direction, open state, and hinge side. - Fixed a bug where permission node `Permissions.ABILITY_OPERATOR_COMMAND_QUICK_BAR` does not have effect. - Fixed a bug where player permission in the player list is always visitor even if the player is already an operator. +- Allay now support semver in package manifest version fields. ### Removed diff --git a/api/src/main/java/org/allaymc/api/pack/Pack.java b/api/src/main/java/org/allaymc/api/pack/Pack.java index fd9fc307ba..fa80285e40 100644 --- a/api/src/main/java/org/allaymc/api/pack/Pack.java +++ b/api/src/main/java/org/allaymc/api/pack/Pack.java @@ -4,7 +4,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.allaymc.api.utils.SemVersion; +import org.allaymc.api.utils.SemanticVersion; import java.nio.ByteBuffer; import java.security.MessageDigest; @@ -48,18 +48,12 @@ public UUID getId() { return this.manifest.getHeader().getUuid(); } - public SemVersion getVersion() { + public SemanticVersion getVersion() { return this.manifest.getHeader().getVersion(); } public String getStringVersion() { - var version = this.getVersion(); - return String.join( - ".", - String.valueOf(version.major()), - String.valueOf(version.minor()), - String.valueOf(version.patch()) - ); + return this.getVersion().toVersionString(); } public int getSize() { diff --git a/api/src/main/java/org/allaymc/api/pack/PackManifest.java b/api/src/main/java/org/allaymc/api/pack/PackManifest.java index 2026f4deaf..1832a3fdb4 100644 --- a/api/src/main/java/org/allaymc/api/pack/PackManifest.java +++ b/api/src/main/java/org/allaymc/api/pack/PackManifest.java @@ -5,7 +5,7 @@ import com.google.gson.stream.JsonReader; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.allaymc.api.utils.SemVersion; +import org.allaymc.api.utils.SemanticVersion; import java.io.InputStreamReader; import java.util.*; @@ -26,8 +26,8 @@ public class PackManifest { static { var builder = new GsonBuilder(); builder.disableHtmlEscaping(); - builder.registerTypeAdapter(SemVersion.class, new SemVersion.Serializer()); - builder.registerTypeAdapter(SemVersion.class, new SemVersion.Deserializer()); + builder.registerTypeAdapter(SemanticVersion.class, new SemanticVersion.PackManifestSerializer()); + builder.registerTypeAdapter(SemanticVersion.class, new SemanticVersion.PackManifestDeserializer()); builder.registerTypeAdapter(Pack.Type.class, new Pack.Type.Deserializer()); builder.registerTypeAdapter(Pack.Type.class, new Pack.Type.Serializer()); builder.registerTypeAdapter(PackManifest.Capability.class, new PackManifest.Capability.Deserializer()); @@ -111,7 +111,7 @@ public static class Header { private String name; private String description; private UUID uuid; - private SemVersion version; + private SemanticVersion version; } @Data @@ -119,7 +119,7 @@ public static class Module { private UUID uuid; private String description; - private SemVersion version; + private SemanticVersion version; private Pack.Type type; } } diff --git a/api/src/main/java/org/allaymc/api/utils/SemVersion.java b/api/src/main/java/org/allaymc/api/utils/SemVersion.java index f2b2f979ee..cb5df3bcf2 100644 --- a/api/src/main/java/org/allaymc/api/utils/SemVersion.java +++ b/api/src/main/java/org/allaymc/api/utils/SemVersion.java @@ -6,7 +6,8 @@ import java.lang.reflect.Type; /** - * Represents a semantic version. + * A semantic like version for Minecraft. + * If you need a real semantic version, please use {@link SemanticVersion} instead. * * @see Semantic Versioning */ diff --git a/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java b/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java new file mode 100644 index 0000000000..93b14908e4 --- /dev/null +++ b/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java @@ -0,0 +1,126 @@ +package org.allaymc.api.utils; + +import com.google.gson.*; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A semantic version implementation that follows the Semantic Versioning 2.0.0 specification. + * + * @see Semantic Versioning + */ +public record SemanticVersion(int major, int minor, int patch, @Nullable String perRelease, + @Nullable String build) implements Comparable { + + public static SemanticVersion fromString(String versionStr) { + var r = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", Pattern.MULTILINE); + Matcher matcher = r.matcher(versionStr); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid version string: " + versionStr); + } + var major = Integer.parseInt(matcher.group(1)); + var minor = Integer.parseInt(matcher.group(2)); + var patch = Integer.parseInt(matcher.group(3)); + var preRelease = matcher.group(4); + var build = matcher.group(5); + + return new SemanticVersion(major, minor, patch, preRelease, build); + } + + public String toVersionString() { + var versionStr = String.format("%d.%d.%d", major, minor, patch); + if (perRelease != null) { + versionStr += "-" + perRelease; + } + if (build != null) { + versionStr += "+" + build; + } + return versionStr; + } + + @Override + public int compareTo(@NonNull SemanticVersion o) { + if (this.major != o.major) { + return Integer.compare(this.major, o.major); + } + if (this.minor != o.minor) { + return Integer.compare(this.minor, o.minor); + } + if (this.patch != o.patch) { + return Integer.compare(this.patch, o.patch); + } + if (this.perRelease == null && o.perRelease != null) { + return 1; + } + if (this.perRelease != null && o.perRelease == null) { + return -1; + } + if (this.perRelease != null) { + var thisPreReleaseParts = this.perRelease.split("\\."); + var otherPreReleaseParts = o.perRelease.split("\\."); + for (int i = 0; i < Math.max(thisPreReleaseParts.length, otherPreReleaseParts.length); i++) { + if (i >= thisPreReleaseParts.length) { + return -1; + } + if (i >= otherPreReleaseParts.length) { + return 1; + } + var thisPart = thisPreReleaseParts[i]; + var otherPart = otherPreReleaseParts[i]; + var thisPartIsNumeric = thisPart.matches("\\d+"); + var otherPartIsNumeric = otherPart.matches("\\d+"); + if (thisPartIsNumeric && otherPartIsNumeric) { + var cmp = Integer.compare(Integer.parseInt(thisPart), Integer.parseInt(otherPart)); + if (cmp != 0) { + return cmp; + } + } else if (!thisPartIsNumeric && !otherPartIsNumeric) { + var cmp = thisPart.compareTo(otherPart); + if (cmp != 0) { + return cmp; + } + } else { + return thisPartIsNumeric ? -1 : 1; + } + } + } + return 0; + } + + public static class PackManifestSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(SemanticVersion src, Type typeOfSrc, JsonSerializationContext context) { + if(src.perRelease == null && src.build == null) { + var json = new JsonArray(); + json.add(src.major); + json.add(src.minor); + json.add(src.patch); + return json; + } else { + return new JsonPrimitive(src.toVersionString()); + } + } + } + + public static class PackManifestDeserializer implements JsonDeserializer { + + @Override + public SemanticVersion deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if(json.isJsonArray() && (json.getAsJsonArray().size() == 3)) { + var major = json.getAsJsonArray().get(0).getAsInt(); + var minor = json.getAsJsonArray().get(1).getAsInt(); + var patch = json.getAsJsonArray().get(2).getAsInt(); + return new SemanticVersion(major, minor, patch, null, null); + } else if(json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + return SemanticVersion.fromString(json.getAsString()); + } else { + throw new JsonParseException("Invalid version format: " + json); + } + } + } +} diff --git a/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java b/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java new file mode 100644 index 0000000000..bc282c152e --- /dev/null +++ b/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java @@ -0,0 +1,194 @@ +package org.allaymc.api.utils; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SemanticVersionTest { + @Nested + class ConstructorTests { + @Test + void testBasicConstructor() { + var version = new SemanticVersion(1, 2, 3, "beta", "build-1"); + + assertEquals(1, version.major()); + assertEquals(2, version.minor()); + assertEquals(3, version.patch()); + assertEquals("beta", version.perRelease()); + assertEquals("build-1", version.build()); + } + } + + @Nested + class FromStringTests { + @Test + void simpleVersion() { + var version = SemanticVersion.fromString("1.2.3"); + + assertEquals(1, version.major()); + assertEquals(2, version.minor()); + assertEquals(3, version.patch()); + assertNull(version.perRelease()); + assertNull(version.build()); + } + + @Test + void versionOverTen() { + var version = SemanticVersion.fromString("12.34.56"); + + assertEquals(12, version.major()); + assertEquals(34, version.minor()); + assertEquals(56, version.patch()); + assertNull(version.perRelease()); + assertNull(version.build()); + } + + @Test + void versionWithPreReleaseAndBuild() { + var version = SemanticVersion.fromString("1.2.3-beta+build-1"); + + assertEquals(1, version.major()); + assertEquals(2, version.minor()); + assertEquals(3, version.patch()); + assertEquals("beta", version.perRelease()); + assertEquals("build-1", version.build()); + } + + @Test + void versionWithPreReleaseOnly() { + var version = SemanticVersion.fromString("1.2.3-beta"); + + assertEquals(1, version.major()); + assertEquals(2, version.minor()); + assertEquals(3, version.patch()); + assertEquals("beta", version.perRelease()); + assertNull(version.build()); + } + + @Test + void versionWithBuildOnly() { + var version = SemanticVersion.fromString("1.2.3+build-1"); + + assertEquals(1, version.major()); + assertEquals(2, version.minor()); + assertEquals(3, version.patch()); + assertNull(version.perRelease()); + assertEquals("build-1", version.build()); + } + } + + @Nested + class InvalidStringTests { + @Test + void missingPatch() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("1.2") + ); + } + + @Test + void nonNumericMajor() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("a.2.3") + ); + } + + @Test + void nonNumericMinor() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("1.b.3") + ); + } + + @Test + void nonNumericPatch() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("1.2.c") + ); + } + + @Test + void onlyPreRelease() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("beta") + ); + } + + @Test + void onlyBuild() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("+build-1") + ); + } + + @Test + void invalidVersion() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("invalid-version") + ); + } + + @Test + void invalidPerRelease() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("1.2.3-") + ); + } + + @Test + void invalidBuild() { + assertThrows(IllegalArgumentException.class, () -> + SemanticVersion.fromString("1.2.3+") + ); + } + } + + @Nested + class CompareTests { + @Test + void compareVersions() { + var version1 = SemanticVersion.fromString("1.2.3"); + var version2 = SemanticVersion.fromString("1.2.4"); + var version3 = SemanticVersion.fromString("1.3.0"); + var version4 = SemanticVersion.fromString("2.0.0"); + + assertTrue(version1.compareTo(version2) < 0); + assertTrue(version2.compareTo(version3) < 0); + assertTrue(version3.compareTo(version4) < 0); + assertTrue(version4.compareTo(version1) > 0); + } + + @Test + void comparePreRelease() { + var version1 = SemanticVersion.fromString("1.2.3-alpha"); + var version2 = SemanticVersion.fromString("1.2.3-beta"); + var version3 = SemanticVersion.fromString("1.2.3"); + + assertTrue(version1.compareTo(version2) < 0); + assertTrue(version2.compareTo(version3) < 0); + assertTrue(version3.compareTo(version1) > 0); + } + + @Test + void officialSemVerExamples() { + // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 + var version1 = SemanticVersion.fromString("1.0.0-alpha"); + var version2 = SemanticVersion.fromString("1.0.0-alpha.1"); + var version3 = SemanticVersion.fromString("1.0.0-alpha.beta"); + var version4 = SemanticVersion.fromString("1.0.0-beta"); + var version5 = SemanticVersion.fromString("1.0.0-beta.2"); + var version6 = SemanticVersion.fromString("1.0.0-beta.11"); + var version7 = SemanticVersion.fromString("1.0.0-rc.1"); + var version8 = SemanticVersion.fromString("1.0.0"); + + assertTrue(version1.compareTo(version2) < 0); + assertTrue(version2.compareTo(version3) < 0); + assertTrue(version3.compareTo(version4) < 0); + assertTrue(version4.compareTo(version5) < 0); + assertTrue(version5.compareTo(version6) < 0); + assertTrue(version6.compareTo(version7) < 0); + assertTrue(version7.compareTo(version8) < 0); + } + } +} From 65618aa032d9b764b1336ca700544bd639c3ed88 Mon Sep 17 00:00:00 2001 From: harryxi Date: Sat, 21 Feb 2026 01:12:09 +0800 Subject: [PATCH 2/2] fix: support semver in package manifest in a better way --- CHANGELOG.md | 3 +- api/build.gradle.kts | 3 +- .../main/java/org/allaymc/api/pack/Pack.java | 6 +- .../org/allaymc/api/pack/PackManifest.java | 9 +- .../org/allaymc/api/pack/PackVersion.java | 72 +++++++ .../org/allaymc/api/utils/SemVersion.java | 1 - .../allaymc/api/utils/SemanticVersion.java | 126 ------------ .../api/utils/SemanticVersionTest.java | 194 ------------------ 8 files changed, 82 insertions(+), 332 deletions(-) create mode 100644 api/src/main/java/org/allaymc/api/pack/PackVersion.java delete mode 100644 api/src/main/java/org/allaymc/api/utils/SemanticVersion.java delete mode 100644 server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c1210d60eb..b8ec7973e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,14 +82,13 @@ Unless otherwise specified, any version comparison below is the comparison of th - End spawn platform creation during teleport to The End. - Added per-chunk POI persistence (LevelDB) and runtime indexing for fast nearest-portal lookup. - Updated the chunk version to 42 (1.21.120). -- (API) Added a semver2.0 implementation in `org.allaymc.api.util.version.SemanticVersion`. ### Changed - (API) Changed `BucketFillSound` and `BucketEmptySound` from `boolean water` to `Type` enum (`WATER`, `LAVA`, `POWDER_SNOW`, `FISH`). - (API) Changed `EntityPhysicsComponent.updateMotion(boolean)` to `updateMotion(LiquidState)` for richer liquid state information. - Improved physics engine motion threshold handling: small forces (e.g. buoyancy) now accumulate across ticks instead of being zeroed out. -- (API) The return type of Pack.getVersion() is changed from `SemVersion` to `SemanticVersion` to support semver in package manifest. +- (API) The return type of Pack.getVersion() is changed from `SemVersion` to `Semver` from the `semver4j`. This is because the support of semver in package manifest version fields. ### Fixed diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 15582de693..1be1fb9d53 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -18,10 +18,11 @@ dependencies { exclude(group = "org.joml", module = "joml") } api(libs.snakeyaml) + api(libs.semver4j) } tasks.withType().configureEach { - enabled = true; + enabled = true isFailOnError = false (options as StandardJavadocDocletOptions).apply { diff --git a/api/src/main/java/org/allaymc/api/pack/Pack.java b/api/src/main/java/org/allaymc/api/pack/Pack.java index fa80285e40..68d05000f5 100644 --- a/api/src/main/java/org/allaymc/api/pack/Pack.java +++ b/api/src/main/java/org/allaymc/api/pack/Pack.java @@ -4,7 +4,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.allaymc.api.utils.SemanticVersion; +import org.semver4j.Semver; import java.nio.ByteBuffer; import java.security.MessageDigest; @@ -48,12 +48,12 @@ public UUID getId() { return this.manifest.getHeader().getUuid(); } - public SemanticVersion getVersion() { + public Semver getVersion() { return this.manifest.getHeader().getVersion(); } public String getStringVersion() { - return this.getVersion().toVersionString(); + return this.getVersion().getVersion(); } public int getSize() { diff --git a/api/src/main/java/org/allaymc/api/pack/PackManifest.java b/api/src/main/java/org/allaymc/api/pack/PackManifest.java index 1832a3fdb4..a6a000caff 100644 --- a/api/src/main/java/org/allaymc/api/pack/PackManifest.java +++ b/api/src/main/java/org/allaymc/api/pack/PackManifest.java @@ -5,7 +5,6 @@ import com.google.gson.stream.JsonReader; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.allaymc.api.utils.SemanticVersion; import java.io.InputStreamReader; import java.util.*; @@ -26,8 +25,8 @@ public class PackManifest { static { var builder = new GsonBuilder(); builder.disableHtmlEscaping(); - builder.registerTypeAdapter(SemanticVersion.class, new SemanticVersion.PackManifestSerializer()); - builder.registerTypeAdapter(SemanticVersion.class, new SemanticVersion.PackManifestDeserializer()); + builder.registerTypeAdapter(PackVersion.class, new PackVersion.Deserializer()); + builder.registerTypeAdapter(PackVersion.class, new PackVersion.Serializer()); builder.registerTypeAdapter(Pack.Type.class, new Pack.Type.Deserializer()); builder.registerTypeAdapter(Pack.Type.class, new Pack.Type.Serializer()); builder.registerTypeAdapter(PackManifest.Capability.class, new PackManifest.Capability.Deserializer()); @@ -111,7 +110,7 @@ public static class Header { private String name; private String description; private UUID uuid; - private SemanticVersion version; + private PackVersion version; } @Data @@ -119,7 +118,7 @@ public static class Module { private UUID uuid; private String description; - private SemanticVersion version; + private PackVersion version; private Pack.Type type; } } diff --git a/api/src/main/java/org/allaymc/api/pack/PackVersion.java b/api/src/main/java/org/allaymc/api/pack/PackVersion.java new file mode 100644 index 0000000000..29409e9e16 --- /dev/null +++ b/api/src/main/java/org/allaymc/api/pack/PackVersion.java @@ -0,0 +1,72 @@ +package org.allaymc.api.pack; + + +import com.google.gson.*; +import org.semver4j.Semver; + +/** + * This class is not for public use! It has been mark as public just for the convenience of Gson serialization and deserialization. + * A version for pack manifest, which store the version in either an array format or a string format. + * + * @author harry-xi + */ +public class PackVersion extends Semver { + private final PackVersionType type; + + PackVersion(String version, PackVersionType type) { + super(version); + this.type = type; + } + + PackVersion(String version) { + this(version, PackVersionType.Arr); + } + + PackVersion(int major, int minor, int patch) { + this( String.format("%d.%d.%d", major, minor, patch), PackVersionType.Arr); + } + + enum PackVersionType { + Arr, + Str + } + + protected static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(PackVersion src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) { + if(src.type == PackVersionType.Arr) { + var arr = new JsonArray(); + arr.add(src.getMajor()); + arr.add(src.getMinor()); + arr.add(src.getPatch()); + return arr; + } else { + return new JsonPrimitive(src.toString()); + } + } + } + + protected static class Deserializer implements JsonDeserializer { + @Override + public PackVersion deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if(json.isJsonArray()) { + var arr = json.getAsJsonArray(); + if(arr.size() != 3) { + throw new JsonParseException("Version array must have at least 3 elements"); + } + var major = arr.get(0).getAsInt(); + var minor = arr.get(1).getAsInt(); + var patch = arr.get(2).getAsInt(); + return new PackVersion(major, minor, patch); + } else if(json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { + try { + return new PackVersion(json.getAsJsonPrimitive().getAsString()); + } catch (Exception e) { + throw new JsonParseException("Invalid version string: " + json.getAsString(), e); + } + } else { + throw new JsonParseException("Invalid version format: " + json); + } + } + } +} diff --git a/api/src/main/java/org/allaymc/api/utils/SemVersion.java b/api/src/main/java/org/allaymc/api/utils/SemVersion.java index cb5df3bcf2..a2329b2bc9 100644 --- a/api/src/main/java/org/allaymc/api/utils/SemVersion.java +++ b/api/src/main/java/org/allaymc/api/utils/SemVersion.java @@ -7,7 +7,6 @@ /** * A semantic like version for Minecraft. - * If you need a real semantic version, please use {@link SemanticVersion} instead. * * @see Semantic Versioning */ diff --git a/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java b/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java deleted file mode 100644 index 93b14908e4..0000000000 --- a/api/src/main/java/org/allaymc/api/utils/SemanticVersion.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.allaymc.api.utils; - -import com.google.gson.*; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import java.lang.reflect.Type; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A semantic version implementation that follows the Semantic Versioning 2.0.0 specification. - * - * @see Semantic Versioning - */ -public record SemanticVersion(int major, int minor, int patch, @Nullable String perRelease, - @Nullable String build) implements Comparable { - - public static SemanticVersion fromString(String versionStr) { - var r = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", Pattern.MULTILINE); - Matcher matcher = r.matcher(versionStr); - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid version string: " + versionStr); - } - var major = Integer.parseInt(matcher.group(1)); - var minor = Integer.parseInt(matcher.group(2)); - var patch = Integer.parseInt(matcher.group(3)); - var preRelease = matcher.group(4); - var build = matcher.group(5); - - return new SemanticVersion(major, minor, patch, preRelease, build); - } - - public String toVersionString() { - var versionStr = String.format("%d.%d.%d", major, minor, patch); - if (perRelease != null) { - versionStr += "-" + perRelease; - } - if (build != null) { - versionStr += "+" + build; - } - return versionStr; - } - - @Override - public int compareTo(@NonNull SemanticVersion o) { - if (this.major != o.major) { - return Integer.compare(this.major, o.major); - } - if (this.minor != o.minor) { - return Integer.compare(this.minor, o.minor); - } - if (this.patch != o.patch) { - return Integer.compare(this.patch, o.patch); - } - if (this.perRelease == null && o.perRelease != null) { - return 1; - } - if (this.perRelease != null && o.perRelease == null) { - return -1; - } - if (this.perRelease != null) { - var thisPreReleaseParts = this.perRelease.split("\\."); - var otherPreReleaseParts = o.perRelease.split("\\."); - for (int i = 0; i < Math.max(thisPreReleaseParts.length, otherPreReleaseParts.length); i++) { - if (i >= thisPreReleaseParts.length) { - return -1; - } - if (i >= otherPreReleaseParts.length) { - return 1; - } - var thisPart = thisPreReleaseParts[i]; - var otherPart = otherPreReleaseParts[i]; - var thisPartIsNumeric = thisPart.matches("\\d+"); - var otherPartIsNumeric = otherPart.matches("\\d+"); - if (thisPartIsNumeric && otherPartIsNumeric) { - var cmp = Integer.compare(Integer.parseInt(thisPart), Integer.parseInt(otherPart)); - if (cmp != 0) { - return cmp; - } - } else if (!thisPartIsNumeric && !otherPartIsNumeric) { - var cmp = thisPart.compareTo(otherPart); - if (cmp != 0) { - return cmp; - } - } else { - return thisPartIsNumeric ? -1 : 1; - } - } - } - return 0; - } - - public static class PackManifestSerializer implements JsonSerializer { - - @Override - public JsonElement serialize(SemanticVersion src, Type typeOfSrc, JsonSerializationContext context) { - if(src.perRelease == null && src.build == null) { - var json = new JsonArray(); - json.add(src.major); - json.add(src.minor); - json.add(src.patch); - return json; - } else { - return new JsonPrimitive(src.toVersionString()); - } - } - } - - public static class PackManifestDeserializer implements JsonDeserializer { - - @Override - public SemanticVersion deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if(json.isJsonArray() && (json.getAsJsonArray().size() == 3)) { - var major = json.getAsJsonArray().get(0).getAsInt(); - var minor = json.getAsJsonArray().get(1).getAsInt(); - var patch = json.getAsJsonArray().get(2).getAsInt(); - return new SemanticVersion(major, minor, patch, null, null); - } else if(json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) { - return SemanticVersion.fromString(json.getAsString()); - } else { - throw new JsonParseException("Invalid version format: " + json); - } - } - } -} diff --git a/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java b/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java deleted file mode 100644 index bc282c152e..0000000000 --- a/server/src/test/java/org/allaymc/api/utils/SemanticVersionTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.allaymc.api.utils; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class SemanticVersionTest { - @Nested - class ConstructorTests { - @Test - void testBasicConstructor() { - var version = new SemanticVersion(1, 2, 3, "beta", "build-1"); - - assertEquals(1, version.major()); - assertEquals(2, version.minor()); - assertEquals(3, version.patch()); - assertEquals("beta", version.perRelease()); - assertEquals("build-1", version.build()); - } - } - - @Nested - class FromStringTests { - @Test - void simpleVersion() { - var version = SemanticVersion.fromString("1.2.3"); - - assertEquals(1, version.major()); - assertEquals(2, version.minor()); - assertEquals(3, version.patch()); - assertNull(version.perRelease()); - assertNull(version.build()); - } - - @Test - void versionOverTen() { - var version = SemanticVersion.fromString("12.34.56"); - - assertEquals(12, version.major()); - assertEquals(34, version.minor()); - assertEquals(56, version.patch()); - assertNull(version.perRelease()); - assertNull(version.build()); - } - - @Test - void versionWithPreReleaseAndBuild() { - var version = SemanticVersion.fromString("1.2.3-beta+build-1"); - - assertEquals(1, version.major()); - assertEquals(2, version.minor()); - assertEquals(3, version.patch()); - assertEquals("beta", version.perRelease()); - assertEquals("build-1", version.build()); - } - - @Test - void versionWithPreReleaseOnly() { - var version = SemanticVersion.fromString("1.2.3-beta"); - - assertEquals(1, version.major()); - assertEquals(2, version.minor()); - assertEquals(3, version.patch()); - assertEquals("beta", version.perRelease()); - assertNull(version.build()); - } - - @Test - void versionWithBuildOnly() { - var version = SemanticVersion.fromString("1.2.3+build-1"); - - assertEquals(1, version.major()); - assertEquals(2, version.minor()); - assertEquals(3, version.patch()); - assertNull(version.perRelease()); - assertEquals("build-1", version.build()); - } - } - - @Nested - class InvalidStringTests { - @Test - void missingPatch() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("1.2") - ); - } - - @Test - void nonNumericMajor() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("a.2.3") - ); - } - - @Test - void nonNumericMinor() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("1.b.3") - ); - } - - @Test - void nonNumericPatch() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("1.2.c") - ); - } - - @Test - void onlyPreRelease() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("beta") - ); - } - - @Test - void onlyBuild() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("+build-1") - ); - } - - @Test - void invalidVersion() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("invalid-version") - ); - } - - @Test - void invalidPerRelease() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("1.2.3-") - ); - } - - @Test - void invalidBuild() { - assertThrows(IllegalArgumentException.class, () -> - SemanticVersion.fromString("1.2.3+") - ); - } - } - - @Nested - class CompareTests { - @Test - void compareVersions() { - var version1 = SemanticVersion.fromString("1.2.3"); - var version2 = SemanticVersion.fromString("1.2.4"); - var version3 = SemanticVersion.fromString("1.3.0"); - var version4 = SemanticVersion.fromString("2.0.0"); - - assertTrue(version1.compareTo(version2) < 0); - assertTrue(version2.compareTo(version3) < 0); - assertTrue(version3.compareTo(version4) < 0); - assertTrue(version4.compareTo(version1) > 0); - } - - @Test - void comparePreRelease() { - var version1 = SemanticVersion.fromString("1.2.3-alpha"); - var version2 = SemanticVersion.fromString("1.2.3-beta"); - var version3 = SemanticVersion.fromString("1.2.3"); - - assertTrue(version1.compareTo(version2) < 0); - assertTrue(version2.compareTo(version3) < 0); - assertTrue(version3.compareTo(version1) > 0); - } - - @Test - void officialSemVerExamples() { - // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 - var version1 = SemanticVersion.fromString("1.0.0-alpha"); - var version2 = SemanticVersion.fromString("1.0.0-alpha.1"); - var version3 = SemanticVersion.fromString("1.0.0-alpha.beta"); - var version4 = SemanticVersion.fromString("1.0.0-beta"); - var version5 = SemanticVersion.fromString("1.0.0-beta.2"); - var version6 = SemanticVersion.fromString("1.0.0-beta.11"); - var version7 = SemanticVersion.fromString("1.0.0-rc.1"); - var version8 = SemanticVersion.fromString("1.0.0"); - - assertTrue(version1.compareTo(version2) < 0); - assertTrue(version2.compareTo(version3) < 0); - assertTrue(version3.compareTo(version4) < 0); - assertTrue(version4.compareTo(version5) < 0); - assertTrue(version5.compareTo(version6) < 0); - assertTrue(version6.compareTo(version7) < 0); - assertTrue(version7.compareTo(version8) < 0); - } - } -}