diff --git a/src/default.ts b/src/default.ts deleted file mode 100644 index 952d795..0000000 --- a/src/default.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ValueOf } from "type-fest"; - -import { - SzType, - SzDefaultOrNullable, - SzLiteral, - SzArray, - SzObject, - SzOptional, - SzDefault, - SzNullable, -} from "./types"; - -type SzDefaultableType = - | (SzType & SzDefaultOrNullable) - | SzLiteral - | SzArray - | SzObject<{ - [key: string]: SzDefaultableType | (SzType & SzOptional); - }>; - -type NonOptionalKeysOf> = Exclude< - { - [Key in keyof T]: T extends Record - ? T[Key] extends SzOptional - ? never - : Key - : never; - }[keyof T], - undefined ->; -type SzDefaultValue = T extends SzDefault - ? Value - : T extends SzNullable - ? null - : T extends { type: "literal" } - ? T["value"] - : T extends SzArray - ? [] - : T extends SzObject - ? ValueOf extends SzDefaultOrNullable | SzOptional - ? { - [Key in NonOptionalKeysOf]: SzDefaultValue; - } - : never - : never; - -export function getDefaultValue( - shape: T -): SzDefaultValue; -export function getDefaultValue( - shape: T -): unknown { - if ("defaultValue" in shape) { - return shape.defaultValue; - } - if ("isNullable" in shape && shape.isNullable) { - return null; - } - - switch (shape.type) { - case "literal": - return shape.value; - - case "array": - return []; - - case "object": - return Object.fromEntries( - (Object.entries(shape.properties) as [any, any][]) - .filter( - ([, value]) => - !value.isOptional && getDefaultValue(value) !== undefined - ) - .map(([key, value]) => [key, getDefaultValue(value)]) - ); - } -} diff --git a/src/dezerialize.ts b/src/dezerialize.ts new file mode 100644 index 0000000..dd3b695 --- /dev/null +++ b/src/dezerialize.ts @@ -0,0 +1,291 @@ +import { z } from "zod"; +import { + SzOptional, + SzNullable, + SzDefault, + SzLiteral, + SzArray, + SzObject, + SzUnion, + SzDiscriminatedUnion, + SzIntersection, + SzTuple, + SzRecord, + SzMap, + SzSet, + SzFunction, + SzEnum, + SzPromise, + SzType, + SzString, + SzNumber, + SzBoolean, + SzBigInt, + SzNaN, + SzDate, + SzAny, + SzNever, + SzNull, + SzUndefined, + SzUnknown, + SzVoid, +} from "./types"; +import { ZodTypes } from "./zod-types"; + +type DistributiveOmit = T extends any + ? Omit + : never; +type OmitKey = DistributiveOmit; + +// Types must match the exported dezerialize function's implementation +export type Dezerialize = + // Modifier types + T extends SzOptional + ? Dezerialize> extends infer I + ? I extends ZodTypes + ? z.ZodOptional + : never + : never + : T extends SzNullable + ? Dezerialize> extends infer I + ? I extends ZodTypes + ? z.ZodNullable + : never + : never + : T extends SzDefault + ? Dezerialize>> extends infer I + ? I extends ZodTypes + ? z.ZodDefault + : never + : never // Primitives + : T extends SzString + ? z.ZodString + : T extends SzNumber + ? z.ZodNumber + : T extends SzBoolean + ? z.ZodBoolean + : T extends SzBigInt + ? z.ZodBigInt + : T extends SzNaN + ? z.ZodNaN + : T extends SzDate + ? z.ZodDate + : T extends SzUndefined + ? z.ZodUndefined + : T extends SzNull + ? z.ZodNull + : T extends SzAny + ? z.ZodAny + : T extends SzUnknown + ? z.ZodUnknown + : T extends SzNever + ? z.ZodNever + : T extends SzVoid + ? z.ZodVoid + : T extends SzLiteral + ? z.ZodLiteral // List Collections + : T extends SzTuple + ? z.ZodTuple //DezerializeArray> + : T extends SzSet + ? z.ZodSet> + : T extends SzArray + ? z.ZodArray> // Key/Value Collections + : T extends SzObject + ? z.ZodObject<{ + [Property in keyof Properties]: Dezerialize; + }> + : T extends SzRecord + ? z.ZodRecord, Dezerialize> + : T extends SzMap + ? z.ZodMap, Dezerialize> // Enum + : T extends SzEnum + ? z.ZodEnum // Union/Intersection + : T extends SzUnion + ? z.ZodUnion + : T extends SzDiscriminatedUnion + ? z.ZodDiscriminatedUnion + : T extends SzIntersection + ? z.ZodIntersection, Dezerialize> // Specials + : T extends SzFunction + ? z.ZodFunction, Dezerialize> + : T extends SzPromise + ? z.ZodPromise> + : unknown; + +type DezerializersMap = { + [T in SzType["type"]]: (shape: Extract) => ZodTypes; //Dezerialize>; +}; +const dezerializers = { + number: (shape) => { + let n = z.number(); + if (shape.min !== undefined) { + n = shape.minInclusive ? n.min(shape.min) : n.gt(shape.min); + } + if (shape.max !== undefined) { + n = shape.maxInclusive ? n.max(shape.max) : n.lt(shape.max); + } + if (shape.multipleOf !== undefined) { + n = n.multipleOf(shape.multipleOf); + } + if (shape.int) { + n = n.int(); + } + if (shape.finite) { + n = n.finite(); + } + return n; + }, + string: (shape) => { + let s = z.string(); + if (shape.min !== undefined) { + s = s.min(shape.min); + } + if (shape.max !== undefined) { + s = s.max(shape.max); + } + if (shape.length !== undefined) { + s = s.length(shape.length); + } + if (shape.startsWith !== undefined) { + s = s.startsWith(shape.startsWith); + } + if (shape.endsWith !== undefined) { + s = s.endsWith(shape.endsWith); + } + if ("includes" in shape) { + s = s.includes(shape.includes, { position: shape.position }); + } + if ("regex" in shape) { + s = s.regex(new RegExp(shape.regex, shape.flags)); + } + if ("kind" in shape) { + if (shape.kind == "ip") { + s = s.ip({ version: shape.version }); + } else if (shape.kind == "datetime") { + s = s.datetime({ offset: shape.offset, precision: shape.precision }); + } else { + s = s[shape.kind](); + } + } + + return s; + }, + boolean: () => z.boolean(), + nan: () => z.nan(), + bigInt: (shape) => { + let i = z.bigint(); + if (shape.min !== undefined) { + i = shape.minInclusive ? i.min(shape.min) : i.gt(shape.min); + } + if (shape.max !== undefined) { + i = shape.maxInclusive ? i.max(shape.max) : i.lt(shape.max); + } + if (shape.multipleOf !== undefined) { + i = i.multipleOf(shape.multipleOf); + } + return i; + }, + date: (shape) => { + let i = z.date(); + if (shape.min !== undefined) { + i = i.min(new Date(shape.min)); + } + if (shape.max !== undefined) { + i = i.max(new Date(shape.max)); + } + return i; + }, + undefined: () => z.undefined(), + null: () => z.null(), + any: () => z.any(), + unknown: () => z.unknown(), + never: () => z.never(), + void: () => z.void(), + + literal: (shape) => z.literal(shape.value), + + tuple: ((shape: SzTuple) => { + let i = z.tuple(shape.items.map(dezerialize) as any); + if (shape.rest) { + i = i.rest(dezerialize(shape.rest) as any); + } + return i; + }) as any, + set: ((shape: SzSet) => { + let i = z.set(dezerialize(shape.value)); + if (shape.minSize !== undefined) { + i = i.min(shape.minSize); + } + if (shape.maxSize !== undefined) { + i = i.max(shape.maxSize); + } + return i; + }) as any, + array: ((shape: SzArray) => { + let i = z.array(dezerialize(shape.element)); + if (shape.minLength !== undefined) { + i = i.min(shape.minLength); + } + if (shape.maxLength !== undefined) { + i = i.max(shape.maxLength); + } + return i; + }) as any, + + object: ((shape: SzObject) => + z.object( + Object.fromEntries( + Object.entries(shape.properties).map(([key, value]) => [ + key, + dezerialize(value), + ]) + ) + )) as any, + record: ((shape: SzRecord) => + z.record(dezerialize(shape.key), dezerialize(shape.value))) as any, + map: ((shape: SzMap) => + z.map(dezerialize(shape.key), dezerialize(shape.value))) as any, + + enum: ((shape: SzEnum) => z.enum(shape.values)) as any, + + union: ((shape: SzUnion) => + z.union(shape.options.map(dezerialize) as any)) as any, + discriminatedUnion: ((shape: SzDiscriminatedUnion) => + z.discriminatedUnion( + shape.discriminator, + shape.options.map(dezerialize) as any + )) as any, + intersection: ((shape: SzIntersection) => + z.intersection(dezerialize(shape.left), dezerialize(shape.right))) as any, + + function: ((shape: SzFunction) => + z.function( + dezerialize(shape.args) as any, + dezerialize(shape.returns) + )) as any, + promise: ((shape: SzPromise) => z.promise(dezerialize(shape.value))) as any, +} satisfies DezerializersMap as DezerializersMap; + +// Must match the exported Dezerialize types +// export function dezerialize(_shape: T): Dezerialize; +export function dezerialize(shape: SzType): ZodTypes { + if ("isOptional" in shape) { + const { isOptional, ...rest } = shape; + const inner = dezerialize(rest); + return isOptional ? inner.optional() : inner; + } + + if ("isNullable" in shape) { + const { isNullable, ...rest } = shape; + const inner = dezerialize(rest); + return isNullable ? inner.nullable() : inner; + } + + if ("defaultValue" in shape) { + const { defaultValue, ...rest } = shape; + const inner = dezerialize(rest); + return inner.default(defaultValue); + } + + return dezerializers[shape.type](shape as any); +} diff --git a/src/index.test.ts b/src/index.test.ts index b9ed3f4..9197a27 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,509 +1,360 @@ import { expect, test } from "vitest"; import { z } from "zod"; -import { getDefaultValue, SzInfer, zerialize, mapTypesToViews } from "./"; +import { dezerialize, SzType, zerialize, Zerialize } from "./index"; -import { PRIMITIVES } from "./zerialize"; +const p = < + Schema extends z.ZodFirstPartySchemaTypes, + Shape extends SzType = Zerialize +>( + schema: Schema, + shape: Shape +): [Schema, Shape] => [schema, shape]; enum Fruits { Apple, Banana, } -const s = zerialize; test.each([ - [ - PRIMITIVES, - { - ZodAny: "any", - ZodBigInt: "bigInt", - ZodBoolean: "boolean", - ZodDate: "date", - ZodNaN: "nan", - ZodNever: "never", - ZodNull: "null", - ZodNumber: "number", - ZodString: "string", - ZodUndefined: "undefined", - ZodUnknown: "unknown", - ZodVoid: "void", - }, - ], - - [typeof mapTypesToViews, "function"], - - [getDefaultValue({ type: "null", isNullable: true }), null], - [getDefaultValue({ type: "literal", value: "abc" }), "abc"], - [getDefaultValue({ type: "array", element: { type: "string" } }), []], - [ - getDefaultValue({ - type: "object", - properties: { - name: { type: "literal", value: "Gregor" }, - }, - }), - { - name: "Gregor", - }, - ], - - [s(z.string().nullable()), { type: "string", isNullable: true }], - [s(z.boolean()), { type: "boolean" }], - [s(z.nan()), { type: "nan" }], - [s(z.null()), { type: "null" }], - [s(z.undefined()), { type: "undefined" }], - [s(z.any()), { type: "any" }], - [s(z.unknown()), { type: "unknown" }], - [s(z.never()), { type: "never" }], - [s(z.void()), { type: "void" }], - [s(z.nativeEnum(Fruits)), { type: "unknown" }], - [ - s(z.object({ name: z.string() }).brand<"Cat">()), - { - type: "object", - properties: { - name: { - type: "string", - }, + p(z.boolean(), { type: "boolean" }), + p(z.nan(), { type: "nan" }), + p(z.null(), { type: "null" }), + p(z.undefined(), { type: "undefined" }), + p(z.any(), { type: "any" }), + p(z.unknown(), { type: "unknown" }), + p(z.never(), { type: "never" }), + p(z.void(), { type: "void" }), + p(z.nativeEnum(Fruits), { type: "unknown" }), + p(z.object({ name: z.string() }).brand<"Cat">(), { + type: "object", + properties: { + name: { + type: "string", }, }, - ], - [s(z.number().catch(42)), { type: "number" }], - - [s(z.string()) satisfies { type: "string" }, { type: "string" }], - [s(z.number()) satisfies { type: "number" }, { type: "number" }], - - [s(z.string().regex(/sth/)), { type: "string", regex: "sth" }], - [ - s(z.string().min(3).max(7).regex(/sth/u)), - { type: "string", min: 3, max: 7, regex: "sth", flags: "u" }, - ], - - [ - s(z.string().length(10).startsWith("a").endsWith("z").includes("rst")), - { - type: "string", - length: 10, - startsWith: "a", - endsWith: "z", - includes: "rst", - }, - ], - - [ - s(z.string().includes("special word", { position: 5 })), - { type: "string", includes: "special word", position: 5 }, - ], - - [s(z.string().email()), { type: "string", kind: "email" }], - [s(z.string().url()), { type: "string", kind: "url" }], - [s(z.string().emoji()), { type: "string", kind: "emoji" }], - [s(z.string().uuid()), { type: "string", kind: "uuid" }], - [s(z.string().cuid()), { type: "string", kind: "cuid" }], - [s(z.string().cuid2()), { type: "string", kind: "cuid2" }], - [s(z.string().ulid()), { type: "string", kind: "ulid" }], - [s(z.string().ip()), { type: "string", kind: "ip" }], - [s(z.string().datetime()), { type: "string", kind: "datetime" }], - - [ - s(z.string().ip({ version: "v4" })), - { type: "string", kind: "ip", version: "v4" }, - ], - - [ - s(z.string().datetime({ offset: true, precision: 3 })), - { type: "string", kind: "datetime", offset: true, precision: 3 }, - ], - - [ - s(z.string().optional()) satisfies { - type: "string"; - isOptional: true; - }, - { type: "string", isOptional: true }, - ], - [ - s(z.string().default("foo")) satisfies { - type: "string"; - defaultValue: string; - }, - { type: "string", defaultValue: "foo" }, - ], - [ - s(z.string().optional().default("foo")) satisfies { - type: "string"; - isOptional: true; - defaultValue?: string | undefined; - }, - { type: "string", isOptional: true, defaultValue: "foo" }, - ], - [ - s(z.number().default(42)) satisfies { - type: "number"; - defaultValue: number; - }, - { type: "number", defaultValue: 42 }, - ], - - [ - s(z.number().min(23).max(42).multipleOf(5)), - { - type: "number", - min: 23, - max: 42, - multipleOf: 5, - minInclusive: true, - maxInclusive: true, - }, - ], - - [ - s(z.number().gt(14).lt(20)), - { - type: "number", - min: 14, - max: 20, - }, - ], - - [ - s(z.number().gte(14).lte(20)), - { - type: "number", - min: 14, - minInclusive: true, - max: 20, - maxInclusive: true, - }, - ], - - [ - s(z.number().positive()), - { - type: "number", - min: 0, - }, - ], - [ - s(z.number().negative()), - { - type: "number", - max: 0, - }, - ], - - [ - s(z.number().nonnegative()), - { - type: "number", - min: 0, - minInclusive: true, - }, - ], - - [ - s(z.number().nonpositive()), - { - type: "number", - max: 0, - maxInclusive: true, - }, - ], - - [ - s(z.number().safe()), - { - type: "number", - min: -9007199254740991, - max: 9007199254740991, - minInclusive: true, - maxInclusive: true, - }, - ], - - [ - s(z.number().int()), - { - type: "number", - int: true, - }, - ], - - [ - s(z.number().finite()), - { - type: "number", - finite: true, - }, - ], - - [ - s(z.bigint().min(23n).max(42n).multipleOf(5n)), - { - type: "bigInt", - min: 23n, - minInclusive: true, - max: 42n, - maxInclusive: true, - multipleOf: 5n, - }, - ], - - [ - s(z.bigint().gt(14n).lt(20n)), - { - type: "bigInt", - min: 14n, - max: 20n, - }, - ], - - [ - s(z.bigint().gte(14n).lte(20n)), - { - type: "bigInt", - min: 14n, - minInclusive: true, - max: 20n, - maxInclusive: true, - }, - ], - - [ - s(z.bigint().positive()), - { - type: "bigInt", - min: 0n, - }, - ], - [ - s(z.bigint().negative()), - { + }), + p(z.number().catch(42), { type: "number" }), + + p(z.string(), { type: "string" }), + + p(z.string().regex(/sth/), { type: "string", regex: "sth" }), + p(z.string().min(3).max(7).regex(/sth/u), { + type: "string", + min: 3, + max: 7, + regex: "sth", + flags: "u", + }), + + p(z.string().length(10).startsWith("a").endsWith("z").includes("rst"), { + type: "string", + length: 10, + startsWith: "a", + endsWith: "z", + includes: "rst", + }), + + p(z.string().includes("special word", { position: 5 }), { + type: "string", + includes: "special word", + position: 5, + }), + + p(z.string().email(), { type: "string", kind: "email" }), + p(z.string().url(), { type: "string", kind: "url" }), + p(z.string().emoji(), { type: "string", kind: "emoji" }), + p(z.string().uuid(), { type: "string", kind: "uuid" }), + p(z.string().cuid(), { type: "string", kind: "cuid" }), + p(z.string().cuid2(), { type: "string", kind: "cuid2" }), + p(z.string().ulid(), { type: "string", kind: "ulid" }), + p(z.string().ip(), { type: "string", kind: "ip" }), + p(z.string().datetime(), { type: "string", kind: "datetime" }), + + p(z.string().ip({ version: "v4" }), { + type: "string", + kind: "ip", + version: "v4", + }), + + p(z.string().datetime({ offset: true, precision: 3 }), { + type: "string", + kind: "datetime", + offset: true, + precision: 3, + }), + + p(z.number(), { type: "number" }), + + p(z.string().optional(), { type: "string", isOptional: true }), + p(z.string().nullable(), { type: "string", isNullable: true }), + + p(z.string().default("foo"), { type: "string", defaultValue: "foo" }), + p(z.string().optional().default("foo"), { + type: "string", + isOptional: true, + defaultValue: "foo", + }), + + p(z.number().default(42), { type: "number", defaultValue: 42 }), + + p(z.number().min(23).max(42).multipleOf(5), { + type: "number", + min: 23, + minInclusive: true, + max: 42, + maxInclusive: true, + multipleOf: 5, + }), + + p(z.number().gt(14).lt(20), { + type: "number", + min: 14, + max: 20, + }), + + p(z.number().gte(14).lte(20), { + type: "number", + min: 14, + minInclusive: true, + max: 20, + maxInclusive: true, + }), + + p(z.number().positive(), { + type: "number", + min: 0, + }), + + p(z.number().negative(), { + type: "number", + max: 0, + }), + + p(z.number().nonnegative(), { + type: "number", + min: 0, + minInclusive: true, + }), + + p(z.number().nonpositive(), { + type: "number", + max: 0, + maxInclusive: true, + }), + + p(z.number().safe(), { + type: "number", + min: -9007199254740991, + minInclusive: true, + max: 9007199254740991, + maxInclusive: true, + }), + + p(z.number().int(), { + type: "number", + int: true, + }), + + p(z.number().finite(), { + type: "number", + finite: true, + }), + + p(z.bigint().min(BigInt(23)).max(BigInt(42)).multipleOf(BigInt(5)), { + type: "bigInt", + min: BigInt(23), + minInclusive: true, + max: BigInt(42), + maxInclusive: true, + multipleOf: BigInt(5), + } as any), + + p(z.bigint().gt(14n).lt(20n), { + type: "bigInt", + min: 14n, + max: 20n, + }), + + p(z.bigint().gte(14n).lte(20n), { + type: "bigInt", + min: 14n, + minInclusive: true, + max: 20n, + maxInclusive: true, + }), + + p(z.bigint().positive(), { + type: "bigInt", + min: 0n, + }), + + p(z.bigint().negative(), { + type: "bigInt", + max: 0n, + }), + + p(z.bigint().nonnegative(), { + type: "bigInt", + min: 0n, + minInclusive: true, + }), + + p(z.bigint().nonpositive(), { + type: "bigInt", + max: 0n, + maxInclusive: true, + }), + + p(z.date().min(new Date("1999-01-01")).max(new Date("2001-12-31")), { + type: "date", + min: 915148800000, + max: 1009756800000, + }), + + p(z.object({ foo: z.string() }), { + type: "object", + properties: { foo: { type: "string" } }, + }), + + p(z.literal("Gregor"), { type: "literal", value: "Gregor" }), + + p(z.array(z.number()), { + type: "array", + element: { type: "number" }, + }), + + p(z.array(z.number()).min(3).max(10), { + type: "array", + element: { type: "number" }, + minLength: 3, + maxLength: 10, + }), + + p(z.array(z.number()).length(10), { + type: "array", + element: { type: "number" }, + minLength: 10, + maxLength: 10, + }), + + p(z.union([z.string(), z.number()]), { + type: "union", + options: [{ type: "string" }, { type: "number" }], + }), + + p(z.intersection(z.string(), z.number()), { + type: "intersection", + left: { type: "string" }, + right: { type: "number" }, + }), + + p(z.tuple([z.string(), z.number()]), { + type: "tuple", + items: [{ type: "string" }, { type: "number" }], + }), + + p(z.tuple([z.string(), z.number()]).rest(z.bigint()), { + type: "tuple", + items: [{ type: "string" }, { type: "number" }], + rest: { type: "bigInt", - max: 0n, }, - ], - - [ - s(z.bigint().nonnegative()), - { - type: "bigInt", - min: 0n, - minInclusive: true, - }, - ], - - [ - s(z.bigint().nonpositive()), - { - type: "bigInt", - max: 0n, - maxInclusive: true, - }, - ], - - [ - s(z.date().min(new Date("1999-01-01")).max(new Date("2001-12-31"))), - { type: "date", min: 915148800000, max: 1009756800000 }, - ], - - [ - s(z.object({ foo: z.string() })) satisfies { - type: "object"; - properties: { foo: { type: "string" } }; - }, - { type: "object", properties: { foo: { type: "string" } } }, - ], - - [ - s(z.literal("Gregor")) satisfies { type: "literal"; value: "Gregor" }, - { type: "literal", value: "Gregor" }, - ], - - [ - s(z.array(z.number())) satisfies { - type: "array"; - element: { type: "number" }; - }, - { type: "array", element: { type: "number" } }, - ], - - [ - s(z.array(z.number()).min(3).max(10)), - { type: "array", element: { type: "number" }, minLength: 3, maxLength: 10 }, - ], - - [ - s(z.array(z.number()).length(10)), - { - type: "array", - element: { type: "number" }, - minLength: 10, - maxLength: 10, - }, - ], - - [ - s(z.union([z.string(), z.number()])) satisfies { - type: "union"; - options: { type: "string" } | { type: "number" }[]; - }, - { type: "union", options: [{ type: "string" }, { type: "number" }] }, - ], - - [ - s(z.intersection(z.string(), z.number())) satisfies { - type: "intersection"; - left: { type: "string" }; - right: { type: "number" }; - }, - { - type: "intersection", - left: { type: "string" }, - right: { type: "number" }, - }, - ], - - [ - s(z.tuple([z.string(), z.number()])) satisfies { - type: "tuple"; - items: ({ type: "string" } | { type: "number" })[]; - }, - { type: "tuple", items: [{ type: "string" }, { type: "number" }] }, - ], - - [ - s(z.tuple([z.string(), z.number()]).rest(z.bigint())), + }), + + p(z.set(z.string()), { type: "set", value: { type: "string" } }), + + p(z.set(z.string()).min(5).max(10), { + type: "set", + value: { type: "string" }, + minSize: 5, + maxSize: 10, + }), + + p(z.set(z.string()).size(5), { + type: "set", + value: { type: "string" }, + minSize: 5, + maxSize: 5, + }), + + p(z.record(z.literal(42)), { + type: "record", + key: { type: "string" }, + value: { type: "literal", value: 42 }, + }), + p(z.map(z.number(), z.string()), { + type: "map", + key: { type: "number" }, + value: { type: "string" }, + }), + + p(z.enum(["foo", "bar"]), { + type: "enum", + values: ["foo", "bar"], + }), + + p(z.union([z.string(), z.number()]), { + type: "union", + options: [{ type: "string" }, { type: "number" }], + }), + p(z.intersection(z.string(), z.number()), { + type: "intersection", + left: { type: "string" }, + right: { type: "number" }, + }), + + p(z.function(z.tuple([z.string()]), z.number()), { + type: "function", + args: { type: "tuple", items: [{ type: "string" }] }, + returns: { type: "number" }, + }), + p(z.promise(z.string()), { type: "promise", value: { type: "string" } }), + + p( + z.lazy(() => z.string().refine(() => true)), + { type: "string" } + ), + + p( + z + .number() + .catch(23) + .pipe(z.promise(z.literal(42))), { - type: "tuple", - items: [{ type: "string" }, { type: "number" }], - rest: { - type: "bigInt", - }, - }, - ], - - [ - s(z.record(z.literal(42))) satisfies { - type: "record"; - key: { type: "string" }; - value: { type: "literal"; value: 42 }; - }, - { - type: "record", - key: { type: "string" }, + type: "promise", value: { type: "literal", value: 42 }, - }, - ], - - [ - s(z.map(z.number(), z.string())) satisfies { - type: "map"; - key: { type: "number" }; - value: { type: "string" }; - }, - { type: "map", key: { type: "number" }, value: { type: "string" } }, - ], - - [ - s(z.set(z.string())) satisfies { type: "set"; value: { type: "string" } }, - { type: "set", value: { type: "string" } }, - ], - - [ - s(z.set(z.string()).min(5).max(10)) satisfies { - type: "set"; - value: { type: "string" }; - }, - { type: "set", value: { type: "string" }, minSize: 5, maxSize: 10 }, - ], - - [ - s(z.set(z.string()).size(5)) satisfies { - type: "set"; - value: { type: "string" }; - }, - { type: "set", value: { type: "string" }, minSize: 5, maxSize: 5 }, - ], - - [ - s(z.function(z.tuple([z.string()]), z.number())) satisfies { - type: "function"; - args: { type: "tuple"; items: [{ type: "string" }] }; - returns: { type: "number" }; - }, - { - type: "function", - args: { type: "tuple", items: [{ type: "string" }] }, - returns: { type: "number" }, - }, - ], + } + ), +] as const)("zerialize %#", (schema, shape) => { + expect(zerialize(schema)).toEqual(shape); + expect(zerialize(dezerialize(shape) as any)).toEqual(zerialize(schema)); +}); - [ - s(z.enum(["foo", "bar"])) satisfies { - type: "enum"; - values: ["foo", "bar"]; - }, - { type: "enum", values: ["foo", "bar"] }, - ], - - [ - s(z.lazy(() => z.string().refine(() => true))) satisfies { type: "string" }, - { type: "string" }, - ], - - [ - s( - z - .number() - .catch(23) - .pipe(z.promise(z.literal(42))) - ) satisfies { - type: "promise"; - value: { type: "literal"; value: 42 }; - }, - { type: "promise", value: { type: "literal", value: 42 } }, - ], -] as const)("%#", (output, expected) => { - expect(output).toEqual(expected); +test.each([ + p(z.string(), { type: "string", isOptional: false }), + p(z.string(), { type: "string", isNullable: false }), +])("isOptional/isNullable", (schema, shape) => { + expect(zerialize(dezerialize(shape) as any)).toEqual(zerialize(schema)); }); test("discriminated union", () => { - const discUnion = z + const schema = z .discriminatedUnion("name", [ z.object({ name: z.literal("Gregor"), age: z.number().optional() }), z.object({ name: z.literal("Lea"), reach: z.number() }), ]) .default({ name: "Lea", reach: 42 }); - const result = zerialize(discUnion); - result satisfies { - type: "discriminatedUnion"; - discriminator: "name"; - options: [ - { - type: "object"; - properties: { - name: { type: "literal"; value: "Gregor" }; - age: { type: "number"; isOptional: true }; - }; - }, - { - type: "object"; - properties: { - name: { type: "literal"; value: "Lea" }; - reach: { type: "number" }; - }; - } - ]; - }; - type InfType = SzInfer; - ({}) as InfType satisfies - | { name: "Gregor"; age?: number } - | { name: "Lea"; reach: number }; - - ({ name: "Gregor" }) satisfies InfType; - - expect(result).toEqual({ + const shape = zerialize(schema); + + // type InfType = z.infer>; + // ({}) as InfType satisfies + // | { name: "Gregor"; age?: number } + // | { name: "Lea"; reach: number }; + + // ({ name: "Gregor" }) satisfies InfType; + + expect(shape).toEqual({ type: "discriminatedUnion", discriminator: "name", options: [ @@ -525,7 +376,31 @@ test("discriminated union", () => { defaultValue: { name: "Lea", reach: 42 }, }); - const shapeDefault = getDefaultValue(result); - // shapeDefault satisfies InfType; - expect(shapeDefault).toEqual({ name: "Lea", reach: 42 }); + // expectTypeOf(shape).toMatchTypeOf<{ + // type: "discriminatedUnion"; + // discriminator: "name"; + // options: [ + // { + // type: "object"; + // properties: { + // name: { type: "literal"; value: "Gregor" }; + // age: { type: "number"; isOptional: true }; + // }; + // }, + // { + // type: "object"; + // properties: { + // name: { type: "literal"; value: "Lea" }; + // reach: { type: "number" }; + // }; + // } + // ]; + // }>(); + + expect( + (dezerialize(shape as SzType) as z.ZodDefault)._def.defaultValue() + ).toEqual({ + name: "Lea", + reach: 42, + }); }); diff --git a/src/index.ts b/src/index.ts index 8e4e327..8fff7fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import { SzType } from "./types"; -export { getDefaultValue } from "./default"; -export { zerialize } from "./zerialize"; -export type { SzInfer } from "./infer"; +export * from "./dezerialize"; +export * from "./zerialize"; + export * from "./types"; export { mapTypesToViews } from "./ui"; diff --git a/src/infer.ts b/src/infer.ts deleted file mode 100644 index f2dbc92..0000000 --- a/src/infer.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { RequiredKeysOf, OptionalKeysOf } from "type-fest"; - -import { - SzType, - SzOptional, - SzNullable, - SzArray, - SzObject, - SzUnion, - SzDiscriminatedUnion, - SzIntersection, - SzTuple, - SzRecord, - SzMap, - SzSet, - SzFunction, - SzEnum, - SzPromise, -} from "./types"; - -type PrimitiveTypes = { - string: string; - number: number; - boolean: boolean; - nan: number; - date: Date; - symbol: symbol; - undefined: undefined; - null: null; - any: any; - unknown: unknown; - never: never; - void: void; -}; - -type RequiredKeys> = { - [K in keyof T]: T[K] extends SzOptional ? never : K; -}[keyof T]; -type OptionalKeys> = { - [K in keyof T]: T[K] extends SzOptional ? K : never; -}[keyof T]; - -// Similar to z.infer but based on serialized types -export type SzInfer = - | (T extends SzOptional ? undefined : never) - | (T extends SzNullable ? null : never) - | (T["type"] extends keyof PrimitiveTypes - ? PrimitiveTypes[T["type"]] - : T extends { type: "literal" } - ? T["value"] - : T extends SzArray - ? SzInfer[] - : T extends SzObject - ? { [Key in RequiredKeys]: SzInfer } & { - [Key in OptionalKeys]?: SzInfer; - } - : T extends SzUnion - ? SzInfer - : T extends SzDiscriminatedUnion - ? SzInfer - : T extends SzIntersection - ? SzInfer & SzInfer - : T extends SzTuple - ? SzInfer[] - : T extends SzRecord - ? Record> - : T extends SzMap - ? Record> - : T extends SzSet - ? T[] - : T extends SzFunction - ? (...args: SzInfer[]) => SzInfer - : T extends SzEnum - ? Values[number] - : T extends SzPromise - ? Promise> - : unknown); diff --git a/src/types.ts b/src/types.ts index 28d4af6..a99a906 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,25 +1,34 @@ import { ValueOf } from "type-fest"; -type DistributeType = T extends any ? { type: T } : never; - export type SzNumber = { type: "number"; min?: number; max?: number; - minInclusive?: number; - maxInclusive?: number; + minInclusive?: boolean; + maxInclusive?: boolean; multipleOf?: number; - int?: true; - finite?: true; + int?: boolean; + finite?: boolean; }; export type SzBigInt = { type: "bigInt"; min?: bigint; max?: bigint; - minInclusive?: number; - maxInclusive?: number; + minInclusive?: boolean; + maxInclusive?: boolean; multipleOf?: bigint; }; + +export const STRING_KINDS = new Set([ + "email", + "url", + "emoji", + "uuid", + "cuid", + "cuid2", + "ulid", +] as const); + export type SzString = { type: "string"; min?: number; @@ -44,7 +53,7 @@ export type SzString = { precision?: number; } | { - kind: "email" | "url" | "emoji" | "uuid" | "cuid" | "cuid" | "ulid"; + kind: typeof STRING_KINDS extends Set ? T : never; } ); @@ -53,55 +62,78 @@ export type SzDate = { min?: number; max?: number; }; -type PlainPrimitiveTypeNames = - | "boolean" - | "nan" - | "undefined" - | "null" - | "any" - | "unknown" - | "never" - | "void"; + +export type SzBoolean = { type: "boolean" }; +export type SzNaN = { type: "nan" }; +export type SzUndefined = { type: "undefined" }; +export type SzNull = { type: "null" }; +export type SzAny = { type: "any" }; +export type SzUnknown = { type: "unknown" }; +export type SzNever = { type: "never" }; +export type SzVoid = { type: "void" }; + export type SzPrimitive = - | DistributeType + | SzBoolean | SzNumber | SzBigInt | SzString - | SzDate; + | SzNaN + | SzDate + | SzUndefined + | SzNull + | SzAny + | SzUnknown + | SzNever + | SzVoid; + export type SzLiteral = { type: "literal"; value: T }; -export type SzArray = { +export type SzArray = { type: "array"; element: T; minLength?: number; maxLength?: number; }; -export type SzObject> = { +export type SzObject< + T extends Record = Record +> = { type: "object"; properties: T; }; -export type SzUnion = { + +export type SzUnion = { type: "union"; options: Options; }; +export type SzDiscriminatedUnionOption = { + [key in Discriminator]: SzType; +} & SzType; export type SzDiscriminatedUnion< - Discriminator extends string, - Options extends SzType[] + Discriminator extends string = string, + Options extends SzDiscriminatedUnionOption[] = [] > = { type: "discriminatedUnion"; discriminator: Discriminator; options: Options; }; -export type SzIntersection = { +export type SzIntersection< + Left extends SzType = SzType, + Right extends SzType = SzType +> = { type: "intersection"; left: Left; right: Right; }; -export type SzTuple = { +export type SzTuple< + Items extends [SzType, ...SzType[]] | [] = [SzType, ...SzType[]] | [] +> = { type: "tuple"; items: Items; rest?: SzType; }; -export type SzRecord = { +export type SzRecord< + Key extends SzKey = SzKey, + Value extends SzType = SzType +> = { type: "record"; key: Key; value: Value; @@ -111,30 +143,35 @@ export type SzMap = { key: Key; value: Value; }; -export type SzSet = { +export type SzSet = { type: "set"; value: T; minSize?: number; maxSize?: number; }; -export type SzFunction = { +export type SzFunction = { type: "function"; args: Args; returns: Return; }; -export type SzEnum = { +export type SzEnum< + Values extends [string, ...string[]] = [string, ...string[]] +> = { type: "enum"; values: Values; }; -export type SzPromise = { type: "promise"; value: T }; +export type SzPromise = { + type: "promise"; + value: T; +}; // Modifiers -export type SzNullable = { isNullable: true }; -export type SzOptional = { isOptional: true }; +export type SzNullable = { isNullable: boolean }; +export type SzOptional = { isOptional: boolean }; export type SzDefault = { defaultValue: T }; // Conjunctions -export type SzKey = { type: "string" | "number" | "symbol" }; +export type SzKey = SzString | SzNumber; export type SzDefaultOrNullable = SzDefault | SzNullable; export type SzType = ( @@ -149,6 +186,7 @@ export type SzType = ( | SzRecord | SzMap | SzSet + | SzFunction | SzEnum | SzPromise ) & diff --git a/src/zerialize.ts b/src/zerialize.ts index 742425b..90fd5c1 100644 --- a/src/zerialize.ts +++ b/src/zerialize.ts @@ -16,9 +16,12 @@ import { SzFunction, SzEnum, SzPromise, - SzPrimitive, SzNumber, + SzPrimitive, + SzType, + STRING_KINDS, } from "./types"; +import { ZodTypes, ZTypeName } from "./zod-types"; export const PRIMITIVES = { ZodString: "string", @@ -40,20 +43,11 @@ export const PRIMITIVES = { >; export type PrimitiveMap = typeof PRIMITIVES; -// Zod Type helpers -type Schema = z.ZodFirstPartySchemaTypes; -type TypeName = T["_def"]["typeName"]; - -type IsZodPrimitive = TypeName extends keyof PrimitiveMap - ? any - : never; - -type ZerializeArray = { - [Index in keyof Items]: Zerialize; -}; +type IsZodPrimitive = + ZTypeName extends keyof PrimitiveMap ? any : never; // Types must match the exported zerialize function's implementation -export type Zerialize = +export type Zerialize = // Modifier types T extends z.ZodOptional ? Zerialize & SzOptional @@ -65,15 +59,17 @@ export type Zerialize = T extends z.ZodNumber ? SzNumber : T extends IsZodPrimitive - ? { - type: (typeof PRIMITIVES)[TypeName]; - } + ? { type: PrimitiveMap[ZTypeName] } : // - T extends z.ZodLiteral - ? SzLiteral + T extends z.ZodLiteral + ? SzLiteral : // List Collections T extends z.ZodTuple - ? SzTuple> + ? { + [Index in keyof Items]: Zerialize; + } extends infer SzItems extends [SzType, ...SzType[]] | [] + ? SzTuple + : SzType : T extends z.ZodSet ? SzSet> : T extends z.ZodArray @@ -94,17 +90,28 @@ export type Zerialize = ? { type: "unknown" } : // Union/Intersection T extends z.ZodUnion - ? SzUnion> + ? { + [Index in keyof Options]: Zerialize; + } extends infer SzOptions extends [SzType, ...SzType[]] + ? SzUnion + : SzType : T extends z.ZodDiscriminatedUnion - ? SzDiscriminatedUnion> + ? SzDiscriminatedUnion< + Discriminator, + { + [Index in keyof Options]: Zerialize; + } + > : T extends z.ZodIntersection ? SzIntersection, Zerialize> : // Specials T extends z.ZodFunction - ? SzFunction, Zerialize> + ? Zerialize extends infer SzArgs extends SzTuple + ? SzFunction> + : SzType : T extends z.ZodPromise ? SzPromise> - : // Unserializable types, fallback to serializing an inner type + : // Unserializable types, fallback to serializing inner type T extends z.ZodLazy ? Zerialize : T extends z.ZodEffects @@ -115,32 +122,21 @@ export type Zerialize = ? Zerialize : T extends z.ZodCatch ? Zerialize - : unknown; + : SzType; type ZodTypeMap = { - [Key in TypeName]: Extract; + [Key in ZTypeName]: Extract; }; type ZerializersMap = { - [Key in TypeName]: ( - def: ZodTypeMap[Key]["_def"] - ) => Zerialize; + [Key in ZTypeName]: (def: ZodTypeMap[Key]["_def"]) => any; //Zerialize; }; -const STRING_KINDS = new Set([ - "email", - "url", - "emoji", - "uuid", - "cuid", - "cuid2", - "ulid", -]); - +const s = zerialize as any; const zerializers = { - ZodOptional: (def) => ({ ...zerialize(def.innerType), isOptional: true }), - ZodNullable: (def) => ({ ...zerialize(def.innerType), isNullable: true }), + ZodOptional: (def) => ({ ...s(def.innerType), isOptional: true }), + ZodNullable: (def) => ({ ...s(def.innerType), isNullable: true }), ZodDefault: (def) => ({ - ...zerialize(def.innerType), + ...s(def.innerType), defaultValue: def.defaultValue(), }), @@ -204,7 +200,7 @@ const zerializers = { ? { precision: check.precision } : {}), } - : STRING_KINDS.has(check.kind) + : STRING_KINDS.has(check.kind as any) ? { kind: check.kind, /* c8 ignore next 2 -- Guard */ @@ -279,13 +275,13 @@ const zerializers = { }), ZodSet: (def) => ({ type: "set", - value: zerialize(def.valueType), + value: s(def.valueType), ...(def.minSize === null ? {} : { minSize: def.minSize.value }), ...(def.maxSize === null ? {} : { maxSize: def.maxSize.value }), }), ZodArray: (def) => ({ type: "array", - element: zerialize(def.type), + element: s(def.type), ...(def.exactLength === null ? {} @@ -302,19 +298,19 @@ const zerializers = { properties: Object.fromEntries( Object.entries(def.shape()).map(([key, value]) => [ key, - zerialize(value as Schema), + s(value as ZodTypes), ]) ), }), ZodRecord: (def) => ({ type: "record", key: zerialize(def.keyType), - value: zerialize(def.valueType), + value: s(def.valueType), }), ZodMap: (def) => ({ type: "map", - key: zerialize(def.keyType), - value: zerialize(def.valueType), + key: s(def.keyType), + value: s(def.valueType), }), ZodEnum: (def) => ({ type: "enum", values: def.values }), @@ -323,7 +319,7 @@ const zerializers = { ZodUnion: (def) => ({ type: "union", - options: def.options.map(zerialize), + options: def.options.map(s), }), ZodDiscriminatedUnion: (def) => ({ type: "discriminatedUnion", @@ -332,7 +328,7 @@ const zerializers = { }), ZodIntersection: (def) => ({ type: "intersection", - left: zerialize(def.left), + left: s(def.left), right: zerialize(def.right), }), @@ -351,8 +347,8 @@ const zerializers = { } satisfies ZerializersMap as ZerializersMap; // Must match the exported Zerialize types -export function zerialize(_schema: T): Zerialize; -export function zerialize(schema: Schema): unknown { +// export function zerialize(_schema: T): Zerialize { +export function zerialize(schema: ZodTypes): unknown { const { _def: def } = schema; return zerializers[def.typeName](def as any); } diff --git a/src/zod-types.ts b/src/zod-types.ts new file mode 100644 index 0000000..c466266 --- /dev/null +++ b/src/zod-types.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; + +type Modifiers = + | z.ZodOptional + | z.ZodNullable + | z.ZodDefault; + +type Primitives = + | z.ZodString + | z.ZodNumber + | z.ZodNaN + | z.ZodBigInt + | z.ZodBoolean + | z.ZodDate + | z.ZodUndefined + | z.ZodNull + | z.ZodAny + | z.ZodUnknown + | z.ZodNever + | z.ZodVoid; + +type ListCollections = + | z.ZodTuple + | z.ZodSet + | z.ZodArray; + +type KVCollections = + | z.ZodObject + | z.ZodRecord + | z.ZodMap; + +type ADTs = + | z.ZodUnion + | z.ZodDiscriminatedUnion[]> + | z.ZodIntersection + | z.ZodNativeEnum + | z.ZodEnum; + +export type ZodTypes = + | Modifiers + | Primitives + | ListCollections + | KVCollections + | ADTs + | z.ZodFunction + | z.ZodLazy + | z.ZodLiteral + | z.ZodEffects + | z.ZodCatch + | z.ZodPromise + | z.ZodBranded + | z.ZodPipeline; + +export type ZTypeName = T["_def"]["typeName"];