Version refactoring

This commit is contained in:
Kir_Antipov 2023-01-27 12:04:42 +00:00
parent 17bc59ff61
commit 4bbd4cc2d9
2 changed files with 236 additions and 21 deletions

View file

@ -1,31 +1,165 @@
export default class Version {
public readonly major: number;
public readonly minor: number;
public readonly build: number;
import { SemVer, coerce, parse as parseSemVer } from "semver";
public constructor(major: number, minor: number, build: number);
/**
* Represents a version number, which is a set of three non-negative integers: major, minor, and patch.
*
* This interface provides methods to compare versions and format them into a string representation.
*/
export interface Version {
/**
* The major version number.
*/
get major(): number;
public constructor(version: string);
/**
* The minor version number.
*/
get minor(): number;
public constructor(major: number | string, minor?: number, build?: number) {
if (typeof major === "string") {
[this.major, this.minor, this.build] = major.split(".").map(x => isNaN(+x) ? 0 : +x).concat(0, 0);
} else {
this.major = major || 0;
this.minor = minor || 0;
this.build = build || 0;
}
/**
* The patch version number.
*/
get patch(): number;
/**
* Compares the current version to another one.
*
* @param other - The version to compare with.
*
* @returns A number indicating the comparison result:
*
* - 0 if both versions are equal.
* - A positive number if the current version is greater.
* - A negative number if the other version is greater.
*/
compare(other?: string | Version): number;
/**
* Formats the version into a string representation.
*
* @returns The string representation of the version.
*/
format(): string;
/**
* Returns the original string representation of the version.
*
* @returns The original string representation of the version.
*/
toString(): string;
}
/**
* Parses a version string into a {@link Version} instance.
*
* @param version - The version string to parse.
*
* @returns A {@link Version} instance if parsing is successful, or `undefined` if it fails.
*/
export function parseVersion(version: string): Version | undefined {
return SemVerVersion.parse(version);
}
/**
* Regular expression for matching semver-like tags in version strings.
*/
const SEMVER_TAG_REGEX = /[a-z]{0,2}((\d+\.\d+)(\.\d+)?(.*))/i;
/**
* Represents a version number compliant with the Semantic Versioning specification.
*/
class SemVerVersion implements Version {
/**
* The SemVer object representing the parsed semantic version.
*/
private readonly _semver: SemVer;
/**
* The original string representation of the version.
*/
private readonly _version: string;
/**
* Constructs a new {@link SemVerVersion} instance.
*
* @param semver - The SemVer object representing the parsed semantic version.
* @param version - The original string representation of the version.
*/
constructor(semver: SemVer, version?: string) {
this._semver = semver;
this._version = version ?? semver.format();
}
public equals(version: unknown): boolean {
if (version instanceof Version) {
return this.major === version.major && this.minor === version.minor && this.build === version.build;
/**
* Parses a version string into a {@link SemVerVersion} instance.
*
* @param version - The version string to parse.
*
* @returns A {@link SemVerVersion} instance if parsing is successful, or `undefined` if it fails.
*/
static parse(version: string): SemVerVersion | undefined {
const semver = parseSemVer(version);
if (semver) {
return new SemVerVersion(semver, version);
}
return typeof version === "string" && this.equals(new Version(version));
const match = version.match(SEMVER_TAG_REGEX);
if (match) {
const numericVersion = match[3] ? match[1] : `${match[2]}.0${match[4]}`;
const parsedSemVer = parseSemVer(numericVersion) || coerce(numericVersion);
return new SemVerVersion(parsedSemVer, match[0]);
}
return undefined;
}
public static fromName(name: string): string {
const match = name.match(/[a-z]{0,2}\d+\.\d+.*/i);
return match ? match[0] : name;
/**
* @inheritdoc
*/
get major(): number {
return this._semver.major;
}
/**
* @inheritdoc
*/
get minor(): number {
return this._semver.minor;
}
/**
* @inheritdoc
*/
get patch(): number {
return this._semver.patch;
}
/**
* @inheritdoc
*/
compare(other?: string | Version): number {
if (other === null || other === undefined) {
return 1;
}
if (typeof other === "string") {
other = SemVerVersion.parse(other);
}
return other instanceof SemVerVersion ? this._semver.compare(other._semver) : -other.compare(this);
}
/**
* @inheritdoc
*/
format(): string {
return this._semver.format();
}
/**
* @inheritdoc
*/
toString(): string {
return this._version;
}
}

View file

@ -0,0 +1,81 @@
import { parseVersion } from "@/utils/versioning/version";
describe("parseVersion", () => {
test("returns undefined when parsing invalid string", () => {
const version = parseVersion("abc");
expect(version).toBeUndefined();
});
test("parses classic semver format (major.minor.patch)", () => {
const version = parseVersion("1.2.3");
expect(version).toMatchObject({ major: 1, minor: 2, patch: 3 });
});
test("parses classic semver format with pre-release information", () => {
const version = parseVersion("1.2.3-alpha.1");
expect(version).toMatchObject({ major: 1, minor: 2, patch: 3 });
expect(version.toString()).toBe("1.2.3-alpha.1");
});
test("parses version strings with missing patch number (major.minor)", () => {
const version = parseVersion("1.2");
expect(version).toMatchObject({ major: 1, minor: 2, patch: 0 });
expect(version.format()).toBe("1.2.0");
expect(version.toString()).toBe("1.2");
});
test("parses version strings with missing patch number and pre-release information", () => {
const version = parseVersion("1.2-alpha.1");
expect(version).toMatchObject({ major: 1, minor: 2, patch: 0 });
expect(version.format()).toBe("1.2.0-alpha.1");
expect(version.toString()).toBe("1.2-alpha.1");
});
test("file version is correctly extracted from the filename", () => {
expect(String(parseVersion("sodium-fabric-mc1.17.1-0.3.2+build.7"))).toBe("mc1.17.1-0.3.2+build.7");
expect(String(parseVersion("fabric-api-0.40.1+1.18_experimental"))).toBe("0.40.1+1.18_experimental");
expect(String(parseVersion("TechReborn-5.0.8-beta+build.111"))).toBe("5.0.8-beta+build.111");
expect(String(parseVersion("TechReborn-1.17-5.0.1-beta+build.29"))).toBe("1.17-5.0.1-beta+build.29");
expect(String(parseVersion("Terra-forge-5.3.3-BETA+ec3b0e5d"))).toBe("5.3.3-BETA+ec3b0e5d");
expect(String(parseVersion("modmenu-2.0.12"))).toBe("2.0.12");
expect(String(parseVersion("enhancedblockentities-0.5+1.17"))).toBe("0.5+1.17");
expect(String(parseVersion("sync-mc1.17.x-1.2"))).toBe("mc1.17.x-1.2");
});
});
describe("Version", () => {
test("contains valid major, minor, and patch numbers", () => {
expect(parseVersion("1.2.3")).toMatchObject({ major: 1, minor: 2, patch: 3 });
expect(parseVersion("1.2.3-alpha.1")).toMatchObject({ major: 1, minor: 2, patch: 3 });
expect(parseVersion("1.2")).toMatchObject({ major: 1, minor: 2, patch: 0 });
expect(parseVersion("1.2-alpha.1")).toMatchObject({ major: 1, minor: 2, patch: 0 });
});
test("compares versions correctly", () => {
const version1 = parseVersion("1.2.3");
const version2 = parseVersion("2.3.4");
expect(version1.compare(version2)).toBeLessThan(0);
expect(version2.compare(version1)).toBeGreaterThan(0);
expect(version1.compare(version1)).toBe(0);
});
test("formats correctly", () => {
expect(parseVersion("1.0.0").format()).toEqual("1.0.0");
expect(parseVersion("1.0.0-alpha.1").format()).toEqual("1.0.0-alpha.1");
expect(parseVersion("1.0").format()).toEqual("1.0.0");
expect(parseVersion("1.0-alpha.1").format()).toEqual("1.0.0-alpha.1");
});
test("toString returns the original string representation", () => {
expect(parseVersion("1.0.0").toString()).toEqual("1.0.0");
expect(parseVersion("1.0").toString()).toEqual("1.0");
expect(parseVersion("1.0.0-alpha.1").toString()).toEqual("1.0.0-alpha.1");
expect(parseVersion("1.0-alpha.1").toString()).toEqual("1.0-alpha.1");
});
});