From b7fa4fab6766fe76aa2e1c28c3baa595b7476a3d Mon Sep 17 00:00:00 2001
From: Kir_Antipov <kp.antipov@gmail.com>
Date: Fri, 25 Nov 2022 01:37:53 +0300
Subject: [PATCH] `QuiltModMetadata` refactoring

---
 README.md                                     |  30 +--
 src/metadata/quilt/quilt-mod-config.ts        |  66 +++++++
 .../quilt/quilt-mod-metadata-reader.ts        |  15 +-
 src/metadata/quilt/quilt-mod-metadata.ts      | 176 +++++++++++-------
 test/content/quilt/quilt.mod.json             |  31 ++-
 .../metadata/mod-metadata-reader.test.ts      |   6 +
 6 files changed, 211 insertions(+), 113 deletions(-)
 create mode 100644 src/metadata/quilt/quilt-mod-config.ts

diff --git a/README.md b/README.md
index ec3061b..2c8e0e7 100644
--- a/README.md
+++ b/README.md
@@ -201,7 +201,7 @@ Can be automatically retrieved from the config file of your mod:
 
 - `quilt.mod.json` (Quilt)
 
-  - `mc-publish` field *(recommended)*:
+  - Custom `mc-publish` field:
       ```json
       {
         // ...
@@ -211,16 +211,6 @@ Can be automatically retrieved from the config file of your mod:
       }
       ```
 
-  - `projects` field:
-      ```json
-      {
-        // ...
-        "projects": {
-          "modrinth": "AANobbMI"
-        },
-      }
-      ```
-
 #### modrinth-token
 
 A valid token for the Modrinth API. It's required if you want to publish your assets to Modrinth.
@@ -307,7 +297,7 @@ Can be automatically retrieved from the config file of your mod:
 
 - `quilt.mod.json` (Quilt)
 
-  - `mc-publish` field *(recommended)*:
+  - Custom `mc-publish` field:
       ```json
       {
         // ...
@@ -317,16 +307,6 @@ Can be automatically retrieved from the config file of your mod:
       }
       ```
 
-  - `projects` field:
-      ```json
-      {
-        // ...
-        "projects": {
-          "curseforge": 394468
-        },
-      }
-      ```
-
 #### curseforge-token
 
 A valid token for the CurseForge API. It's required if you want to publish your assets to CurseForge.
@@ -613,7 +593,7 @@ Can be automatically retrieved from the config file of your mod:
     "required-dependency",
     {
       "id": "optional-dependency",
-      "version": "0.1.0",
+      "versions": "0.1.0",
       "optional": true
     }
   ],
@@ -631,11 +611,11 @@ Can be automatically retrieved from the config file of your mod:
   "breaks": [
     {
       "id": "incompatible-dependency",
-      "version": "*"
+      "versions": "*"
     },
     {
       "id": "conflicting-dependency",
-      "version": "*",
+      "versions": "*",
       "unless": "some-mod-that-fixes-conflict"
     }
   ],
diff --git a/src/metadata/quilt/quilt-mod-config.ts b/src/metadata/quilt/quilt-mod-config.ts
new file mode 100644
index 0000000..f0d3692
--- /dev/null
+++ b/src/metadata/quilt/quilt-mod-config.ts
@@ -0,0 +1,66 @@
+type Plugin = string | { adapter?: string, value: string };
+
+type Entrypoint = Plugin;
+
+type License = string | {
+    name: string;
+    id: string;
+    url: string;
+    description?: string;
+};
+
+type Dependency = string | {
+    id: string;
+    version?: string;
+    versions?: string | string[];
+    reason?: string;
+    optional?: boolean;
+    unless?: Dependency | Dependency[];
+};
+
+// https://github.com/QuiltMC/rfcs/blob/main/specification/0002-quilt.mod.json.md
+type QuiltModConfig = {
+    schema_version: 1;
+
+    quilt_loader: {
+        group: string;
+        id: string;
+        provides?: Dependency[];
+        version: string;
+        entrypoints?: Record<string, Entrypoint | Entrypoint[]>;
+        plugins?: Plugin[];
+        jars?: string[];
+        language_adapters?: Record<string, string>;
+        depends?: Dependency[];
+        breaks?: Dependency[];
+        load_type?: "always" | "if_possible" | "if_required";
+        repositories?: string[];
+        intermediate_mappings?: string;
+        metadata?: Record<string, unknown>;
+        name?: string;
+        description?: string;
+        contributors?: Record<string, string>;
+        contact?: {
+            email?: string;
+            homepage?: string;
+            issues?: string;
+            sources?: string;
+            [key: string]: string;
+        };
+        license?: License | License[];
+        icon?: string | Record<string, string>;
+
+    };
+
+    mixin?: string | string[];
+    access_widener?: string | string[];
+    minecraft?: {
+        environment?: "client" | "dedicated_server" | "*";
+    };
+} & Record<string, any>;
+
+namespace QuiltModConfig {
+    export const FILENAME = "quilt.mod.json";
+}
+
+export default QuiltModConfig;
diff --git a/src/metadata/quilt/quilt-mod-metadata-reader.ts b/src/metadata/quilt/quilt-mod-metadata-reader.ts
index caac99a..b934a56 100644
--- a/src/metadata/quilt/quilt-mod-metadata-reader.ts
+++ b/src/metadata/quilt/quilt-mod-metadata-reader.ts
@@ -1,17 +1,20 @@
-import ModMetadata from "../../metadata/mod-metadata";
-import ZippedModMetadataReader from "../../metadata/zipped-mod-metadata-reader";
+import ModMetadata from "../mod-metadata";
+import ZippedModMetadataReader from "../zipped-mod-metadata-reader";
+import QuiltModConfig from "./quilt-mod-config";
 import QuiltModMetadata from "./quilt-mod-metadata";
 
-export default class QuiltModMetadataReader extends ZippedModMetadataReader {
+class QuiltModMetadataReader extends ZippedModMetadataReader<QuiltModConfig> {
     constructor() {
-        super("quilt.mod.json");
+        super(QuiltModConfig.FILENAME);
     }
 
-    protected loadConfig(buffer: Buffer): Record<string, unknown> {
+    protected loadConfig(buffer: Buffer): QuiltModConfig {
         return JSON.parse(buffer.toString("utf8"));
     }
 
-    protected createMetadataFromConfig(config: Record<string, unknown>): ModMetadata {
+    protected createMetadataFromConfig(config: QuiltModConfig): ModMetadata {
         return new QuiltModMetadata(config);
     }
 }
+
+export default QuiltModMetadataReader;
diff --git a/src/metadata/quilt/quilt-mod-metadata.ts b/src/metadata/quilt/quilt-mod-metadata.ts
index 92dd098..a806756 100644
--- a/src/metadata/quilt/quilt-mod-metadata.ts
+++ b/src/metadata/quilt/quilt-mod-metadata.ts
@@ -1,24 +1,41 @@
 import action from "../../../package.json";
-import Dependency from "../../metadata/dependency";
-import DependencyKind from "../../metadata/dependency-kind";
-import ModConfig from "../../metadata/mod-config";
-import ModConfigDependency from "../../metadata/mod-config-dependency";
+import Dependency from "../dependency";
+import DependencyKind from "../dependency-kind";
 import PublisherTarget from "../../publishing/publisher-target";
+import ModMetadata from "../mod-metadata";
+import QuiltModConfig from "./quilt-mod-config";
 
-function extractId(id?: string): string | null {
+type Aliases = Map<PublisherTarget, string>;
+
+type QuiltDependency = QuiltModConfig["quilt_loader"]["breaks"][number];
+
+type ExtendedQuiltDependency = QuiltDependency & {
+    embedded?: boolean;
+    incompatible?: boolean;
+};
+
+function getDependencies(config: QuiltModConfig): Dependency[] {
+    const root = config.quilt_loader;
+    return getExtendedDependencyEntries(root.depends)
+        .concat(getExtendedDependencyEntries(root.provides, x => x.embedded = true))
+        .concat(getExtendedDependencyEntries(root.breaks, x => x.incompatible = true))
+        .map(parseDependency)
+        .filter((x, i, self) => self.findIndex(y => x.id === y.id && x.kind === y.kind) === i);
+}
+
+function parseId(id?: string): string | null {
     if (!id) {
         return id ?? null;
     }
 
     const separatorIndex = id.indexOf(":");
-    if (separatorIndex !== -1) {
-        id = id.substring(separatorIndex + 1);
+    if (separatorIndex === -1) {
+        return id;
     }
-
-    return id;
+    return id.substring(separatorIndex + 1);
 }
 
-function getDependencyEntries(container: any, transformer?: (x: any) => void): any[] {
+function getExtendedDependencyEntries(container: QuiltDependency[], transformer?: (x: ExtendedQuiltDependency) => void): ExtendedQuiltDependency[] {
     if (!Array.isArray(container)) {
         return [];
     }
@@ -30,68 +47,101 @@ function getDependencyEntries(container: any, transformer?: (x: any) => void): a
     return container;
 }
 
-const ignoredByDefault = ["minecraft", "java", "quilt_loader"];
-const aliases = new Map([
+function parseDependency(body: ExtendedQuiltDependency): Dependency {
+    const id = parseId(typeof body === "string" ? body : String(body.id ?? ""));
+    const ignoredByDefault = isDependencyIgnoredByDefault(id);
+    const defaultAliases = getDefaultDependencyAliases(id);
+
+    if (typeof body === "string") {
+        return Dependency.create({ id, ignore: ignoredByDefault, aliases: defaultAliases });
+    }
+
+    const version = body.version ?? (Array.isArray(body.versions) ? body.versions.join(" || ") : body.versions || "*");
+    const kind = (
+        body.incompatible && body.unless && DependencyKind.Conflicts ||
+        body.incompatible && DependencyKind.Breaks ||
+        body.embedded && DependencyKind.Includes ||
+        body.optional && DependencyKind.Recommends ||
+        DependencyKind.Depends
+    );
+    const ignore = body[action.name]?.ignore ?? ignoredByDefault;
+
+    const aliases = new Map([...(defaultAliases || [])]);
+    for (const target of PublisherTarget.getValues()) {
+        const targetName = PublisherTarget.toString(target).toLowerCase();
+        const alias = body[action.name]?.[targetName];
+        if (alias) {
+            aliases.set(target, String(alias));
+        }
+    }
+
+    return Dependency.create({ id, version, kind, ignore, aliases });
+}
+
+const ignoredByDefault = [
+    "minecraft",
+    "java",
+    "quilt_loader",
+];
+function isDependencyIgnoredByDefault(id: string): boolean {
+    return ignoredByDefault.includes(id);
+}
+
+const defaultAliases = new Map<string, string | Aliases>([
     ["fabric", "fabric-api"],
     ["quilted_fabric_api", "qsl"],
 ]);
-function createDependency(body: any): Dependency {
-    const id = extractId(typeof body === "string" ? body : String(body.id ?? ""));
-    const ignore = ignoredByDefault.includes(id);
-    if (id.startsWith("quilted_") || id.startsWith("quilt_")) {
-        aliases.set(id, "qsl");
+function getDefaultDependencyAliases(id: string): Aliases | null {
+    if (id.startsWith("quilted_")) {
+        id = "quilted_fabric_api";
     }
 
-    if (typeof body === "string") {
-        const dependencyAliases = aliases.has(id) ? new Map(PublisherTarget.getValues().map(x => [x, aliases.get(id)])) : null;
-        return Dependency.create({ id, ignore, aliases: dependencyAliases });
+    if (!defaultAliases.has(id)) {
+        return null;
     }
 
-    const dependencyMetadata = {
-        ignore,
-        ...body,
-        id,
-        version: body.version ?? String(Array.isArray(body.versions) ? body.versions[0] : body.versions || "*"),
-        kind: (
-            body.incompatible && body.unless && DependencyKind.Conflicts ||
-            body.incompatible && DependencyKind.Breaks ||
-            body.embedded && DependencyKind.Includes ||
-            body.optional && DependencyKind.Recommends ||
-            DependencyKind.Depends
-        )
-    };
-    if (aliases.has(id)) {
-        if (!dependencyMetadata[action.name]) {
-            dependencyMetadata[action.name] = {};
-        }
-        for (const target of PublisherTarget.getValues()) {
-            const targetName = PublisherTarget.toString(target).toLowerCase();
-            if (typeof dependencyMetadata[action.name][targetName] !== "string") {
-                dependencyMetadata[action.name][targetName] = aliases.get(id);
-            }
-        }
+    const aliases = defaultAliases.get(id);
+    if (typeof aliases !== "string") {
+        return new Map([...aliases]);
     }
-    return new ModConfigDependency(dependencyMetadata);
+
+    return new Map(PublisherTarget.getValues().map(x => [x, aliases]));
 }
 
-export default class QuiltModMetadata extends ModConfig {
-    public readonly id: string;
-    public readonly name: string;
-    public readonly version: string;
-    public readonly loaders: string[];
-    public readonly dependencies: Dependency[];
+function getProjects(config: QuiltModConfig): Map<PublisherTarget, string> {
+    const projects = new Map();
+    for (const target of PublisherTarget.getValues()) {
+        const targetName = PublisherTarget.toString(target).toLowerCase();
+        const projectId = config[action.name]?.[targetName] ?? config.projects?.[targetName];
 
-    constructor(config: Record<string, unknown>) {
-        super(config);
-        const root = <Record<string, unknown>>this.config.quilt_loader ?? {};
-        this.id = String(root.id ?? "");
-        this.name = String(root.name ?? this.id);
-        this.version = String(root.version ?? "*");
-        this.loaders = ["quilt"];
-        this.dependencies = getDependencyEntries(root.depends)
-            .concat(getDependencyEntries(root.provides, x => x.embedded = true))
-            .concat(getDependencyEntries(root.breaks, x => x.incompatible = true))
-            .map(createDependency)
-            .filter((x, i, self) => self.findIndex(y => x.id === y.id && x.kind === y.kind) === i);
+        if (projectId) {
+            projects.set(target, String(projectId));
+        }
     }
-}
\ No newline at end of file
+    return projects;
+}
+
+class QuiltModMetadata implements ModMetadata {
+    readonly id: string;
+    readonly name: string;
+    readonly version: string;
+    readonly loaders: string[];
+    readonly dependencies: Dependency[];
+
+    private readonly _projects: Map<PublisherTarget, string>;
+
+    constructor(config: QuiltModConfig) {
+        this.id = String(config.quilt_loader.id ?? "");
+        this.name = String(config.quilt_loader.name ?? this.id);
+        this.version = String(config.quilt_loader.version ?? "*");
+        this.loaders = ["quilt"];
+        this.dependencies = getDependencies(config);
+        this._projects = getProjects(config);
+    }
+
+    getProjectId(project: PublisherTarget): string | undefined {
+        return this._projects.get(project);
+    }
+}
+
+export default QuiltModMetadata;
diff --git a/test/content/quilt/quilt.mod.json b/test/content/quilt/quilt.mod.json
index 1b3f1e1..0d930ff 100644
--- a/test/content/quilt/quilt.mod.json
+++ b/test/content/quilt/quilt.mod.json
@@ -27,35 +27,32 @@
     "depends": [
       {
         "id": "quilt_loader",
-        "version": ">=0.11.3"
+        "versions": ">=0.11.3"
       },
       {
         "id": "quilt_base",
-        "version": ">=0.40.0"
+        "versions": ">=0.40.0"
       },
       {
         "id": "minecraft",
-        "version": "1.17.x"
+        "versions": [
+          "1.17",
+          "1.17.1"
+        ]
       },
       {
         "id": "java",
-        "version": ">=16"
+        "versions": ">=16"
       },
       {
         "id": "recommended-mod",
-        "version": "0.2.0",
+        "versions": "0.2.0",
         "optional": true,
         "mc-publish": {
+          "curseforge": 42,
+          "github": "v0.2.0",
           "modrinth": "AAAA",
           "ignore": true
-        },
-        "projects": {
-          "curseforge": 42
-        },
-        "custom": {
-          "projects": {
-            "github": "v0.2.0"
-          }
         }
       }
     ],
@@ -66,22 +63,18 @@
       "breaking-mod",
       {
         "id": "conflicting:conflicting-mod",
-        "version": "<0.40.0",
+        "versions": "<0.40.0",
         "unless": "fix-conflicting-mod"
       }
     ]
   },
   "mc-publish": {
+    "github": "mc1.18-0.4.0-alpha5",
     "modrinth": "AANobbMI"
   },
   "projects": {
     "curseforge": 394468
   },
-  "custom": {
-    "projects": {
-      "github": "mc1.18-0.4.0-alpha5"
-    }
-  },
   "mixins": [
     "example-mod.mixins.json"
   ],
diff --git a/test/unit-tests/metadata/mod-metadata-reader.test.ts b/test/unit-tests/metadata/mod-metadata-reader.test.ts
index 56add9d..060baac 100644
--- a/test/unit-tests/metadata/mod-metadata-reader.test.ts
+++ b/test/unit-tests/metadata/mod-metadata-reader.test.ts
@@ -233,6 +233,12 @@ describe("ModMetadataReader.readMetadata", () => {
             }
         });
 
+        test("version array is supported", async () => {
+            const metadata = (await ModMetadataReader.readMetadata("example-mod.quilt.jar"))!;
+            const minecraft = metadata.dependencies.find(x => x.id === "minecraft");
+            expect(minecraft.version).toStrictEqual("1.17 || 1.17.1");
+        });
+
         test("custom metadata can be attached to dependency entry", async () => {
             const metadata = (await ModMetadataReader.readMetadata("example-mod.quilt.jar"))!;
             const recommended = metadata.dependencies.find(x => x.id === "recommended-mod")!;