diff --git a/Flaw.spec.ts b/Flaw.spec.ts new file mode 100644 index 0000000..57485ba --- /dev/null +++ b/Flaw.spec.ts @@ -0,0 +1,150 @@ +import z from "zod" +import * as gracely from "./index" + +type Date = z.infer +namespace Date { + export const validator = z.string().regex(/(^\d{4}-(0[13-9]|1[012])-([012]\d|30|31)$)|^\d{4}-02-([012]\d)$/) + export function is(value: any): value is Date { + return validator.safeParse(value).success + } + export const flaw = gracely.Flaw.generate("Date string", Date.validator) +} + +type Example = z.infer + +namespace Example { + export function is(value: any): value is Example { + return validator.safeParse(value).success + } + export const validator = z.object({ + anInteger: z.number().int(), + anOptional: z.number().optional(), + date: Date.validator, + email: z.string().email(), + starter: z.string().startsWith("left_"), + test: z + .object({ + deeper: z.boolean(), + }) + .optional(), + }) + export const flaw = gracely.Flaw.generate("model.Example", validator) +} + +describe("Flaw tests", () => { + it("Flaw self tests", () => { + expect(gracely.Flaw.is({})).toBeFalsy() + expect(gracely.Flaw.flaw({})).toEqual({ + flaws: [ + { + condition: "Required", + property: "type", + type: "string", + }, + ], + type: "gracely.Flaw", + }) + const veryFlawedFlaw = { + type: "example", + flaws: [{ type: "example", flaws: [{ epyt: "reversed", flaws: true }] }], + } + expect(gracely.Flaw.is(veryFlawedFlaw)).toBeFalsy() + expect(gracely.Flaw.flaw(veryFlawedFlaw)).toEqual({ + flaws: [ + { + condition: "Required", + property: "flaws.0.flaws.0.type", + type: "string", + }, + { + condition: "Expected array, received boolean", + property: "flaws.0.flaws.0.flaws", + type: "array", + }, + ], + type: "gracely.Flaw", + }) + }) + it("Flaw.generate tests", () => { + const example: Example = { + anInteger: 1, + date: "2022-01-31", + email: "is@valid.mail", + starter: "left_right", + } + const modified: Record = { ...example } + expect(Example.is(example)).toBeTruthy() + expect(Example.flaw(example)).toEqual({ + type: "model.Example", + flaws: [], + }) + modified.email = "isn't valid email" + modified.test = "test" + expect(Example.is(modified)).toBeFalsy() + expect(Example.flaw(modified)).toEqual({ + type: "model.Example", + flaws: [ + { + condition: "Invalid email", + property: "email", + type: "email", + }, + { + condition: "Expected object, received string", + property: "test", + type: "object", + }, + ], + }) + modified.email = "is@valid.mail" + modified.test = { deeper: true } + modified.starter = "right_left" + expect(Example.is(modified)).toBeFalsy() + expect(Example.flaw(modified)).toEqual({ + type: "model.Example", + flaws: [ + { + condition: 'Invalid input: must start with "left_"', + property: "starter", + type: '{"startsWith":"left_"}', + }, + ], + }) + modified.test.deeper = "true" + expect(Example.is(modified)).toBeFalsy() + expect(Example.flaw(modified)).toEqual({ + type: "model.Example", + flaws: [ + { + condition: 'Invalid input: must start with "left_"', + property: "starter", + type: '{"startsWith":"left_"}', + }, + { + condition: "Expected boolean, received string", + property: "test.deeper", + type: "boolean", + }, + ], + }) + modified.starter = "left_something" + modified.test.deeper = 1 + modified.date = "2022-02-30" + expect(Example.is(modified)).toBeFalsy() + expect(Example.flaw(modified)).toEqual({ + type: "model.Example", + flaws: [ + { + condition: "Invalid", + property: "date", + type: "regex", + }, + { + condition: "Expected boolean, received number", + property: "test.deeper", + type: "boolean", + }, + ], + }) + }) +}) diff --git a/Flaw.ts b/Flaw.ts index fe1ce48..54fc2e1 100644 --- a/Flaw.ts +++ b/Flaw.ts @@ -1,18 +1,44 @@ +import z from "zod" + export interface Flaw { property?: string type: string flaws?: Flaw[] condition?: string } - export namespace Flaw { + export const validator: z.ZodType = z.lazy(() => + z.object({ + property: z.string().optional(), + type: z.string(), + flaws: z.array(validator).optional(), // recursive type makes type inference impossible + condition: z.string().optional(), + }) + ) export function is(value: any | Flaw): value is Flaw { - return ( - typeof value == "object" && - (value.property == undefined || typeof value.property == "string") && - typeof value.type == "string" && - (value.flaws == undefined || (Array.isArray(value.flaws) && value.flaws.every(Flaw.is))) && - (value.condition == undefined || typeof value.condition == "string") - ) + return validator.safeParse(value).success + } + export const flaw = generate("gracely.Flaw", validator) + export function generate(type: string, validator: z.ZodType): (value: any) => Flaw { + return (value: any) => { + const parsed = validator.safeParse(value) + return { + type, + flaws: !("error" in parsed) + ? [] + : parsed.error.errors.map(error => ({ + property: error.path.join("."), + type: + "expected" in error && typeof error.expected == "string" + ? error.expected + : !("validation" in error) + ? "unknown" + : typeof error.validation == "string" + ? error.validation + : JSON.stringify(error.validation), + condition: error.message, + })), + } + } } } diff --git a/package-lock.json b/package-lock.json index 91ec73f..57dd210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "gracely", "version": "2.0.3", "license": "MIT", + "dependencies": { + "zod": "^3.18.0" + }, "devDependencies": { "@types/jest": "^28.1.6", "@typescript-eslint/eslint-plugin": "5.31.0", @@ -6151,6 +6154,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz", + "integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -10733,6 +10744,11 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "zod": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz", + "integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==" } } } diff --git a/package.json b/package.json index 3f63148..1d9003a 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,8 @@ "rimraf": "^3.0.2", "ts-jest": "^28.0.7", "typescript": "^4.7.4" + }, + "dependencies": { + "zod": "^3.18.0" } }