From d1af6778de758e4925cae4ea0a3b98932991393b Mon Sep 17 00:00:00 2001
From: RahulGautamSingh <rahultesnik@gmail.com>
Date: Thu, 29 Aug 2024 11:37:39 +0530
Subject: [PATCH] refactor: data validation using schema (#30797)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/data/monorepo.json             |  1 +
 test/validate-schemas.spec.ts      | 64 ++++++++++++++++++++++++++++++
 tools/schemas/monorepo-schema.json | 53 +++++++++++++++++++++++++
 tools/schemas/schema.ts            | 14 +++++++
 4 files changed, 132 insertions(+)
 create mode 100644 test/validate-schemas.spec.ts
 create mode 100644 tools/schemas/monorepo-schema.json
 create mode 100644 tools/schemas/schema.ts

diff --git a/lib/data/monorepo.json b/lib/data/monorepo.json
index 6705901889..a23fdaa4f8 100644
--- a/lib/data/monorepo.json
+++ b/lib/data/monorepo.json
@@ -1,4 +1,5 @@
 {
+  "$schema": "../../tools/schemas/monorepo-schema.json",
   "repoGroups": {
     "accounts": "https://github.com/accounts-js/accounts",
     "acot": "https://github.com/acot-a11y/acot",
diff --git a/test/validate-schemas.spec.ts b/test/validate-schemas.spec.ts
new file mode 100644
index 0000000000..e7f424570b
--- /dev/null
+++ b/test/validate-schemas.spec.ts
@@ -0,0 +1,64 @@
+import fs from 'fs-extra';
+import upath from 'upath';
+import { Json } from '../lib/util/schema-utils';
+import { capitalize } from '../tools/docs/utils';
+import * as Schemas from '../tools/schemas/schema';
+
+describe('validate-schemas', () => {
+  it('validate json files in lib/data against their schemas', async () => {
+    const dataFileDir = 'lib/data';
+    const schemaDir = 'tools/schemas';
+    const schemasAndJsonFiles: {
+      schemaName: keyof typeof Schemas;
+      dataFileName: string;
+    }[] = [];
+
+    const schemaFiles = (await fs.readdir(schemaDir)).filter(
+      (file) => upath.extname(file) === '.json',
+    );
+
+    for (const schemaFile of schemaFiles) {
+      const correspondingDatFileName = schemaFile.replace('-schema', '');
+      const schemaName = `${schemaFile
+        .replace('.json', '')
+        .split('-')
+        .map(capitalize)
+        .join('')}` as keyof typeof Schemas;
+      schemasAndJsonFiles.push({
+        schemaName,
+        dataFileName: correspondingDatFileName,
+      });
+    }
+
+    const settledPromises = await Promise.allSettled(
+      schemasAndJsonFiles.map(async ({ schemaName, dataFileName }) => {
+        const data = Json.parse(
+          await fs.readFile(upath.join(dataFileDir, dataFileName), 'utf8'),
+        );
+
+        // validate json data against schema: using parse here instead of safeParse so we throw
+        // this leads to a better error message when the assertion fails
+        // eslint-disable-next-line import/namespace
+        Schemas[schemaName].parse(data);
+      }),
+    );
+
+    for (let i = 0; i < settledPromises.length; i++) {
+      const { schemaName, dataFileName } = schemasAndJsonFiles[i];
+      const res = {
+        schemaName,
+        dataFileName,
+        settledPromise: { reason: undefined, ...settledPromises[i] },
+      };
+
+      expect(res).toMatchObject({
+        schemaName,
+        dataFileName,
+        settledPromise: {
+          status: 'fulfilled',
+          reason: undefined,
+        },
+      });
+    }
+  });
+});
diff --git a/tools/schemas/monorepo-schema.json b/tools/schemas/monorepo-schema.json
new file mode 100644
index 0000000000..6bc1d6e7a9
--- /dev/null
+++ b/tools/schemas/monorepo-schema.json
@@ -0,0 +1,53 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "properties": {
+    "repoGroups": {
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9. -]+$": {
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            {
+              "type": "array",
+              "items": { "type": "string", "format": "uri" }
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "orgGroups": {
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9. -]+$": {
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            {
+              "type": "array",
+              "items": { "type": "string", "format": "uri" }
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "patternGroups": {
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9. -]+$": {
+          "oneOf": [
+            { "type": "string", "pattern": "^/.+/$" },
+            {
+              "type": "array",
+              "items": { "type": "string", "pattern": "^/.+/$" }
+            }
+          ]
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+  "required": ["repoGroups", "orgGroups", "patternGroups"],
+  "additionalProperties": false
+}
diff --git a/tools/schemas/schema.ts b/tools/schemas/schema.ts
new file mode 100644
index 0000000000..b5361a2472
--- /dev/null
+++ b/tools/schemas/schema.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+const UrlSchema = z.record(
+  z.string(),
+  z.union([z.string(), z.array(z.string())]),
+);
+
+const MonorepoSchema = z.object({
+  repoGroups: UrlSchema,
+  orgGroups: UrlSchema,
+  patternGroups: UrlSchema,
+});
+
+export { MonorepoSchema };
-- 
GitLab