From 26ea23967070a4b1d5a9319f6df4297d4ee8c109 Mon Sep 17 00:00:00 2001
From: Kir_Antipov <kp.antipov@gmail.com>
Date: Thu, 2 Mar 2023 16:51:34 +0000
Subject: [PATCH] Implemented logic to work with Fabric dependencies

---
 src/loaders/fabric/fabric-dependency.ts       | 95 +++++++++++++++++++
 .../loaders/fabric/fabric-dependency.spec.ts  | 83 ++++++++++++++++
 2 files changed, 178 insertions(+)
 create mode 100644 src/loaders/fabric/fabric-dependency.ts
 create mode 100644 tests/unit/loaders/fabric/fabric-dependency.spec.ts

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<string, ReadonlyMap<PlatformType, string>> = 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();
+    });
+});