From c06d1de4d4cb38b8fe459b0cc2e065aebd999daa Mon Sep 17 00:00:00 2001 From: Xicheng Guo Date: Thu, 1 Aug 2024 15:05:31 +0800 Subject: [PATCH] update yaml-check --- package-lock.json | 13 +- package.json | 3 +- .../skipBounds-bound-leading-space.yaml | 4 - .../skipBounds-resolution-leading-space.yaml | 4 - scripts/__tests__/yaml-check.test.ts | 48 ++--- scripts/yaml-check.ts | 166 ++++++------------ 6 files changed, 87 insertions(+), 151 deletions(-) delete mode 100644 scripts/__tests__/examples/skipBounds-bound-leading-space.yaml delete mode 100644 scripts/__tests__/examples/skipBounds-resolution-leading-space.yaml diff --git a/package-lock.json b/package-lock.json index 0a861b4d..4a8f7616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "ts-jest": "^29.2.3", "ts-node": "^10.9.2", "vitepress": "^1.2.2", - "vitepress-plugin-mermaid": "^2.0.16" + "vitepress-plugin-mermaid": "^2.0.16", + "zod": "^3.23.8" } }, "node_modules/@algolia/autocomplete-core": { @@ -7389,6 +7390,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 9a345af3..965c7cb3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "ts-jest": "^29.2.3", "ts-node": "^10.9.2", "vitepress": "^1.2.2", - "vitepress-plugin-mermaid": "^2.0.16" + "vitepress-plugin-mermaid": "^2.0.16", + "zod": "^3.23.8" }, "dependencies": { "skip": "file:" diff --git a/scripts/__tests__/examples/skipBounds-bound-leading-space.yaml b/scripts/__tests__/examples/skipBounds-bound-leading-space.yaml deleted file mode 100644 index 95776d06..00000000 --- a/scripts/__tests__/examples/skipBounds-bound-leading-space.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- packageName: com.test.skip - skipBounds: - - resolution: 1920,1080 - bound: " 123,456,789,1011" \ No newline at end of file diff --git a/scripts/__tests__/examples/skipBounds-resolution-leading-space.yaml b/scripts/__tests__/examples/skipBounds-resolution-leading-space.yaml deleted file mode 100644 index a68e5ff6..00000000 --- a/scripts/__tests__/examples/skipBounds-resolution-leading-space.yaml +++ /dev/null @@ -1,4 +0,0 @@ -- packageName: com.test.skip - skipBounds: - - resolution: " 1920,1080" - bound: 123,456,789,321 \ No newline at end of file diff --git a/scripts/__tests__/yaml-check.test.ts b/scripts/__tests__/yaml-check.test.ts index dda80759..f9e0ace7 100644 --- a/scripts/__tests__/yaml-check.test.ts +++ b/scripts/__tests__/yaml-check.test.ts @@ -1,6 +1,6 @@ import { yamlCheck } from "../yaml-check"; -describe("YAML Check", () => { +describe("YAML Check 2", () => { it("正确格式的 YAML", () => { const detail = yamlCheck("scripts/__tests__/examples/correct-yaml.yaml"); expect(detail).toEqual([ @@ -27,7 +27,9 @@ describe("YAML Check", () => { ]); }); it("缺少 packageName", () => { - expect(() => yamlCheck("scripts/__tests__/examples/missing-packageName.yaml")).toThrow("packageName is required"); + expect(() => yamlCheck("scripts/__tests__/examples/missing-packageName.yaml")).toThrow( + "packageName must be a string" + ); }); it("packageName 不是字符串", () => { expect(() => yamlCheck("scripts/__tests__/examples/packageName-not-string.yaml")).toThrow( @@ -36,7 +38,7 @@ describe("YAML Check", () => { }); it("packageName 为空", () => { expect(() => yamlCheck("scripts/__tests__/examples/packageName-empty.yaml")).toThrow( - "packageName must not be empty" + "packageName must not have leading or trailing whitespace" ); }); it("packageName 有前导空格", () => { @@ -50,19 +52,23 @@ describe("YAML Check", () => { ); }); it("出现未知的键", () => { - expect(() => yamlCheck("scripts/__tests__/examples/unknown-keys.yaml")).toThrow("Unknown keys: unknownKey"); + expect(() => yamlCheck("scripts/__tests__/examples/unknown-keys.yaml")).toThrow( + "Unrecognized key(s) in object: 'unknownKey'" + ); }); it("skipIds 不是数组", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipIds-not-array.yaml")).toThrow("skipIds must be an array"); }); it("skipIds 缺少 id", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipIds-missing-id.yaml")).toThrow("id is required"); + expect(() => yamlCheck("scripts/__tests__/examples/skipIds-missing-id.yaml")).toThrow("id must be a string"); }); it("skipIds.id 不是字符串", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipIds-id-not-string.yaml")).toThrow("id must be a string"); }); it("skipIds.id 为空", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipIds-id-empty.yaml")).toThrow("id must not be empty"); + expect(() => yamlCheck("scripts/__tests__/examples/skipIds-id-empty.yaml")).toThrow( + "id must not have leading or trailing whitespace" + ); }); it("skipIds.id 有前导空格", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipIds-id-leading-space.yaml")).toThrow( @@ -80,7 +86,9 @@ describe("YAML Check", () => { ); }); it("skipTexts.text 为空", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipTexts-text-empty.yaml")).toThrow("text must not be empty"); + expect(() => yamlCheck("scripts/__tests__/examples/skipTexts-text-empty.yaml")).toThrow( + "text must not have leading or trailing whitespace" + ); }); it("skipTexts.text 有前导空格", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipTexts-text-leading-space.yaml")).toThrow( @@ -99,7 +107,7 @@ describe("YAML Check", () => { }); it("skipTexts 出现未知的键", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipTexts-unknown-keys.yaml")).toThrow( - "Unknown keys: unknownKey" + "Unrecognized key(s) in object: 'unknownKey'" ); }); it("skipBounds 不是数组", () => { @@ -108,11 +116,13 @@ describe("YAML Check", () => { ); }); it("skipBounds 缺少 bound", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-missing-bound.yaml")).toThrow("bound is required"); + expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-missing-bound.yaml")).toThrow( + "bound must be a string" + ); }); it("skipBounds 缺少 resolution", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-missing-resolution.yaml")).toThrow( - "resolution is required" + "resolution must be a string" ); }); it("skipBounds 的 bound 不是字符串", () => { @@ -122,17 +132,12 @@ describe("YAML Check", () => { }); it("skipBounds 的 bound 为空", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-bound-empty.yaml")).toThrow( - "bound must not be empty" - ); - }); - it("skipBounds 的 bound 有前导空格", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-bound-leading-space.yaml")).toThrow( - "bound must not have leading or trailing whitespace" + "bound must have four numeric values" ); }); it("skipBounds 用 , 分隔后长度不为 4", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-bound-not-4-parts.yaml")).toThrow( - "bound must have four comma-separated values" + "bound must have four numeric values" ); }); it("skipBounds 的 bound 分隔后不是数字", () => { @@ -147,17 +152,12 @@ describe("YAML Check", () => { }); it("skipBounds 的 resolution 为空", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-resolution-empty.yaml")).toThrow( - "resolution must not be empty" - ); - }); - it("skipBounds 的 resolution 有前导空格", () => { - expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-resolution-leading-space.yaml")).toThrow( - "resolution must not have leading or trailing whitespace" + "resolution must have two numeric values" ); }); it("skipBounds 的 resolution 用 , 分隔后长度不为 2", () => { expect(() => yamlCheck("scripts/__tests__/examples/skipBounds-resolution-not-2-parts.yaml")).toThrow( - "resolution must have two comma-separated values" + "resolution must have two numeric values" ); }); it("skipBounds 的 resolution 分隔后不是数字", () => { diff --git a/scripts/yaml-check.ts b/scripts/yaml-check.ts index c4a8fce0..2f53093b 100644 --- a/scripts/yaml-check.ts +++ b/scripts/yaml-check.ts @@ -1,125 +1,57 @@ import fs from "node:fs"; import yaml from "js-yaml"; - -interface SkipConfig { - packageName: string; - skipIds?: SkipIds[]; - skipTexts?: SkipTexts[]; - skipBounds?: SkipBounds[]; -} - -interface SkipIds { - id: string; -} - -interface SkipTexts { - text?: string; - length?: number; -} - -interface SkipBounds { - bound: string; - resolution: string; -} - +import { z } from "zod"; + +const skipIdsSchema = z.object({ + id: z + .string({ message: "id must be a string" }) + .refine((val) => val.trim() === val, { message: "id must not have leading or trailing whitespace" }), +}); + +const skipTextsSchema = z + .object({ + text: z + .string({ message: "text must be a string" }) + .optional() + .refine((val) => val?.trim() === val, { message: "text must not have leading or trailing whitespace" }), + length: z + .number({ message: "length must be a number" }) + .nonnegative({ message: "length must be non-negative" }) + .optional(), + }) + .strict(); + +const skipBoundsSchema = z.object({ + bound: z.string({ message: "bound must be a string" }).refine((val) => { + const rect = val.split(","); + return rect.length === 4 && rect.every((v) => !isNaN(parseFloat(v))); + }, "bound must have four numeric values"), + resolution: z.string({ message: "resolution must be a string" }).refine((val) => { + const size = val.split(","); + return size.length === 2 && size.every((v) => !isNaN(parseFloat(v))); + }, "resolution must have two numeric values"), +}); + +const skipConfigSchema = z + .object({ + packageName: z + .string({ message: "packageName must be a string" }) + .refine((val) => val.trim() === val, { message: "packageName must not have leading or trailing whitespace" }), + + skipIds: z.array(skipIdsSchema, { message: "skipIds must be an array" }).optional(), + skipTexts: z.array(skipTextsSchema, { message: "skipTexts must be an array" }).optional(), + skipBounds: z.array(skipBoundsSchema, { message: "skipBounds must be an array" }).optional(), + }) + .strict() + .refine((data) => data.skipIds || data.skipTexts || data.skipBounds, "skipIds, skipTexts, or skipBounds is required"); + +// 读取并验证 yaml 文件 export function yamlCheck(yamlPath: string) { try { - const doc = yaml.load(fs.readFileSync(yamlPath, "utf8")) as SkipConfig[]; - doc.forEach((config) => checkConfig(config)); - return doc; + const doc = yaml.load(fs.readFileSync(yamlPath, "utf8")) as any[]; + const parsedDoc = doc.map((config) => skipConfigSchema.parse(config)); + return parsedDoc; } catch (e) { throw e; } } - -function checkConfig(skipConfig: SkipConfig) { - if (!skipConfig.packageName) throw new Error("packageName is required"); - - const packageName = skipConfig.packageName; - if (typeof packageName !== "string") throw new Error("packageName must be a string"); - if (packageName.trim().length === 0) throw new Error("packageName must not be empty"); - if (packageName.trim() !== packageName) throw new Error("packageName must not have leading or trailing whitespace"); - - if (skipConfig.skipIds) checkSkipIds(skipConfig.skipIds); - - if (skipConfig.skipTexts) checkSkipTexts(skipConfig.skipTexts); - - if (skipConfig.skipBounds) checkSkipBounds(skipConfig.skipBounds); - - if (!(skipConfig.skipIds || skipConfig.skipTexts || skipConfig.skipBounds)) { - throw new Error("skipIds, skipTexts, or skipBounds is required"); - } - - const keys = ["skipIds", "skipTexts", "skipBounds", "packageName"]; - const extraKeys = Object.keys(skipConfig).filter((key) => !keys.includes(key)); - if (extraKeys.length > 0) throw new Error(`Unknown keys: ${extraKeys.join(", ")}`); -} - -function checkSkipIds(skipIds: SkipIds[]) { - if (!Array.isArray(skipIds)) throw new Error("skipIds must be an array"); - - skipIds.forEach((skipId) => { - if (!skipId.id) throw new Error("id is required"); - - const id = skipId.id; - if (typeof id !== "string") throw new Error("id must be a string"); - if (id.trim().length === 0) throw new Error("id must not be empty"); - if (id.trim() !== id) throw new Error("id must not have leading or trailing whitespace"); - }); -} - -function checkSkipTexts(skipTexts: SkipTexts[]) { - if (!Array.isArray(skipTexts)) throw new Error("skipTexts must be an array"); - - skipTexts.forEach((skipText) => { - if (skipText.text) { - const text = skipText.text; - if (typeof text !== "string") throw new Error("text must be a string"); - if (text.trim().length === 0) throw new Error("text must not be empty"); - if (text.trim() !== text) throw new Error("text must not have leading or trailing whitespace"); - } - - if (skipText.length) { - const length = skipText.length; - if (typeof length !== "number") throw new Error("length must be a number"); - if (length < 0) throw new Error("length must be non-negative"); - } - - const keys = ["text", "length"]; - const extraKeys = Object.keys(skipText).filter((key) => !keys.includes(key)); - if (extraKeys.length > 0) throw new Error(`Unknown keys: ${extraKeys.join(", ")}`); - }); -} - -function checkSkipBounds(skipBounds: SkipBounds[]) { - if (!Array.isArray(skipBounds)) throw new Error("skipBounds must be an array"); - - skipBounds.forEach((skipBound) => { - if (!skipBound.bound) throw new Error("bound is required"); - if (!skipBound.resolution) throw new Error("resolution is required"); - - const bound = skipBound.bound; - if (typeof bound !== "string") throw new Error("bound must be a string"); - if (bound.trim().length === 0) throw new Error("bound must not be empty"); - if (bound.trim() !== bound) throw new Error("bound must not have leading or trailing whitespace"); - - const rect = bound.split(","); - if (rect.length !== 4) throw new Error("bound must have four comma-separated values"); - rect.forEach((value) => { - const number = parseFloat(value); - if (isNaN(number)) throw new Error("bound must have four numeric values"); - }); - - const resolution = skipBound.resolution; - if (typeof resolution !== "string") throw new Error("resolution must be a string"); - if (resolution.trim().length === 0) throw new Error("resolution must not be empty"); - if (resolution.trim() !== resolution) throw new Error("resolution must not have leading or trailing whitespace"); - - const size = resolution.split(","); - if (size.length !== 2) throw new Error("resolution must have two comma-separated values"); - size.forEach((value) => { - const number = parseFloat(value); - if (isNaN(number)) throw new Error("resolution must have two numeric values"); - }); - }); -}