From 7873d3d503815905f80137758b993e7765305bb0 Mon Sep 17 00:00:00 2001 From: Saiichi Hashimoto Date: Tue, 7 Jun 2022 23:27:04 -0500 Subject: [PATCH] feat(array): use items to build up types --- src/array/index.spec.ts | 113 ++++++++++++------ src/array/index.ts | 255 +++++++++++++++++++++------------------- src/builder.ts | 2 +- src/object/index.ts | 2 +- 4 files changed, 216 insertions(+), 156 deletions(-) diff --git a/src/array/index.spec.ts b/src/array/index.spec.ts index 313398f..4b7ac98 100644 --- a/src/array/index.spec.ts +++ b/src/array/index.spec.ts @@ -6,24 +6,28 @@ import { fields } from "../fields"; import { object } from "../object"; import { mockRule } from "../test-utils"; -import { array } from "."; +import { array, items } from "."; import type { ValidateShape } from "../test-utils"; import type { InferInput, InferOutput } from "../types"; +import type { PartialDeep } from "type-fest"; describe("array", () => { it("builds a sanity config", () => - expect(array().schema()).toEqual({ + expect(array({ of: items() }).schema()).toEqual({ type: "array", of: [], validation: expect.any(Function), })); it("passes through schema values", () => - expect(array({ hidden: false }).schema()).toHaveProperty("hidden", false)); + expect(array({ of: items(), hidden: false }).schema()).toHaveProperty( + "hidden", + false + )); it("parses into an array", () => { - const type = array(); + const type = array({ of: items() }); const value: ValidateShape, never[]> = []; const parsedValue: ValidateShape< @@ -35,7 +39,7 @@ describe("array", () => { }); it("adds primitive types", () => { - const type = array().of(boolean()); + const type = array({ of: items().item(boolean()) }); const schema = type.schema(); @@ -58,14 +62,16 @@ describe("array", () => { }); it("adds nonprimitive types", () => { - const type = array().of( - object({ - fields: fields().field({ - name: "foo", - type: boolean(), - }), - }) - ); + const type = array({ + of: items().item( + object({ + fields: fields().field({ + name: "foo", + type: boolean(), + }), + }) + ), + }); const schema = type.schema(); @@ -95,23 +101,25 @@ describe("array", () => { }); it("creates union with multiple types", () => { - const type = array() - .of( - object({ - fields: fields().field({ - name: "foo", - type: boolean(), - }), - }) - ) - .of( - object({ - fields: fields().field({ - name: "bar", - type: boolean(), - }), - }) - ); + const type = array({ + of: items() + .item( + object({ + fields: fields().field({ + name: "foo", + type: boolean(), + }), + }) + ) + .item( + object({ + fields: fields().field({ + name: "bar", + type: boolean(), + }), + }) + ), + }); const schema = type.schema(); @@ -151,7 +159,7 @@ describe("array", () => { }); it("sets min", () => { - const type = array({ min: 1 }).of(boolean()); + const type = array({ min: 1, of: items().item(boolean()) }); const rule = mockRule(); @@ -173,7 +181,7 @@ describe("array", () => { }); it("sets max", () => { - const type = array({ max: 1 }).of(boolean()); + const type = array({ max: 1, of: items().item(boolean()) }); const rule = mockRule(); @@ -195,7 +203,7 @@ describe("array", () => { }); it("sets length", () => { - const type = array({ length: 1 }).of(boolean()); + const type = array({ length: 1, of: items().item(boolean()) }); const rule = mockRule(); @@ -220,7 +228,7 @@ describe("array", () => { }); it("sets nonempty", () => { - const type = array({ nonempty: true }).of(boolean()); + const type = array({ nonempty: true, of: items().item(boolean()) }); const rule = mockRule(); @@ -243,4 +251,41 @@ describe("array", () => { type.parse([]); }).toThrow(z.ZodError); }); + + it("types custom validation", () => { + const type = array({ + of: items() + .item( + object({ + fields: fields().field({ + name: "foo", + type: boolean(), + }), + }) + ) + .item( + object({ + fields: fields().field({ + name: "bar", + type: boolean(), + }), + }) + ), + validation: (Rule) => + Rule.custom((value) => { + const elements: ValidateShape< + typeof value, + PartialDeep> + > = value; + + return elements.length > 50 || "Needs to be 50 characters"; + }), + }); + + const rule = mockRule(); + + type.schema().validation?.(rule); + + expect(rule.custom).toHaveBeenCalledWith(expect.any(Function)); + }); }); diff --git a/src/array/index.ts b/src/array/index.ts index cb24fb6..4807bb3 100644 --- a/src/array/index.ts +++ b/src/array/index.ts @@ -1,154 +1,188 @@ +import { faker } from "@faker-js/faker"; import { flow } from "lodash/fp"; import { z } from "zod"; import type { FieldOptionKeys } from "../fields"; -import type { InferZod, SanityType, TypeValidation } from "../types"; +import type { InferZod, Resolve, SanityType, TypeValidation } from "../types"; +import type { Faker } from "@faker-js/faker"; import type { Schema } from "@sanity/types"; type UnArray = T extends Array ? U : never; // HACK Shouldn't have to omit FieldOptionKeys because arrays don't need names -type ArrayElementDefinition = Omit< +type ItemDefinition = Omit< UnArray, FieldOptionKeys >; -type ZodArrayElement< - Positions extends string, - Fields extends { - [field in Positions]: SanityType; - } -> = "00" extends Positions - ? z.ZodUnion< - readonly [ - InferZod, - ...Array> - ] - > - : "0" extends Positions - ? InferZod - : z.ZodNever; - type ZodArray< Positions extends string, - Fields extends { - [field in Positions]: SanityType; + Items extends { + [field in Positions]: SanityType; }, NonEmpty extends boolean > = z.ZodArray< - ZodArrayElement, + "00" extends Positions + ? z.ZodUnion< + readonly [ + InferZod, + ...Array> + ] + > + : "0" extends Positions + ? InferZod + : z.ZodNever, NonEmpty extends true ? "atleastone" : "many" >; -interface ArrayType< +interface ItemsType< Positions extends string, - Fields extends { - [field in Positions]: SanityType; - }, - NonEmpty extends boolean -> extends SanityType< - Omit< - TypeValidation< - Schema.ArrayDefinition>>, - z.infer> - >, - FieldOptionKeys - >, - ZodArray - > { - of: < + Items extends { + [field in Positions]: SanityType; + } +> extends SanityType> { + item: < Zod extends z.ZodType, NewPosition extends Exclude<`${Positions}0`, Positions> >( - type: SanityType - ) => ArrayType< + item: SanityType + ) => ItemsType< Positions | NewPosition, // @ts-expect-error -- Not sure how to solve this - Fields & { - [field in NewPosition]: SanityType; - }, - NonEmpty + Resolve< + Items & { + [field in NewPosition]: SanityType; + } + > >; } -type ArrayDef< +const itemsInternal = < Positions extends string, - Fields extends { - [field in Positions]: SanityType; + Items extends { + [field in Positions]: SanityType; }, NonEmpty extends boolean -> = Omit< - TypeValidation< - Schema.ArrayDefinition>>, - z.infer> - >, - FieldOptionKeys | "of" | "type" -> & { - length?: number; - max?: number; - min?: number; - nonempty?: NonEmpty; +>( + items: Array +): ItemsType => { + const zod = z.array( + items.length === 0 + ? z.never() + : items.length === 1 + ? items[0]!.zod + : z.union([ + items[0]!.zod, + items[1]!.zod, + ...(items + .slice(2) + .map( + >({ + zod, + }: SanityType) => zod + ) as unknown as readonly [ + InferZod, + ...Array> + ]), + ]) + ) as ZodArray; + + return { + zod, + parse: zod.parse.bind(zod), + // FIXME Mock the array element types. Not sure how to allow an override, since the function has to be defined before we know the element types. + mock: () => [] as unknown as z.infer>, + schema: () => items.map(({ schema }) => schema()), + item: < + Zod extends z.ZodType, + NewPosition extends Exclude<`${Positions}0`, Positions> + >( + item: SanityType + ) => + itemsInternal< + Positions | NewPosition, + // @ts-expect-error -- Not sure how to solve this + Resolve< + Items & { + [field in NewPosition]: SanityType; + } + >, + NonEmpty + >([...items, item]), + }; }; -const arrayInternal = < +export const items = () => + itemsInternal<"", Record<"", never>, NonEmpty>([]); + +interface ArrayType< Positions extends string, - Fields extends { - [field in Positions]: SanityType; + Items extends { + [field in Positions]: SanityType; }, NonEmpty extends boolean +> extends SanityType< + Omit< + TypeValidation< + Schema.ArrayDefinition>>, + z.infer> + >, + FieldOptionKeys + >, + ZodArray + > {} + +export const array = < + Positions extends string, + Items extends { + [field in Positions]: SanityType; + }, + NonEmpty extends boolean = false >( - def: ArrayDef, - ofs: Array -): ArrayType => { - const { length, max, min, nonempty, validation } = def; + def: Omit< + TypeValidation< + Schema.ArrayDefinition>>, + z.infer> + >, + FieldOptionKeys | "of" | "type" + > & { + length?: number; + max?: number; + min?: number; + mock?: (faker: Faker) => z.infer>; + nonempty?: NonEmpty; + of: ItemsType; + } +): ArrayType => { + const { + length, + max, + min, + nonempty, + of: { schema: itemsSchema, mock: itemsMock, zod: itemsZod }, + mock = itemsMock as unknown as ( + faker: Faker + ) => z.infer>, + validation, + } = def; const zod = flow( - (zod: ZodArray) => + (zod: ZodArray) => !nonempty ? zod : zod.nonempty(), - (zod: ZodArray) => (!min ? zod : zod.min(min)), - (zod: ZodArray) => (!max ? zod : zod.max(max)), - (zod: ZodArray) => + (zod: ZodArray) => (!min ? zod : zod.min(min)), + (zod: ZodArray) => (!max ? zod : zod.max(max)), + (zod: ZodArray) => length === undefined ? zod : zod.length(length) - )( - z.array( - ofs.length === 0 - ? z.never() - : ofs.length === 1 - ? ofs[0]!.zod - : z.union([ - ofs[0]!.zod, - ofs[1]!.zod, - ...(ofs - .slice(2) - .map( - >({ - zod, - }: SanityType) => zod - ) as unknown as readonly [ - InferZod, - ...Array> - ]), - ]) - ) as ZodArray - ) as ZodArray; + )(itemsZod) as ZodArray; return { zod, parse: zod.parse.bind(zod), - // FIXME Mock the array element types. Not sure how to allow an override, since the function has to be defined before we know the element types. - mock: () => [] as unknown as z.infer>, + mock: () => mock(faker), schema: () => ({ ...def, type: "array", - of: ofs.map( - < - Definition extends - | Schema.TypeDefinition - | Schema.TypeReference - >({ - schema, - }: SanityType) => schema() - ), + of: itemsSchema(), validation: flow( (rule) => (!nonempty ? rule : rule.min(1)), (rule) => (!min ? rule : rule.min(min)), @@ -157,24 +191,5 @@ const arrayInternal = < (rule) => validation?.(rule) ?? rule ), }), - of: < - Zod extends z.ZodType, - NewPosition extends Exclude<`${Positions}0`, Positions>, - NonEmpty extends boolean - >( - type: SanityType - ) => - arrayInternal< - Positions | NewPosition, - // @ts-expect-error -- Not sure how to solve this - Fields & { - [field in NewPosition]: SanityType; - }, - NonEmpty - >(def, [...ofs, type]), }; }; - -export const array = ( - def: ArrayDef<"", Record<"", never>, NonEmpty> = {} -) => arrayInternal<"", Record<"", never>, NonEmpty>(def, []); diff --git a/src/builder.ts b/src/builder.ts index 8bd4ba3..877ee51 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -1,4 +1,4 @@ -export { array } from "./array"; +export { array, items } from "./array"; export { block } from "./block"; export { boolean } from "./boolean"; export { date } from "./date"; diff --git a/src/object/index.ts b/src/object/index.ts index af310ba..13528e3 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -34,7 +34,7 @@ export const object = >( ): ObjectType => { const { preview: previewDef, - fields: { schema: fieldsSchema, mock: fieldsMock, zod: fieldsZod }, + fields: { mock: fieldsMock, schema: fieldsSchema, zod: fieldsZod }, mock = fieldsMock, } = def; const zod = fieldsZod as InferFieldsZod;