diff --git a/src/games/game-version-filter.ts b/src/games/game-version-filter.ts new file mode 100644 index 0000000..866e5fd --- /dev/null +++ b/src/games/game-version-filter.ts @@ -0,0 +1,254 @@ +import { Enum, EnumOptions } from "@/utils/enum"; +import { stringEquals } from "@/utils/string-utils"; +import { deprecate } from "node:util"; +import { GameVersion } from "./game-version"; + +// TODO: Remove deprecated stuff in v4.0 + +/** + * Represents a game version filter. + * + * This filter can be used to filter game versions based on the provided criteria. + * + * @partial + */ +enum GameVersionFilterValues { + /** + * No filter applied. + */ + NONE = 0, + + /** + * Filter to include release versions. + */ + RELEASES = 1, + + /** + * Filter to include beta versions. + */ + BETAS = 2, + + /** + * Filter to include alpha versions. + */ + ALPHAS = 4, + + /** + * Filter to include both alpha and beta versions (snapshots). + */ + SNAPSHOTS = ALPHAS | BETAS, + + /** + * Filter to include any version type. + */ + ANY = RELEASES | SNAPSHOTS, + + /** + * Filter to include versions with the minimum patch number. + */ + MIN_PATCH = 8, + + /** + * Filter to include versions with the maximum patch number. + */ + MAX_PATCH = 16, + + /** + * Filter to include versions with the minimum minor number. + */ + MIN_MINOR = 32, + + /** + * Filter to include versions with the maximum minor number. + */ + MAX_MINOR = 64, + + /** + * Filter to include versions with the minimum major number. + */ + MIN_MAJOR = 128, + + /** + * Filter to include versions with the maximum major number. + */ + MAX_MAJOR = 256, + + /** + * Filter to include the last version in a range, considering major, minor, and patch numbers. + */ + MIN = MIN_MAJOR | MIN_MINOR | MIN_PATCH, + + /** + * Filter to include the first version in a range, considering major, minor, and patch numbers. + */ + MAX = MAX_MAJOR | MAX_MINOR | MAX_PATCH, +} + +/** + * Options for configuring the behavior of the `GameVersionFilter` enum. + * + * @partial + */ +const GameVersionFilterOptions: EnumOptions = { + /** + * `GameVersionFilter` is a flag-based enum. + */ + hasFlags: true, + + /** + * The case should be ignored while parsing the filter. + */ + ignoreCase: true, + + /** + * Non-word characters should be ignored while parsing the filter. + */ + ignoreNonWordCharacters: true, +}; + +/** + * Filters game versions based on the provided filter. + * + * @template T - The type of the game versions. + * + * @param versions - An iterable of game versions to filter. + * @param filter - The filter to apply to the versions. + * + * @returns An array of filtered game versions. + */ +function filter(versions: Iterable, filter: GameVersionFilter): T[] { + let filtered = [...versions]; + if (filter === GameVersionFilter.NONE || !filter) { + return filtered; + } + + filtered = filterVersionType(filtered, filter); + filtered = applyVersionRange(filtered, x => x.version.major, filter, GameVersionFilter.MIN_MAJOR, GameVersionFilter.MAX_MAJOR); + filtered = applyVersionRange(filtered, x => x.version.minor, filter, GameVersionFilter.MIN_MINOR, GameVersionFilter.MAX_MINOR); + filtered = applyVersionRange(filtered, x => x.version.patch, filter, GameVersionFilter.MIN_PATCH, GameVersionFilter.MAX_PATCH); + + return filtered; +} + +/** + * Filters game versions based on version type. + * + * @template T - The type of the game versions. + * + * @param versions - An array of game versions to filter. + * @param filter - The filter to apply to the versions. + * + * @returns An array of filtered game versions. + */ +function filterVersionType(versions: T[], filter: GameVersionFilter): T[] { + const allowReleases = GameVersionFilter.hasFlag(filter, GameVersionFilter.RELEASES); + const allowBetas = GameVersionFilter.hasFlag(filter, GameVersionFilter.BETAS); + const allowAlphas = GameVersionFilter.hasFlag(filter, GameVersionFilter.ALPHAS); + const allowAny = (allowReleases && allowBetas && allowAlphas) || !(allowReleases || allowBetas || allowAlphas); + + if (!allowAny) { + return versions.filter(x => (!x.isRelease || allowReleases) && (!x.isBeta || allowBetas) && (!x.isAlpha || allowAlphas)); + } + + return versions; +} + +/** + * Applies a version range filter based on the provided flags. + * + * @template T - The type of the game versions. + * + * @param versions - An array of game versions to filter. + * @param selector - A function to select a specific version value (major, minor, or patch). + * @param flags - The filter flags to apply to the versions. + * @param minFlag - The `minimum` flag applicable to the selected version value. + * @param maxFlag - The `maximum` flag applicable to the selected version value. + * + * @returns An array of filtered game versions. + */ +function applyVersionRange(versions: T[], selector: (x: T) => number, flags: number, minFlag: number, maxFlag: number): T[] { + const comparer = GameVersionFilter.hasFlag(flags, minFlag) ? -1 : GameVersionFilter.hasFlag(flags, maxFlag) ? 1 : 0; + if (!comparer) { + return versions; + } + + const target = versions.reduce((current, version) => Math.sign(selector(version) - current) === comparer ? selector(version) : current, comparer === 1 ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER); + return versions.filter(x => selector(x) === target); +} + +/** + * Converts a version resolver name to a game version filter. + * + * @param versionResolverName - The name of the version resolver. + * + * @returns The corresponding game version filter. + */ +function _fromVersionResolver(versionResolverName: string): GameVersionFilter { + if (stringEquals(versionResolverName, "exact", { ignoreCase: true })) { + return GameVersionFilterValues.MIN | GameVersionFilterValues.RELEASES; + } + + if (stringEquals(versionResolverName, "latest", { ignoreCase: true })) { + return ( + GameVersionFilterValues.MIN_MAJOR | + GameVersionFilterValues.MIN_MINOR | + GameVersionFilterValues.MAX_PATCH | + GameVersionFilterValues.RELEASES + ); + } + + if (stringEquals(versionResolverName, "all", { ignoreCase: true })) { + return GameVersionFilterValues.MIN_MAJOR | GameVersionFilterValues.MIN_MINOR; + } + + return ( + GameVersionFilterValues.MIN_MAJOR | + GameVersionFilterValues.MIN_MINOR | + GameVersionFilterValues.RELEASES + ); +} + +/** + * Converts a version resolver name to a game version filter. + * + * @param versionResolverName - The name of the version resolver. + * + * @returns The corresponding game version filter. + * + * @deprecated + * + * Use keys of the new {@link GameVersionFilter} instead. + */ +const fromVersionResolver = deprecate( + _fromVersionResolver, + "Use the new `game-version-filter` input instead of the deprecated `version-resolver` one." +); + +/** + * A collection of methods to work with `GameVersionFilter`. + * + * @partial + */ +const GameVersionFilterMethods = { + filter, + fromVersionResolver, +}; + + +/** + * Represents a game version filter. + * + * This filter can be used to filter game versions based on the provided criteria. + */ +export const GameVersionFilter = Enum.create( + GameVersionFilterValues, + GameVersionFilterOptions, + GameVersionFilterMethods, +); + +/** + * Represents a game version filter. + * + * This filter can be used to filter game versions based on the provided criteria. + */ +export type GameVersionFilter = Enum; diff --git a/src/utils/minecraft/minecraft-version-resolver.ts b/src/utils/minecraft/minecraft-version-resolver.ts deleted file mode 100644 index 2875a2f..0000000 --- a/src/utils/minecraft/minecraft-version-resolver.ts +++ /dev/null @@ -1,24 +0,0 @@ -import GameVersionResolver from "../versioning/game-version-resolver"; -import { getCompatibleBuilds, MinecraftVersion } from "."; -import Version from "../versioning/version"; - -export default class MinecraftVersionResolver extends GameVersionResolver { - public static readonly exact = new MinecraftVersionResolver((n, v) => [v.find(x => x.version.equals(n))].filter(x => x)); - public static readonly latest = new MinecraftVersionResolver((_, v) => v.find(x => x.isRelease) ? [v.find(x => x.isRelease)] : v.length ? [v[0]] : []); - public static readonly all = new MinecraftVersionResolver((_, v) => v); - public static readonly releases = new MinecraftVersionResolver((_, v) => v.filter(x => x.isRelease)); - public static readonly releasesIfAny = new MinecraftVersionResolver((_, v) => v.find(x => x.isRelease) ? v.filter(x => x.isRelease) : v); - - public static byName(name: string): MinecraftVersionResolver | null { - for (const [key, value] of Object.entries(MinecraftVersionResolver)) { - if (value instanceof MinecraftVersionResolver && key.localeCompare(name, undefined, { sensitivity: "accent" }) === 0) { - return value; - } - } - return null; - } - - public getCompatibleVersions(version: string | Version): Promise { - return getCompatibleBuilds(version); - } -} diff --git a/src/utils/versioning/game-version-resolver.ts b/src/utils/versioning/game-version-resolver.ts deleted file mode 100644 index 8c03814..0000000 --- a/src/utils/versioning/game-version-resolver.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Version from "./version"; - -export default abstract class GameVersionResolver { - private readonly _filter: (version: string | Version, versions: TGameVersion[]) => TGameVersion[]; - - protected constructor(filter?: (version: string | Version, versions: TGameVersion[]) => TGameVersion[]) { - this._filter = filter || ((_, x) => x); - } - - public async resolve(version: string | Version): Promise { - return this.filter(version, await this.getCompatibleVersions(version)); - } - - public filter(version: string | Version, versions: TGameVersion[]): TGameVersion[] { - return this._filter(version, versions); - } - - public abstract getCompatibleVersions(version: string | Version): Promise; -} diff --git a/tests/unit/games/game-version-filter.spec.ts b/tests/unit/games/game-version-filter.spec.ts new file mode 100644 index 0000000..c4802ae --- /dev/null +++ b/tests/unit/games/game-version-filter.spec.ts @@ -0,0 +1,208 @@ +import { parseVersion } from "@/utils/versioning/version"; +import { GameVersion } from "@/games/game-version"; +import { GameVersionFilter } from "@/games/game-version-filter"; + +describe("GameVersionFilter", () => { + describe("filter", () => { + let GAME_VERSIONS = undefined as GameVersion[]; + + beforeEach(() => { + GAME_VERSIONS = [ + { id: "1.0.0-alpha.1", version: parseVersion("1.0.0-alpha.1"), isRelease: false, isSnapshot: true, isAlpha: true, isBeta: false }, + { id: "1.0.0", version: parseVersion("1.0.0"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "1.1.0-beta.2", version: parseVersion("1.1.0-beta.2"), isRelease: false, isSnapshot: true, isAlpha: false, isBeta: true }, + { id: "1.1.0", version: parseVersion("1.1.0"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "1.2.0-beta.2", version: parseVersion("1.2.0-beta.2"), isRelease: false, isSnapshot: true, isAlpha: false, isBeta: true }, + { id: "1.2.0", version: parseVersion("1.2.0"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "1.2.1", version: parseVersion("1.2.1"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "1.2.2", version: parseVersion("1.2.2"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "1.2.3", version: parseVersion("1.2.3"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + { id: "2.0.0", version: parseVersion("2.0.0"), isRelease: true, isSnapshot: false, isAlpha: false, isBeta: false }, + ]; + }); + + describe("NONE", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.NONE)).not.toBe(GAME_VERSIONS); + }); + + test("an unfiltered array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.NONE)).toEqual(GAME_VERSIONS); + }); + }); + + describe("RELEASES", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.RELEASES)).not.toBe(GAME_VERSIONS); + }); + + test("only releases are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.RELEASES); + + expect(versions).toHaveLength(7); + expect(versions.every(x => x.isRelease)).toBe(true); + }); + }); + + describe("ALPHAS", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.ALPHAS)).not.toBe(GAME_VERSIONS); + }); + + test("only alphas are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.ALPHAS); + + expect(versions).toHaveLength(1); + expect(versions.every(x => x.isAlpha)).toBe(true); + }); + }); + + describe("BETAS", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.BETAS)).not.toBe(GAME_VERSIONS); + }); + + test("only betas are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.BETAS); + + expect(versions).toHaveLength(2); + expect(versions.every(x => x.isBeta)).toBe(true); + }); + }); + + describe("SNAPSHOTS", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.SNAPSHOTS)).not.toBe(GAME_VERSIONS); + }); + + test("only snapshots are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.SNAPSHOTS); + + expect(versions).toHaveLength(3); + expect(versions.every(x => x.isSnapshot)).toBe(true); + }); + }); + + describe("MIN_PATCH", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_PATCH)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the lowest patch value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_PATCH); + + expect(versions).toHaveLength(7); + expect(versions.every(x => x.version.patch === 0)).toBe(true); + }); + }); + + describe("MAX_PATCH", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_PATCH)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the highest patch value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_PATCH); + + expect(versions).toHaveLength(1); + expect(versions.every(x => x.version.patch === 3)).toBe(true); + }); + }); + + describe("MIN_MINOR", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_MINOR)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the lowest minor value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_MINOR); + + expect(versions).toHaveLength(3); + expect(versions.every(x => x.version.minor === 0)).toBe(true); + }); + }); + + describe("MAX_MINOR", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_MINOR)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the highest minor value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_MINOR); + + expect(versions).toHaveLength(5); + expect(versions.every(x => x.version.minor === 2)).toBe(true); + }); + }); + + describe("MIN_MAJOR", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_MAJOR)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the lowest major value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MIN_MAJOR); + + expect(versions).toHaveLength(9); + expect(versions.every(x => x.version.major === 1)).toBe(true); + }); + }); + + describe("MAX_MAJOR", () => { + test("a different array is returned", () => { + expect(GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_MAJOR)).not.toBe(GAME_VERSIONS); + }); + + test("only versions with the highest major value are returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.MAX_MAJOR); + + expect(versions).toHaveLength(1); + expect(versions.every(x => x.version.major === 2)).toBe(true); + }); + }); + + describe("RELEASES | MIN", () => { + test("the oldest version is returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.RELEASES | GameVersionFilter.MIN); + + expect(versions).toHaveLength(1); + expect(versions[0]).toMatchObject({ id: "1.0.0" }); + }); + }); + + describe("RELEASES | MAX", () => { + test("the latest version is returned", () => { + const versions = GameVersionFilter.filter(GAME_VERSIONS, GameVersionFilter.RELEASES | GameVersionFilter.MAX); + + expect(versions).toHaveLength(1); + expect(versions[0]).toMatchObject({ id: "2.0.0" }); + }); + }); + }); + + describe("parse", () => { + test("parses all its own formatted values", () => { + for (const value of GameVersionFilter.values()) { + expect(GameVersionFilter.parse(GameVersionFilter.format(value))).toBe(value); + } + }); + + test("parses all friendly names of its own values", () => { + for (const value of GameVersionFilter.values()) { + expect(GameVersionFilter.parse(GameVersionFilter.friendlyNameOf(value))).toBe(value); + } + }); + + test("parses all its own formatted values in lowercase", () => { + for (const value of GameVersionFilter.values()) { + expect(GameVersionFilter.parse(GameVersionFilter.format(value).toLowerCase())).toBe(value); + } + }); + + test("parses all its own formatted values in UPPERCASE", () => { + for (const value of GameVersionFilter.values()) { + expect(GameVersionFilter.parse(GameVersionFilter.format(value).toUpperCase())).toBe(value); + } + }); + }); +});