Skip to content
This repository has been archived by the owner on May 6, 2023. It is now read-only.

Commit

Permalink
feat(types): options.list narrows number and string types
Browse files Browse the repository at this point in the history
  • Loading branch information
saiichihashimoto committed Jun 16, 2022
1 parent cc667d3 commit 69a4018
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 54 deletions.
14 changes: 3 additions & 11 deletions src/array/index.ts
@@ -1,7 +1,7 @@
import { flow } from "lodash/fp";
import { z } from "zod";

import { createType } from "../types";
import { createType, zodUnion } from "../types";

import type {
NamedSchemaFields,
Expand Down Expand Up @@ -63,7 +63,7 @@ export const array = <
zod: zodFn = (zod) => zod as unknown as z.ZodType<Output, any, z.input<Zod>>,
...def
}: Merge<
SanityTypeDef<Schema.ArrayDefinition<z.input<Zod>>, Zod, Output>,
SanityTypeDef<Schema.ArrayDefinition<z.input<Zod>[number]>, Zod, Output>,
{
length?: number;
max?: number;
Expand All @@ -81,15 +81,7 @@ export const array = <
(zod) => (length === undefined ? zod : zod.length(length)),
(zod) => zodFn(zod)
)(
z.array<Zods>(
items.length === 1
? (addKeyToZod(items[0]!.zod) as unknown as Zods)
: (z.union([
addKeyToZod(items[0]!.zod),
addKeyToZod(items[1]!.zod),
...items.slice(2).map(({ zod }) => addKeyToZod(zod)),
]) as unknown as Zods)
)
z.array<Zods>(zodUnion(items.map(({ zod }) => addKeyToZod(zod) as Zods)))
),
schema: () => ({
...def,
Expand Down
31 changes: 31 additions & 0 deletions src/list/index.ts
@@ -0,0 +1,31 @@
import { isObject } from "lodash/fp";

import type { Faker } from "@faker-js/faker";

export type WithTypedOptionsList<
Value,
T extends {
options?: { list?: Array<any | { title: string; value: any }> };
}
> = Omit<T, "options"> & {
options?: Omit<T["options"], "list"> & {
list?: Array<Value | { title: string; value: Value }>;
};
};

type List<T> = Array<T | { title: string; value: T }>;

export const listToListValues = <T>(list: List<T>) =>
list.map((item) =>
isObject(item) && "title" in item && "value" in item ? item.value : item
);

export const listMock =
<Input>(
list: List<Input> | undefined,
mock: (faker: Faker, path: string) => Input
) =>
(faker: Faker, path: string) =>
!list?.length
? mock(faker, path)
: faker.helpers.arrayElement(listToListValues<Input>(list));
21 changes: 21 additions & 0 deletions src/number/index.spec.ts
Expand Up @@ -250,4 +250,25 @@ describe("number", () => {

expect(rule.custom).toHaveBeenCalledWith(expect.any(Function));
});

it("types values from list", () => {
const type = number({
options: {
list: [3, { title: "Four", value: 4 }],
},
});

const value: ValidateShape<InferInput<typeof type>, 3 | 4> = 3;
const parsedValue: ValidateShape<
InferOutput<typeof type>,
3 | 4
> = type.parse(value);

expect(parsedValue).toEqual(value);
expect([3, 4]).toContain(type.mock(faker));

expect(() => {
type.parse(2);
}).toThrow(z.ZodError);
});
});
73 changes: 44 additions & 29 deletions src/number/index.ts
@@ -1,32 +1,39 @@
import { flow } from "lodash/fp";
import { z } from "zod";

import { createType } from "../types";
import { listMock, listToListValues } from "../list";
import { createType, zodUnion } from "../types";

import type { WithTypedOptionsList } from "../list";
import type { Rule, SanityTypeDef } from "../types";
import type { Schema } from "@sanity/types";

export const number = <Output = number>({
export const number = <Input extends number, Output = Input>({
greaterThan,
integer,
lessThan,
max,
min,
options,
negative,
positive,
precision,
validation,
mock = (faker) =>
faker.datatype.number({
max,
min,
precision: 1 / 10 ** (precision ?? 0),
}),
zod: zodFn = (zod) => zod as unknown as z.ZodType<Output, any, number>,
options: { list = undefined } = {},
mock = listMock(
list,
(faker) =>
faker.datatype.number({
max,
min,
precision: 1 / 10 ** (precision ?? 0),
}) as Input
),
zod: zodFn = (zod) => zod as unknown as z.ZodType<Output, any, Input>,
...def
}: SanityTypeDef<
Schema.NumberDefinition,
z.ZodType<number, any, number>,
WithTypedOptionsList<Input, Schema.NumberDefinition>,
z.ZodType<Input, any, Input>,
Output
> & {
greaterThan?: number;
Expand All @@ -40,30 +47,38 @@ export const number = <Output = number>({
} = {}) =>
createType({
mock,
zod: flow(
flow(
(zod: z.ZodNumber) => (!min ? zod : zod.min(min)),
(zod) => (!max ? zod : zod.max(max)),
(zod) => (!greaterThan ? zod : zod.gt(greaterThan)),
(zod) => (!lessThan ? zod : zod.lt(lessThan)),
(zod) => (!integer ? zod : zod.int()),
(zod) => (!positive ? zod : zod.nonnegative()),
(zod) => (!negative ? zod : zod.negative())
),
(zod) =>
!precision
? zod
: zod.transform(
(value) => Math.round(value * 10 ** precision) / 10 ** precision
zod: zodFn(
!list?.length
? flow(
flow(
(zod: z.ZodNumber) => (!min ? zod : zod.min(min)),
(zod) => (!max ? zod : zod.max(max)),
(zod) => (!greaterThan ? zod : zod.gt(greaterThan)),
(zod) => (!lessThan ? zod : zod.lt(lessThan)),
(zod) => (!integer ? zod : zod.int()),
(zod) => (!positive ? zod : zod.nonnegative()),
(zod) => (!negative ? zod : zod.negative())
),
zodFn
)(z.number()),
(zod) =>
!precision
? zod
: zod.transform(
(value) =>
Math.round(value * 10 ** precision) / 10 ** precision
),
(zod) => zod as unknown as z.ZodType<Input, any, Input>
)(z.number())
: zodUnion(
listToListValues<Input>(list).map((value) => z.literal(value))
)
),
schema: () => ({
...def,
options,
type: "number",
validation: flow(
flow(
(rule: Rule<number>) => (!min ? rule : rule.min(min)),
(rule: Rule<Input>) => (!min ? rule : rule.min(min)),
(rule) => (!max ? rule : rule.max(max)),
(rule) => (!greaterThan ? rule : rule.greaterThan(greaterThan)),
(rule) => (!lessThan ? rule : rule.lessThan(lessThan)),
Expand Down
21 changes: 21 additions & 0 deletions src/string/index.spec.ts
Expand Up @@ -181,4 +181,25 @@ describe("string", () => {

expect(rule.custom).toHaveBeenCalledWith(expect.any(Function));
});

it("types values from list", () => {
const type = string({
options: {
list: ["foo", { title: "Bar", value: "bar" }],
},
});

const value: ValidateShape<InferInput<typeof type>, "foo" | "bar"> = "foo";
const parsedValue: ValidateShape<
InferOutput<typeof type>,
"foo" | "bar"
> = type.parse(value);

expect(parsedValue).toEqual(value);
expect(["foo", "bar"]).toContain(type.mock(faker));

expect(() => {
type.parse("fo");
}).toThrow(z.ZodError);
});
});
41 changes: 28 additions & 13 deletions src/string/index.ts
@@ -1,40 +1,55 @@
import { flow } from "lodash/fp";
import { z } from "zod";

import { createType } from "../types";
import { listMock, listToListValues } from "../list";
import { createType, zodUnion } from "../types";

import type { WithTypedOptionsList } from "../list";
import type { Rule, SanityTypeDef } from "../types";
import type { Schema } from "@sanity/types";

export const string = <Output = string>({
export const string = <Input extends string, Output = Input>({
length,
max,
min,
mock = (faker) => faker.random.word(),
options,
regex,
validation,
zod: zodFn = (zod) => zod as unknown as z.ZodType<Output, any, string>,
options: { list = undefined } = {},
mock = listMock(list, (faker) => faker.random.word() as Input),
zod: zodFn = (zod) => zod as unknown as z.ZodType<Output, any, Input>,
...def
}: SanityTypeDef<Schema.StringDefinition, z.ZodString, Output> & {
}: SanityTypeDef<
WithTypedOptionsList<Input, Schema.StringDefinition>,
z.ZodType<Input, any, Input>,
Output
> & {
length?: number;
max?: number;
min?: number;
regex?: RegExp;
} = {}) =>
createType({
mock,
zod: flow(
(zod: z.ZodString) => (!min ? zod : zod.min(min)),
(zod) => (!max ? zod : zod.max(max)),
(zod) => (!length ? zod : zod.length(length)),
(zod) => (!regex ? zod : zod.regex(regex)),
zodFn
)(z.string()),
zod: zodFn(
!list?.length
? flow(
(zod: z.ZodString) => (!min ? zod : zod.min(min)),
(zod) => (!max ? zod : zod.max(max)),
(zod) => (!length ? zod : zod.length(length)),
(zod) => (!regex ? zod : zod.regex(regex)),
(zod) => zod as unknown as z.ZodType<Input, any, Input>
)(z.string())
: zodUnion(
listToListValues<Input>(list).map((value) => z.literal(value))
)
),
schema: () => ({
...def,
options,
type: "string",
validation: flow(
(rule: Rule<string>) => (!min ? rule : rule.min(min)),
(rule: Rule<Input>) => (!min ? rule : rule.min(min)),
(rule) => (!max ? rule : rule.max(max)),
(rule) => (!length ? rule : rule.length(length)),
(rule) => (!regex ? rule : rule.regex(regex)),
Expand Down
14 changes: 13 additions & 1 deletion src/types.ts
@@ -1,13 +1,25 @@
import { z } from "zod";

import type { Faker } from "@faker-js/faker";
import type {
CustomValidator,
Rule as RuleWithoutTypedCustom,
} from "@sanity/types";
import type { Merge, PartialDeep, Promisable, SetOptional } from "type-fest";
import type { z } from "zod";

export type AnyObject = Record<string, unknown>;

export const zodUnion = <Zods extends z.ZodType<any, any, any>>(zods: Zods[]) =>
zods.length === 0
? (z.never() as unknown as Zods)
: zods.length === 1
? (zods[0]! as Zods)
: (z.union([
zods[0]! as Zods,
zods[1]! as Zods,
...zods.slice(2),
]) as unknown as Zods);

// TODO Type Definition across the board
export interface SanityType<
Definition,
Expand Down

0 comments on commit 69a4018

Please sign in to comment.