diff --git a/scripts/accelerator.ts b/scripts/accelerator.ts index b030460..6b02645 100644 --- a/scripts/accelerator.ts +++ b/scripts/accelerator.ts @@ -2,6 +2,7 @@ import { z as zod } from "zod"; import { ZodAcceleratorContent } from "./content"; import { ZodAcceleratorParser } from "./parser"; import { ZodAcceleratorError } from "./error"; +import { zodSchemaIsAsync } from "./utils/zodSchemaIsAsync"; declare module "zod" { interface ZodType { @@ -30,9 +31,12 @@ export abstract class ZodAccelerator { zac.addContext({ zodSchema, }); + const isAsync = zodSchemaIsAsync(zodSchema); + const parseMethod = isAsync ? "safeParseAsync" : "safeParse"; + const mayBeAwait = isAsync ? "await" : ""; zac.addContent(` - let $output = $this.zodSchema.accelerator.safeParse($input); + let $output = ${mayBeAwait} $this.zodSchema.accelerator.${parseMethod}($input); if($output.success === false){ $output.error.message = $output.error.message.replace(".", \`$path.\`); diff --git a/scripts/accelerators/effects.ts b/scripts/accelerators/effects.ts index d5182a2..009e7c0 100644 --- a/scripts/accelerators/effects.ts +++ b/scripts/accelerators/effects.ts @@ -1,6 +1,7 @@ import * as zod from "zod"; import { ZodAccelerator } from "../accelerator"; import type { ZodAcceleratorContent } from "../content"; +import { zodSchemaIsAsync } from ".."; @ZodAccelerator.autoInstance export class ZodEffectAccelerator extends ZodAccelerator { @@ -10,6 +11,7 @@ export class ZodEffectAccelerator extends ZodAccelerator { public makeAcceleratorContent(zodSchema: zod.ZodEffects, zac: ZodAcceleratorContent) { const def = zodSchema._def; + const async = zodSchemaIsAsync(zodSchema); zac.addContext({ transform: def.effect.type === "transform" ? def.effect.transform : undefined, @@ -19,7 +21,7 @@ export class ZodEffectAccelerator extends ZodAccelerator { }); zac.addContent( - def.effect.type !== "preprocess" || ZodEffectAccelerator.contentPart.preprocess(def.effect.transform.constructor.name === "AsyncFunction"), + def.effect.type !== "preprocess" || ZodEffectAccelerator.contentPart.preprocess(async), [ ZodAccelerator.findAcceleratorContent(def.schema), { @@ -28,8 +30,8 @@ export class ZodEffectAccelerator extends ZodAccelerator { output: "$input", }, ], - def.effect.type !== "transform" || ZodEffectAccelerator.contentPart.transform(def.effect.transform.constructor.name === "AsyncFunction"), - def.effect.type !== "refinement" || ZodEffectAccelerator.contentPart.refinement(def.effect.refinement.constructor.name === "AsyncFunction"), + def.effect.type !== "transform" || ZodEffectAccelerator.contentPart.transform(async), + def.effect.type !== "refinement" || ZodEffectAccelerator.contentPart.refinement(async), ); return zac; diff --git a/scripts/accelerators/type.test.ts b/scripts/accelerators/type.test.ts new file mode 100644 index 0000000..e9ce7c9 --- /dev/null +++ b/scripts/accelerators/type.test.ts @@ -0,0 +1,27 @@ + +import * as zod from "zod"; +import { ZodAccelerator } from ".."; +import { ZodAcceleratorError } from "@scripts/error"; + +describe("type", () => { + it("create parser for missing Map", () => { + const schema = zod.map(zod.string(), zod.string()); + const accelerateSchema = ZodAccelerator.build(schema); + const data = new Map(); + data.set("toto", "tata"); + + expect(accelerateSchema.parse(data)).toStrictEqual(schema.parse(data)); + + data.set("tata", 2); + + try { + accelerateSchema.parse(data); + throw new Error(); + } catch (error: any) { + const err: ZodAcceleratorError = error; + expect(err).instanceOf(ZodAcceleratorError); + expect(schema.safeParse(data).success).toBe(false); + expect(err.message).toBe(". : ZodSchema Fail parse."); + } + }); +}); diff --git a/scripts/accelerators/type.ts b/scripts/accelerators/type.ts new file mode 100644 index 0000000..883a3a6 --- /dev/null +++ b/scripts/accelerators/type.ts @@ -0,0 +1,38 @@ +import type { ZodAcceleratorContent } from "@scripts/content"; +import type { ZodType } from "zod"; +import { ZodAccelerator } from "../accelerator"; +import { zodSchemaIsAsync } from ".."; + +@ZodAccelerator.autoInstance +export class ZodTypeAccelerator extends ZodAccelerator { + public get support(): any { + return ZodAccelerator.zod.ZodType; + } + + public makeAcceleratorContent(zodSchema: ZodType, zac: ZodAcceleratorContent) { + const isAsync = zodSchemaIsAsync(zodSchema); + const parseMethod = isAsync ? "safeParseAsync" : "safeParse"; + const mayBeAwait = isAsync ? "await" : ""; + + zac.addContent({ + content: ` + const $output = ${mayBeAwait} $this.zodSchema.${parseMethod}($input); + + if($output.success === false){ + $output.error.message = $output.error.message.replace(".", \`$path.\`); + return { + success: false, + error: new ZodAcceleratorError(\`$path\`, "ZodSchema Fail parse.") + }; + } + + $input = $output.data; + `, + ctx: { + zodSchema, + }, + }); + + return zac; + } +} diff --git a/scripts/index.ts b/scripts/index.ts index fd37cc0..6d612fd 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -30,8 +30,14 @@ import "./accelerators/bigInt"; import "./accelerators/effects"; import "./accelerators/lazy"; +// must be import at last +import "./accelerators/type"; + export * from "./accelerator"; export * from "./error"; export * from "./parser"; +export * from "./utils/zodSchemaIsAsync"; +export * from "./utils/types"; + export { ZodAccelerator as default } from "./accelerator"; diff --git a/scripts/utils/zodSchemaIsAsync.test.ts b/scripts/utils/zodSchemaIsAsync.test.ts new file mode 100644 index 0000000..95e09d4 --- /dev/null +++ b/scripts/utils/zodSchemaIsAsync.test.ts @@ -0,0 +1,400 @@ +import * as zod from "zod"; +import { zodSchemaIsAsync } from "./zodSchemaIsAsync"; + +describe("zodSchemaIsAsync", () => { + it("array", () => { + expect( + zodSchemaIsAsync( + zod.string().array(), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.string().transform(async() => { + await Promise.resolve(); + }) + .array(), + ), + ).toBe(true); + }); + + it("catch", () => { + expect( + zodSchemaIsAsync( + zod.string().catch(""), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.string().transform(async() => { + await Promise.resolve(); + }) + .catch(undefined), + ), + ).toBe(true); + }); + + it("default", () => { + expect( + zodSchemaIsAsync( + zod.string().default(""), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.string().transform(async() => { + await Promise.resolve(); + }) + .default(""), + ), + ).toBe(true); + }); + + it("effect", async() => { + expect( + zodSchemaIsAsync( + zod.preprocess(() => 12, zod.number()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.string().transform(async() => { + await Promise.resolve(); + }), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.string().refine(async() => { + await Promise.resolve(); + }), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.string().superRefine(async() => { + await Promise.resolve(); + }), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.string() + .transform(async() => { + await Promise.resolve(); + }) + .superRefine(() => void undefined), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.string().superRefine((val: any) => { + val.tot = val; + expect(val.ttt).toBe(val); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, new-cap + expect(new val()).toBe(val); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(val()).toBe(val); + + return Promise.resolve(); + }), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.string().superRefine((val: any) => { + throw new Error(); + }), + ), + ).toBe(false); + + const result = zodSchemaIsAsync( + zod.string().superRefine((val: any) => Promise.reject(new Error())), + ); + + await new Promise((res) => void setTimeout(res, 10)); + + expect(result).toBe(true); + }); + + it("intersection", () => { + expect( + zodSchemaIsAsync( + zod.string().and(zod.number()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.string().transform(async() => { + await Promise.resolve(); + }) + .and(zod.number()), + ), + ).toBe(true); + }); + + it("lazy", () => { + expect( + zodSchemaIsAsync( + zod.lazy(() => zod.string().transform(() => 12)), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.lazy(() => zod.string().transform(async() => { + await Promise.resolve(); + })), + ), + ).toBe(true); + + interface Category { + name: string; + subcategories: Category[]; + test?: string; + } + + const zodRecursiveSchema: zod.ZodType = zod.object({ + name: zod.string(), + subcategories: zod.lazy(() => zodRecursiveSchema.array()), + test: zod.string().transform(async() => { + await Promise.resolve(); + return undefined; + }), + }); + + expect( + zodSchemaIsAsync( + zodRecursiveSchema, + ), + ).toBe(true); + }); + + it("map", () => { + expect( + zodSchemaIsAsync( + zod.map(zod.string(), zod.string()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.map( + zod.string().transform(async() => { + await Promise.resolve(); + }), + zod.string(), + ), + ), + ).toBe(true); + + expect( + zodSchemaIsAsync( + zod.map( + zod.string(), + zod.string().transform(async() => { + await Promise.resolve(); + }), + ), + ), + ).toBe(true); + }); + + it("nullable", () => { + expect( + zodSchemaIsAsync( + zod + .string() + .transform(() => 12) + .nullable(), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod + .string() + .transform(async() => { + await Promise.resolve(); + }) + .nullable(), + ), + ).toBe(true); + }); + + it("object", () => { + expect( + zodSchemaIsAsync( + zod.object({ + test: zod.string(), + test1: zod.string().transform(() => 12), + }), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.object({ + test: zod.string().transform(async() => { + await Promise.resolve(); + }), + test1: zod.string().transform(() => 12), + }), + ), + ).toBe(true); + }); + + it("optional", () => { + expect( + zodSchemaIsAsync( + zod + .string() + .transform(() => 12) + .optional(), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod + .string() + .transform(async() => { + await Promise.resolve(); + }) + .optional(), + ), + ).toBe(true); + }); + + it("pipeline", () => { + expect( + zodSchemaIsAsync(zod.pipeline(zod.string(), zod.string())), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.pipeline( + zod.string(), + zod.string().transform(async() => { + await Promise.resolve(); + }), + ), + ), + ).toBe(true); + }); + + it("promise", () => { + expect( + zodSchemaIsAsync( + zod.promise(zod.string()), + ), + ).toBe(true); + }); + + it("readonly", () => { + expect( + zodSchemaIsAsync( + zod.object({ + test: zod.string(), + test1: zod.string().transform(() => 12), + }).readonly(), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.object({ + test: zod.string().transform(async() => { + await Promise.resolve(); + }), + test1: zod.string().transform(() => 12), + }).readonly(), + ), + ).toBe(true); + }); + + it("record", () => { + expect( + zodSchemaIsAsync( + zod.record(zod.string(), zod.string()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.record(zod.string(), zod.string().transform(async() => { + await Promise.resolve(); + })), + ), + ).toBe(true); + }); + + it("set", () => { + expect( + zodSchemaIsAsync( + zod.set(zod.string()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.set( + zod.string().transform(async() => { + await Promise.resolve(); + }), + ), + ), + ).toBe(true); + }); + + it("tuple", () => { + expect( + zodSchemaIsAsync( + zod.tuple([zod.string(), zod.string()]).rest(zod.string()), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.tuple([ + zod.string(), + zod.string().transform(async() => { + await Promise.resolve(); + }), + ]).rest(zod.string()), + ), + ).toBe(true); + }); + + it("union", () => { + expect( + zodSchemaIsAsync( + zod.union([ + zod.string(), + zod.number(), + ]), + ), + ).toBe(false); + + expect( + zodSchemaIsAsync( + zod.union([ + zod.string(), + zod.number().transform(async() => { + await Promise.resolve(); + }), + ]), + ), + ).toBe(true); + }); +}); diff --git a/scripts/utils/zodSchemaIsAsync.ts b/scripts/utils/zodSchemaIsAsync.ts new file mode 100644 index 0000000..9661b66 --- /dev/null +++ b/scripts/utils/zodSchemaIsAsync.ts @@ -0,0 +1,125 @@ +import { ZodEffects, ZodObject, type ZodType, ZodArray, ZodCatch, ZodDefault, ZodIntersection, ZodLazy, ZodOptional, ZodReadonly, ZodRecord, ZodTuple, ZodUnion, ZodPipeline, ZodNullable, ZodPromise, ZodSet, ZodMap } from "zod"; + +export function zodSchemaIsAsync(zodSchema: unknown, lazyMap = new Set()): boolean { + if (lazyMap.has(zodSchema)) { + return false; + } + + lazyMap.add(zodSchema); + + if (zodSchema instanceof ZodArray) { + return zodSchemaIsAsync(zodSchema._def.type, lazyMap); + } else if (zodSchema instanceof ZodCatch) { + return zodSchemaIsAsync(zodSchema._def.innerType, lazyMap); + } else if (zodSchema instanceof ZodDefault) { + return zodSchemaIsAsync(zodSchema._def.innerType, lazyMap); + } else if (zodSchema instanceof ZodEffects) { + if ( + ( + zodSchema._def.effect.type === "transform" + || zodSchema._def.effect.type === "preprocess" + ) + && zodSchema._def.effect.transform.constructor.name === "AsyncFunction" + ) { + return true; + } else if ( + zodSchema._def.effect.type === "refinement" + && zodSchema._def.effect.refinement.constructor.name === "AsyncFunction" + ) { + return true; + } else if (zodSchemaIsAsync(zodSchema._def.schema, lazyMap)) { + return true; + } else { + const effectFunction = zodSchema._def.effect.type === "refinement" + ? zodSchema._def.effect.refinement + : zodSchema._def.effect.transform; + + function anyFunction() { + return void undefined; + } + + const impossibleErrorProxy: any = new Proxy( + anyFunction, + { + get() { + return impossibleErrorProxy; + }, + construct() { + anyFunction(); + return impossibleErrorProxy; + }, + set() { + return true; + }, + apply() { + return impossibleErrorProxy; + }, + }, + ); + + try { + const result = effectFunction( + impossibleErrorProxy, + { + addIssue: () => void undefined, + path: [], + }, + ); + + if (result instanceof Promise) { + result.catch(() => void undefined); + } + + return result instanceof Promise; + } catch { + return false; + } + } + } else if ( + zodSchema instanceof ZodIntersection + && ( + zodSchemaIsAsync(zodSchema._def.left, lazyMap) + || zodSchemaIsAsync(zodSchema._def.right, lazyMap) + ) + ) { + return true; + } else if (zodSchema instanceof ZodLazy) { + return zodSchemaIsAsync(zodSchema._def.getter(), lazyMap); + } else if (zodSchema instanceof ZodMap) { + return zodSchemaIsAsync(zodSchema._def.keyType, lazyMap) + || zodSchemaIsAsync(zodSchema._def.valueType, lazyMap); + } else if (zodSchema instanceof ZodNullable) { + return zodSchemaIsAsync(zodSchema._def.innerType, lazyMap); + } else if (zodSchema instanceof ZodObject) { + return !Object.values(zodSchema._def.shape() as ZodType) + .every((value) => !zodSchemaIsAsync(value, lazyMap)); + } else if (zodSchema instanceof ZodOptional) { + return zodSchemaIsAsync(zodSchema._def.innerType, lazyMap); + } else if (zodSchema instanceof ZodPipeline) { + return zodSchemaIsAsync(zodSchema._def.in, lazyMap) + || zodSchemaIsAsync(zodSchema._def.out, lazyMap); + } else if (zodSchema instanceof ZodPromise) { + return true; + } else if (zodSchema instanceof ZodReadonly) { + return zodSchemaIsAsync(zodSchema._def.innerType, lazyMap); + } else if ( + zodSchema instanceof ZodRecord + && ( + zodSchemaIsAsync(zodSchema._def.keyType, lazyMap) + || zodSchemaIsAsync(zodSchema._def.valueType, lazyMap) + ) + ) { + return true; + } else if (zodSchema instanceof ZodSet) { + return zodSchemaIsAsync(zodSchema._def.valueType, lazyMap); + } else if (zodSchema instanceof ZodTuple) { + return !(zodSchema._def.items as ZodType[]) + .every((value) => !zodSchemaIsAsync(value, lazyMap)) + || zodSchemaIsAsync(zodSchema._def.rest, lazyMap); + } else if (zodSchema instanceof ZodUnion) { + return !(zodSchema._def.options as ZodType[]) + .every((value) => !zodSchemaIsAsync(value, lazyMap)); + } + + return false; +} diff --git a/vitest.config.js b/vitest.config.js index 193d45d..cdf1b2c 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -12,7 +12,7 @@ export default defineConfig({ "text", "json", "html", "json-summary" ], reportsDirectory: "coverage", - include: ["scripts/accelerators/**/**.ts"], + include: ["scripts/accelerators/**/**.ts", "scripts/utils/**/**.ts"], } }, plugins: [tsconfigPaths()],