From 03030afceae2ae1a005130eab3d092f209a835ee Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 22 Jun 2024 13:43:56 +0800 Subject: [PATCH] feat: add meta schema Also: - Make object's `properties` optional --- .eslintignore | 1 + src/dezerialize.ts | 2 +- src/index.test.ts | 503 +++++++++++++++++++++-------------- src/schema.zodex | 642 +++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 +- 5 files changed, 958 insertions(+), 192 deletions(-) create mode 100644 .eslintignore create mode 100644 src/schema.zodex diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..56ac0f3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/schema.zodex diff --git a/src/dezerialize.ts b/src/dezerialize.ts index 3471301..150ca92 100644 --- a/src/dezerialize.ts +++ b/src/dezerialize.ts @@ -339,7 +339,7 @@ const dezerializers = { object: ((shape: SzObject, opts: DezerializerOptions) => { let i = z.object( Object.fromEntries( - Object.entries(shape.properties).map(([key, value]) => { + Object.entries(shape.properties ?? {}).map(([key, value]) => { return [ key, checkRef(value, opts) || diff --git a/src/index.test.ts b/src/index.test.ts index 0510d2c..aad698f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,8 +1,14 @@ +import fs from "fs"; import { expect, test } from "vitest"; import { z } from "zod"; import { dezerialize, SzType, zerialize, Zerialize } from "./index"; +const zodexSchemaJSON = JSON.parse( + fs.readFileSync("./src/schema.zodex", "utf-8") +); +const zodexSchema = dezerialize(zodexSchemaJSON); + const p = < Schema extends z.ZodFirstPartySchemaTypes, Shape extends SzType = Zerialize @@ -406,200 +412,12 @@ test.each([ value: { type: "literal", value: 42 }, } ), - - p( - z.tuple([ - z.string(), - z.number(), - z.tuple([z.string()]).rest( - z.set( - z.record( - z.record( - z.map( - z.map( - z.string(), - z.union([ - z.string(), - z.discriminatedUnion("status", [ - z.object({ - status: z.literal("success"), - data: z.string(), - }), - z.object({ - status: z.literal("failed"), - name: z.intersection( - z.object({}), - z.intersection( - z.promise( - z - .function() - .args(z.string()) - .returns( - z.function().args(categorySchemaNested) - ) - ), - z.object({}) - ) - ), - }), - ]), - z.number(), - ]) - ), - z.string() - ) - ), - z.string() - ) - ) - ), - ]), - { - items: [ - { - type: "string", - }, - { - type: "number", - }, - { - items: [ - { - type: "string", - }, - ], - rest: { - type: "set", - value: { - key: { - key: { - type: "string", - }, - type: "record", - value: { - key: { - key: { - type: "string", - }, - type: "map", - value: { - options: [ - { - type: "string", - }, - { - discriminator: "status", - options: [ - { - properties: { - data: { - type: "string", - }, - status: { - type: "literal", - value: "success", - }, - }, - type: "object", - }, - { - properties: { - name: { - left: { - properties: {}, - type: "object", - }, - right: { - left: { - type: "promise", - value: { - args: { - items: [ - { - type: "string", - }, - ], - rest: { - type: "unknown", - }, - type: "tuple", - }, - returns: { - args: { - items: [ - { - properties: { - name: { - type: "string", - }, - subcategory: { - $ref: "#/items/2/rest/value/key/value/key/value/options/1/options/1/properties/name/right/left/value/returns/args/items/0", - }, - }, - type: "object", - }, - ], - rest: { - type: "unknown", - }, - type: "tuple", - }, - returns: { - type: "unknown", - }, - type: "function", - }, - type: "function", - }, - }, - right: { - properties: {}, - type: "object", - }, - type: "intersection", - }, - type: "intersection", - }, - status: { - type: "literal", - value: "failed", - }, - }, - type: "object", - }, - ], - type: "discriminatedUnion", - }, - { - type: "number", - }, - ], - type: "union", - }, - }, - type: "map", - value: { - type: "string", - }, - }, - }, - type: "record", - value: { - type: "string", - }, - }, - }, - type: "tuple", - }, - ], - type: "tuple", - } - ), ] as const)("zerialize %#", (schema, shape) => { const zer = zerialize(schema); - // console.log(JSON.stringify(zer, null, 2)); expect(zer).toEqual(shape); expect(zerialize(dezerialize(shape) as any)).toEqual(zerialize(schema)); + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test.each([ @@ -611,9 +429,24 @@ test.each([ properties: {}, }), ])("isOptional/isNullable/readonly", (schema, shape) => { + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; expect(zerialize(dezerialize(shape) as any)).toEqual(zerialize(schema)); }); +test("object with optional properties", () => { + const shape = { + type: "object", + readonly: false, + }; + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; + + expect(() => { + dezerialize(shape as any); + }).not.to.throw(); +}); + test("discriminated union", () => { const schema = z .discriminatedUnion("name", [ @@ -679,6 +512,9 @@ test("discriminated union", () => { name: "Lea", reach: 42, }); + + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("coerce (number)", () => { @@ -690,6 +526,9 @@ test("coerce (number)", () => { coerce: true, }); expect(dezerialize(shape as SzType).parse("42")).toEqual(42); + + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("coerce (bigint)", () => { @@ -701,6 +540,9 @@ test("coerce (bigint)", () => { coerce: true, }); expect(dezerialize(shape as SzType).parse("42")).toEqual(42n); + + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("coerce (date)", () => { @@ -714,6 +556,8 @@ test("coerce (date)", () => { expect(dezerialize(shape as SzType).parse("1999-01-01")).toEqual( new Date("1999-01-01") ); + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("coerce (string)", () => { @@ -725,6 +569,9 @@ test("coerce (string)", () => { coerce: true, }); expect(dezerialize(shape as SzType).parse(42)).toEqual("42"); + + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("coerce (boolean)", () => { @@ -736,6 +583,8 @@ test("coerce (boolean)", () => { coerce: true, }); expect(dezerialize(shape as SzType).parse(0)).toEqual(false); + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); test("named superrefinements and transforms", () => { @@ -830,6 +679,9 @@ test("named superrefinements and transforms", () => { expect(res4.success).to.be.true; // Will be transformed down expect(res4.data.getTime()).toBeLessThan(new Date().getTime()); + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("preprocess", () => { @@ -855,6 +707,9 @@ test("preprocess", () => { expect(res1.success).to.be.true; expect(res1.data).to.be.equal(1500); + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("dezerialize effects without options", () => { @@ -891,6 +746,9 @@ test("dezerialize effects without options", () => { ) as z.SafeParseSuccess; expect(res1.success).to.be.true; + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("describe", () => { @@ -911,6 +769,9 @@ test("describe", () => { ) as z.SafeParseSuccess; expect(res1.success).to.be.true; + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("recursive schemas (nested)", () => { @@ -957,6 +818,9 @@ test("recursive schemas (nested)", () => { expect(serialized).toEqual(expectedShape); const dezSchema = dezerialize(serialized); + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("recursive schemas", () => { @@ -1006,6 +870,9 @@ test("recursive schemas", () => { expect(serialized).toEqual(expectedShape); const dezSchema = dezerialize(serialized); + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; }); test("recursive tuple schema", () => { @@ -1050,4 +917,260 @@ test("recursive tuple schema", () => { expect(serialized).toEqual(expectedShape); const dezSchema = dezerialize(serialized); + + const parsed = zodexSchema.safeParse(expectedShape); + expect(parsed.success).to.be.true; +}); + +test("Object with inner $ref", () => { + const schema = z.promise( + z + .function() + .args(z.string()) + .returns(z.function().args(categorySchemaNested)) + ); + const shape = { + type: "promise", + value: { + args: { + items: [ + { + type: "string", + }, + ], + rest: { + type: "unknown", + }, + type: "tuple", + }, + returns: { + args: { + items: [ + { + properties: { + name: { + type: "string", + }, + subcategory: { + $ref: "#/value/returns/args/items/0", + }, + }, + type: "object", + }, + ], + rest: { + type: "unknown", + }, + type: "tuple", + }, + returns: { + type: "unknown", + }, + type: "function", + }, + type: "function", + }, + }; + + const zer = zerialize(schema); + expect(zer).toEqual(shape); + expect(zerialize(dezerialize(shape as any) as any)).toEqual( + zerialize(schema) + ); + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; +}); + +test.skip("Large object with inner $ref", () => { + const schema = z.tuple([ + z.string(), + z.number(), + z.tuple([z.string()]).rest( + z.set( + z.record( + z.string(), + z.record( + z.map( + z.string(), + z.map( + z.string(), + z.union([ + z.string(), + z.discriminatedUnion("status", [ + z.object({ + status: z.literal("success"), + data: z.string(), + }), + z.object({ + status: z.literal("failed"), + name: z.intersection( + z.object({}), + z.intersection( + z.promise( + z + .function() + .args(z.string()) + .returns(z.function().args(categorySchemaNested)) + ), + z.object({}) + ) + ), + }), + ]), + z.number(), + ]) + ) + ) + ) + ) + ) + ), + ]); + const shape = { + items: [ + { + type: "string", + }, + { + type: "number", + }, + { + items: [ + { + type: "string", + }, + ], + rest: { + type: "set", + value: { + value: { + key: { + type: "string", + }, + type: "record", + value: { + value: { + key: { + type: "string", + }, + type: "map", + value: { + options: [ + { + type: "string", + }, + { + discriminator: "status", + options: [ + { + properties: { + data: { + type: "string", + }, + status: { + type: "literal", + value: "success", + }, + }, + type: "object", + }, + { + properties: { + name: { + left: { + properties: {}, + type: "object", + }, + right: { + left: { + type: "promise", + value: { + args: { + items: [ + { + type: "string", + }, + ], + rest: { + type: "unknown", + }, + type: "tuple", + }, + returns: { + args: { + items: [ + { + properties: { + name: { + type: "string", + }, + subcategory: { + $ref: "#/items/2/rest/value/value/value/value/value/options/1/options/1/properties/name/right/left/value/returns/args/items/0", + }, + }, + type: "object", + }, + ], + rest: { + type: "unknown", + }, + type: "tuple", + }, + returns: { + type: "unknown", + }, + type: "function", + }, + type: "function", + }, + }, + right: { + properties: {}, + type: "object", + }, + type: "intersection", + }, + type: "intersection", + }, + status: { + type: "literal", + value: "failed", + }, + }, + type: "object", + }, + ], + type: "discriminatedUnion", + }, + { + type: "number", + }, + ], + type: "union", + }, + }, + type: "map", + key: { + type: "string", + }, + }, + }, + type: "record", + key: { + type: "string", + }, + }, + }, + type: "tuple", + }, + ], + type: "tuple", + }; + const zer = zerialize(schema); + expect(zer).toEqual(shape); + expect(zerialize(dezerialize(shape as any) as any)).toEqual( + zerialize(schema) + ); + const parsed = zodexSchema.safeParse(shape); + expect(parsed.success).to.be.true; }); diff --git a/src/schema.zodex b/src/schema.zodex new file mode 100644 index 0000000..a3f0b7e --- /dev/null +++ b/src/schema.zodex @@ -0,0 +1,642 @@ +{ + "$defs": { + "reference-or-type": { + "type": "union", + "options": [ + { + "$ref": "#/$defs/type" + }, + { + "$ref": "#/$defs/reference" + } + ] + }, + "reference": { + "describe": "JSON Reference", + "type": "object", + "properties": { + "$ref": { + "type": "string" + } + } + }, + "string": { + "type": "intersection", + "describe": "String", + "left": { + "describe": "String basics", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["string"], + "defaultValue": "string" + }, + "coerce": {"type": "boolean", "isOptional": true}, + "min": {"type": "number", "isOptional": true}, + "max": {"type": "number", "isOptional": true}, + "length": {"type": "number", "isOptional": true}, + "startsWith": {"type": "string", "isOptional": true}, + "endsWith": {"type": "string", "isOptional": true}, + + "toLowerCase": {"type": "boolean", "isOptional": true}, + "toUpperCase": {"type": "boolean", "isOptional": true}, + "trim": {"type": "boolean", "isOptional": true} + } + }, + "right": { + "type": "intersection", + "left": { + "describe": "String includes", + "type": "union", + "options": [ + { + "type": "object", + "properties": { + } + }, + { + "type": "object", + "properties": { + "includes": {"type": "string"}, + "position": {"type": "number", "isOptional": true} + } + } + ] + }, + "right": { + "describe": "String kinds", + "type": "union", + "options": [ + { + "type": "object", + "properties": { + } + }, + { + "type": "object", + "properties": { + "kind": { + "type": "enum", + "values": ["ip"], + "defaultValue": "ip" + }, + "version": { + "type": "enum", + "values": ["v4", "v6"], + "isOptional": true + } + } + }, + { + "type": "object", + "properties": { + "regex": {"type": "string"}, + "flags": {"type": "string", "isOptional": true} + } + }, + { + "type": "object", + "properties": { + "kind": { + "type": "enum", + "values": ["time"], + "defaultValue": "time" + }, + "precision": {"type": "number", "isOptional": true} + } + }, + { + "type": "object", + "properties": { + "kind": { + "type": "enum", + "values": ["datetime"], + "defaultValue": "datetime" + }, + "offset": {"type": "boolean", "isOptional": true}, + "local": {"type": "boolean", "isOptional": true}, + "precision": {"type": "number", "isOptional": true} + } + }, + { + "type": "object", + "properties": { + "kind": { + "type": "enum", + "values": [ + "email", + "url", + "emoji", + "uuid", + "nanoid", + "cuid", + "cuid2", + "ulid", + "date", + "duration", + "base64" + ] + } + } + } + ] + } + } + }, + "number": { + "describe": "Number", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["number"], + "defaultValue": "number" + }, + "coerce": {"type": "boolean", "isOptional": true}, + "min": {"type": "number", "isOptional": true}, + "max": {"type": "number", "isOptional": true}, + "minInclusive": {"type": "boolean", "isOptional": true}, + "maxInclusive": {"type": "boolean", "isOptional": true}, + "multipleOf": {"type": "number", "isOptional": true}, + "int": {"type": "boolean", "isOptional": true}, + "finite": {"type": "boolean", "isOptional": true} + } + }, + "key": { + "describe": "Key", + "type": "union", + "options": [ + { + "$ref": "#/$defs/string" + }, + { + "$ref": "#/$defs/number" + } + ] + }, + "tuple": { + "describe": "Tuple", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["tuple"], + "defaultValue": "tuple" + }, + "items": { + "type": "tuple", + "items": [ + { + "$ref": "#/$defs/reference-or-type" + } + ], + "rest": { + "$ref": "#/$defs/reference-or-type" + } + }, + "rest": { + "type": "union", + "options": [{ + "$ref": "#/$defs/reference-or-type" + }], + "isOptional": true + } + } + }, + "type": { + "type": "intersection", + "left": { + "type": "union", + "options": [ + { + "describe": "Boolean", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["boolean"], + "defaultValue": "boolean" + }, + "coerce": { + "type": "boolean", + "isOptional": true + } + } + }, + { + "$ref": "#/$defs/number" + }, + { + "describe": "BigInt", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["bigInt"], + "defaultValue": "bigInt" + }, + "coerce": {"type": "boolean", "isOptional": true}, + "min": {"type": "string", "isOptional": true}, + "max": {"type": "string", "isOptional": true}, + "minInclusive": {"type": "boolean", "isOptional": true}, + "maxInclusive": {"type": "boolean", "isOptional": true}, + "multipleOf": {"type": "string", "isOptional": true} + } + }, + { + "$ref": "#/$defs/string" + }, + { + "describe": "Not a Number", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["nan"], + "defaultValue": "nan" + } + } + }, + { + "describe": "Date", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["date"], + "defaultValue": "date" + }, + "coerce": {"type": "boolean", "isOptional": true}, + "min": {"type": "number", "isOptional": true}, + "max": {"type": "number", "isOptional": true} + } + }, + { + "describe": "Undefined", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["undefined"], + "defaultValue": "undefined" + } + } + }, + { + "describe": "Null", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["null"], + "defaultValue": "null" + } + } + }, + { + "describe": "Any", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["any"], + "defaultValue": "any" + } + } + }, + { + "describe": "Unknown", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["unknown"], + "defaultValue": "unknown" + } + } + }, + { + "describe": "Never", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["never"], + "defaultValue": "never" + } + } + }, + { + "describe": "Void", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["void"], + "defaultValue": "void" + } + } + }, + { + "describe": "Symbol", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["symbol"], + "defaultValue": "symbol" + } + } + }, + { + "describe": "Literal", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["literal"], + "defaultValue": "literal" + }, + "value": { + "type": "union", + "options": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + { + "describe": "Array", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["array"], + "defaultValue": "array" + }, + "element": { + "$ref": "#/$defs/reference-or-type" + }, + "minLength": {"type": "number", "isOptional": true}, + "maxLength": {"type": "number", "isOptional": true} + } + }, + { + "describe": "Object", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["object"], + "defaultValue": "object" + }, + "properties": { + "type": "record", + "key": { "type": "string" }, + "value": { + "$ref": "#/$defs/reference-or-type" + }, + "isOptional": true + }, + "unknownKeys": { + "type": "enum", + "values": ["strict", "strip", "passthrough"], + "isOptional": true + } + } + }, + { + "describe": "Union", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["union"], + "defaultValue": "union" + }, + "options": { + "type": "tuple", + "items": [ + { + "$ref": "#/$defs/reference-or-type" + } + ], + "rest": { + "$ref": "#/$defs/reference-or-type" + } + } + } + }, + { + "describe": "Discriminated Union", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["discriminatedUnion"], + "defaultValue": "discriminatedUnion" + }, + "discriminator": { + "type": "string" + }, + "options": { + "type": "array", + "element": { + "$ref": "#/$defs/reference-or-type" + } + } + } + }, + { + "describe": "Intersection", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["intersection"], + "defaultValue": "intersection" + }, + "left": { + "$ref": "#/$defs/reference-or-type" + }, + "right": { + "$ref": "#/$defs/reference-or-type" + } + } + }, + { + "$ref": "#/$defs/tuple" + }, + { + "describe": "Record", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["record"], + "defaultValue": "record" + }, + "key": { + "$ref": "#/$defs/key" + }, + "value": { + "$ref": "#/$defs/reference-or-type" + } + } + }, + { + "describe": "Map", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["map"], + "defaultValue": "map" + }, + "key": { + "$ref": "#/$defs/key" + }, + "value": { + "$ref": "#/$defs/reference-or-type" + } + } + }, + { + "describe": "Set", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["set"], + "defaultValue": "set" + }, + "value": { + "$ref": "#/$defs/reference-or-type" + }, + "minSize": {"type": "number", "isOptional": true}, + "maxSize": {"type": "number", "isOptional": true} + } + }, + { + "describe": "Function", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["function"], + "defaultValue": "function" + }, + "args": { + "$ref": "#/$defs/tuple" + }, + "returns": { + "$ref": "#/$defs/reference-or-type" + } + } + }, + { + "describe": "Enum", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["enum"], + "defaultValue": "enum" + }, + "values": { + "type": "tuple", + "items": [{ + "type": "string" + }], + "rest": { + "type": "string" + } + } + } + }, + { + "describe": "Promise", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["promise"], + "defaultValue": "promise" + }, + "value": { + "$ref": "#/$defs/reference-or-type" + } + } + }, + { + "describe": "Effect", + "type": "object", + "properties": { + "type": { + "type": "enum", + "values": ["effect"], + "defaultValue": "effect" + }, + "effects": { + "type": "array", + "element": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "enum", + "values": [ + "refinement", + "transform", + "preprocess" + ] + } + } + } + }, + "inner": { + "$ref": "#/$defs/reference-or-type" + } + } + } + ] + }, + "right": { + "describe": "Modifiers", + "type": "object", + "properties": { + "isNullable": {"type": "boolean", "isOptional": true}, + "isOptional": {"type": "boolean", "isOptional": true}, + "defaultValue": { + "type": "union", + "options": [ + { + "type": "any" + } + ], + "isOptional": true + }, + "description": {"type": "string", "isOptional": true}, + "readonly": {"type": "boolean", "isOptional": true} + } + } + } + }, + "type": "union", + "options": [ + { + "$ref": "#/$defs/type" + } + ] +} diff --git a/src/types.ts b/src/types.ts index f6bf875..f3cc2e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,7 +115,7 @@ export type SzObject< T extends Record = Record > = { type: "object"; - properties: T; + properties?: T; unknownKeys?: "strict" | "strip" | "passthrough"; };