diff --git a/src/loaders/fabric/fabric-dependency.ts b/src/loaders/fabric/fabric-dependency.ts new file mode 100644 index 0000000..0d5efc1 --- /dev/null +++ b/src/loaders/fabric/fabric-dependency.ts @@ -0,0 +1,95 @@ +import { Dependency, createDependency } from "@/dependencies"; +import { PlatformType } from "@/platforms"; +import { $i } from "@/utils/collections"; +import { FabricDependencyType } from "./fabric-dependency-type"; +import { RawFabricMetadata } from "./raw-fabric-metadata"; + +/** + * Represents a single dependency for a Fabric mod project. + */ +export interface FabricDependency { + /** + * The identifier for the dependency. + */ + id: string; + + /** + * The version range for the dependency. + * + * Can be a single version or an array of version ranges. + */ + version: string | string[]; + + /** + * The type of the dependency. + */ + type: FabricDependencyType; +} + +/** + * Interface representing a list of dependencies for a Fabric mod project. + */ +export interface FabricDependencyList { + /** + * The key is a Mod ID of the dependency. + * + * The value is a string or array of strings declaring supported version ranges. + */ + [id: string]: string | string[] | undefined; +} + +/** + * A list of special dependencies that should be ignored. + */ +const IGNORED_DEPENDENCIES: readonly string[] = [ + "minecraft", + "java", + "fabricloader", +]; + +/** + * A map of aliases for special dependencies for different platforms. + */ +const DEPENDENCY_ALIASES: ReadonlyMap> = new Map([ + ["fabric", "fabric-api"], +].map(([k, v]) => + [k, typeof v === "string" ? $i(PlatformType.values()).map(x => [x, v] as const).toMap() : v], +)); + +/** + * Retrieves Fabric dependencies from the metadata. + * + * @param metadata - The raw Fabric metadata. + * + * @returns An array of Fabric dependencies. + */ +export function getFabricDependencies(metadata: RawFabricMetadata): FabricDependency[] { + return $i(FabricDependencyType.values()).flatMap(type => toFabricDependencyArray(metadata?.[type], type)).toArray(); +} + +/** + * Converts a {@link FabricDependencyList} to a proper array of Fabric dependencies. + * + * @param list - The list of fabric dependencies. + * @param type - The type of the dependencies in the list. + * + * @returns An array of Fabric dependencies. + */ +export function toFabricDependencyArray(list: FabricDependencyList, type: FabricDependencyType): FabricDependency[] { + return Object.entries(list || {}).map(([id, version]) => ({ id, version, type })); +} + +/** +* Converts {@link FabricDependency} to a {@link Dependency} object. +* +* @returns A Dependency object representing the given Fabric dependency, or `undefined` if the input is invalid.. +*/ +export function normalizeFabricDependency(dependency: FabricDependency): Dependency | undefined { + return createDependency({ + id: dependency?.id, + versions: dependency?.version, + type: FabricDependencyType.toDependencyType(dependency?.type || FabricDependencyType.DEPENDS), + ignore: IGNORED_DEPENDENCIES.includes(dependency?.id), + aliases: DEPENDENCY_ALIASES.get(dependency?.id), + }); +} diff --git a/tests/unit/loaders/fabric/fabric-dependency.spec.ts b/tests/unit/loaders/fabric/fabric-dependency.spec.ts new file mode 100644 index 0000000..6af2a72 --- /dev/null +++ b/tests/unit/loaders/fabric/fabric-dependency.spec.ts @@ -0,0 +1,83 @@ +import { DependencyType } from "@/dependencies/dependency-type"; +import { RawFabricMetadata } from "@/loaders/fabric/raw-fabric-metadata"; +import { FabricDependencyType } from "@/loaders/fabric/fabric-dependency-type"; +import { getFabricDependencies, normalizeFabricDependency, toFabricDependencyArray } from "@/loaders/fabric/fabric-dependency"; + +describe("getFabricDependencies", () => { + test("returns an array of dependencies specified in the given metadata", () => { + const metadata = { + schemaVersion: 1, + id: "example-mod", + version: "1.0.0", + + depends: { "depends-id": "1.0.0" }, + recommends: { "recommends-id": "2.0.0" }, + suggests: { "suggests-id": "3.0.0" }, + breaks: { "breaks-id": ["4.0.0", "5.0.0"] }, + conflicts: { + "conflicts-id-1": "6.0.0", + "conflicts-id-2": "7.0.0", + }, + } as RawFabricMetadata; + + const dependencies = getFabricDependencies(metadata); + + expect(dependencies).toEqual([ + { id: "depends-id", version: "1.0.0", type: FabricDependencyType.DEPENDS }, + { id: "recommends-id", version: "2.0.0", type: FabricDependencyType.RECOMMENDS }, + { id: "suggests-id", version: "3.0.0", type: FabricDependencyType.SUGGESTS }, + { id: "breaks-id", version: ["4.0.0", "5.0.0"], type: FabricDependencyType.BREAKS }, + { id: "conflicts-id-1", version: "6.0.0", type: FabricDependencyType.CONFLICTS }, + { id: "conflicts-id-2", version: "7.0.0", type: FabricDependencyType.CONFLICTS }, + ]); + }); + + test("returns an empty array if no dependencies were specified", () => { + expect(getFabricDependencies({} as RawFabricMetadata)).toEqual([]); + }); + + test("returns an empty array if metadata was null or undefined", () => { + expect(getFabricDependencies(null)).toEqual([]); + expect(getFabricDependencies(undefined)).toEqual([]); + }); +}); + +describe("toFabricDependencyArray", () => { + test("converts a dependency list to an array", () => { + const conflicting = { + "conflicts-id-1": "6.0.0", + "conflicts-id-2": ["7.0.0", "8.0.0"], + }; + + const dependencies = toFabricDependencyArray(conflicting, FabricDependencyType.CONFLICTS); + + expect(dependencies).toEqual([ + { id: "conflicts-id-1", version: "6.0.0", type: FabricDependencyType.CONFLICTS }, + { id: "conflicts-id-2", version: ["7.0.0", "8.0.0"], type: FabricDependencyType.CONFLICTS }, + ]); + }); + + test("returns an empty array if no dependencies were specified", () => { + expect(toFabricDependencyArray({}, FabricDependencyType.DEPENDS)).toEqual([]); + }); + + test("returns an empty array if dependency list was null or undefined", () => { + expect(toFabricDependencyArray(null, FabricDependencyType.DEPENDS)).toEqual([]); + expect(toFabricDependencyArray(undefined, FabricDependencyType.DEPENDS)).toEqual([]); + }); +}); + +describe("normalizeFabricDependency", () => { + test("converts Fabric dependency to a more abstract Dependency object", () => { + const fabricDependency = { id: "recommends-id", version: "2.0.0", type: FabricDependencyType.RECOMMENDS }; + + const dependency = normalizeFabricDependency(fabricDependency); + + expect(dependency).toMatchObject({ id: "recommends-id", versions: ["2.0.0"], type: DependencyType.RECOMMENDED }); + }); + + test("returns undefined if dependency was null or undefined", () => { + expect(normalizeFabricDependency(null)).toBeUndefined(); + expect(normalizeFabricDependency(undefined)).toBeUndefined(); + }); +});