Skip to content

Commit

Permalink
Support ZodIntersection/ZodEffects/ZodPipeline (kiliman#34, kiliman#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
solomonhawk committed May 2, 2023
1 parent 8121879 commit 6657865
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 38 deletions.
65 changes: 46 additions & 19 deletions example/app/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
ZodDefault,
ZodEffects,
ZodEnum,
ZodIntersection,
ZodLiteral,
ZodNativeEnum,
ZodNumber,
ZodObject,
ZodOptional,
ZodPipeline,
ZodString,
ZodType,
ZodTypeAny,
Expand Down Expand Up @@ -43,11 +45,11 @@ function parseParams(o: any, schema: any, key: string, value: any) {
parseParams(o[parentProp], shape[parentProp], rest.join('.'), value)
return
}
let isArray = false

if (key.includes('[]')) {
isArray = true
key = key.replace('[]', '')
}

const def = shape[key]
if (def) {
processDef(def, o, key, value as string)
Expand All @@ -68,6 +70,7 @@ function getParamsInternal<T>(
} else {
entries = Object.entries(params)
}

for (let [key, value] of entries) {
// infer an empty param as if it wasn't defined in the first place
if (value === '') {
Expand All @@ -92,14 +95,9 @@ function getParamsInternal<T>(
}
}
for (let issue of result.error.issues) {
const { message, path, code, expected, received } = issue
const [key, index] = path
let value = o[key]
let prop = key
if (index !== undefined) {
value = value[index]
prop = `${key}[${index}]`
}
const { message, path } = issue
const [key] = path

addError(key, message)
}
return { success: false, data: undefined, errors }
Expand Down Expand Up @@ -181,11 +179,36 @@ export type InputPropType = {
pattern?: string
}

export function useFormInputProps(schema: any, options: any = {}) {
const shape = schema.shape
const defaultOptions = options
return function props(key: string, options: any = {}) {
options = { ...defaultOptions, ...options }
function getShapeFromZodType(
schema: ZodType,
shape: any = {}
): Record<string, ZodType> {
if (schema instanceof ZodObject) {
for (const key of Object.keys(schema.shape)) {
shape[key] = schema.shape[key]
}

return schema.shape
}

if (schema instanceof ZodEffects) {
return getShapeFromZodType(schema._def.schema, shape)
}

if (schema instanceof ZodIntersection) {
return {
...getShapeFromZodType(schema._def.left, shape),
...getShapeFromZodType(schema._def.right, shape),
}
}

return {}
}

export function useFormInputProps(schema?: ZodType) {
const shape = schema ? getShapeFromZodType(schema) : {}

return function props(key: string) {
const def = shape[key]
if (!def) {
throw new Error(`no such key: ${key}`)
Expand Down Expand Up @@ -234,7 +257,7 @@ function processDef(def: ZodTypeAny, o: any, key: string, value: string) {
}
}

function getInputProps(name: string, def: ZodTypeAny): InputPropType {
function getInputProps(name: string, def: ZodTypeAny, optional: boolean = false): InputPropType {
let type = 'text'
let min, max, minlength, maxlength, pattern
if (def instanceof ZodString) {
Expand All @@ -256,16 +279,20 @@ function getInputProps(name: string, def: ZodTypeAny): InputPropType {
} else if (def instanceof ZodDate) {
type = 'date'
} else if (def instanceof ZodArray) {
return getInputProps(name, def.element)
return getInputProps(name, def.element, optional)
} else if (def instanceof ZodOptional) {
return getInputProps(name, def.unwrap())
return getInputProps(name, def.unwrap(), true)
} else if (def instanceof ZodEffects) {
return getInputProps(name, def._def.schema, optional)
} else if (def instanceof ZodPipeline) {
return getInputProps(name, def._def.out, optional)
}

let inputProps: InputPropType = {
name,
type,
}
if (!def.isOptional()) inputProps.required = true
if (!(def.isOptional() || optional)) inputProps.required = true
if (min) inputProps.min = min
if (max) inputProps.max = max
if (minlength && Number.isFinite(minlength)) inputProps.minLength = minlength
Expand Down
54 changes: 39 additions & 15 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
ZodDefault,
ZodEffects,
ZodEnum,
ZodIntersection,
ZodLiteral,
ZodNativeEnum,
ZodNumber,
ZodObject,
ZodOptional,
ZodPipeline,
ZodString,
ZodType,
ZodTypeAny,
Expand Down Expand Up @@ -43,9 +45,7 @@ function parseParams(o: any, schema: any, key: string, value: any) {
parseParams(o[parentProp], shape[parentProp], rest.join('.'), value)
return
}
let isArray = false
if (key.includes('[]')) {
isArray = true
key = key.replace('[]', '')
}
const def = shape[key]
Expand Down Expand Up @@ -92,14 +92,9 @@ function getParamsInternal<T>(
}
}
for (let issue of result.error.issues) {
const { message, path, code, expected, received } = issue
const [key, index] = path
let value = o[key]
let prop = key
if (index !== undefined) {
value = value[index]
prop = `${key}[${index}]`
}
const { message, path } = issue
const [key] = path

addError(key, message)
}
return { success: false, data: undefined, errors }
Expand Down Expand Up @@ -181,11 +176,36 @@ export type InputPropType = {
pattern?: string
}

export function useFormInputProps(schema: any, options: any = {}) {
const shape = schema.shape
const defaultOptions = options
return function props(key: string, options: any = {}) {
options = { ...defaultOptions, ...options }
function getShapeFromZodType(
schema: ZodType,
shape: any = {}
): Record<string, ZodType> {
if (schema instanceof ZodObject) {
for (const key of Object.keys(schema.shape)) {
shape[key] = schema.shape[key]
}

return schema.shape
}

if (schema instanceof ZodEffects) {
return getShapeFromZodType(schema._def.schema, shape)
}

if (schema instanceof ZodIntersection) {
return {
...getShapeFromZodType(schema._def.left, shape),
...getShapeFromZodType(schema._def.right, shape),
}
}

return {}
}

export function useFormInputProps(schema: ZodType) {
const shape = getShapeFromZodType(schema)

return function props(key: string) {
const def = shape[key]
if (!def) {
throw new Error(`no such key: ${key}`)
Expand Down Expand Up @@ -259,6 +279,10 @@ function getInputProps(name: string, def: ZodTypeAny, optional: boolean = false)
return getInputProps(name, def.element, optional)
} else if (def instanceof ZodOptional) {
return getInputProps(name, def.unwrap(), true)
} else if (def instanceof ZodEffects) {
return getInputProps(name, def._def.schema, optional)
} else if (def instanceof ZodPipeline) {
return getInputProps(name, def._def.out, optional)
}

let inputProps: InputPropType = {
Expand Down
31 changes: 27 additions & 4 deletions test/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ describe('test useFormInputProps', () => {
email: z.string().email(),
url: z.string().url(),
date: z.date(),
coercedDate: z.string().pipe(z.coerce.date()).optional()
})
const inputProps = useFormInputProps(schema)
expect(inputProps('email')).toEqual({
Expand All @@ -317,6 +318,10 @@ describe('test useFormInputProps', () => {
name: 'date',
required: true,
})
expect(inputProps('coercedDate')).toEqual({
type: 'date',
name: 'coercedDate',
})
})
it('should support min/max props', () => {
const schema = z.object({
Expand Down Expand Up @@ -351,6 +356,24 @@ describe('test useFormInputProps', () => {
pattern: '^\\d{5}$',
})
})
it('should support intersection types', () => {
const schema = z.intersection(
z.object({ a: z.string() }),
z.object({ b: z.number() })
)
const inputProps = useFormInputProps(schema)

expect(inputProps('a')).toEqual({
type: 'text',
name: 'a',
required: true
})
expect(inputProps('b')).toEqual({
type: 'number',
name: 'b',
required: true
})
})
})

describe('test nested objects and arrays', () => {
Expand Down Expand Up @@ -429,7 +452,7 @@ describe('test refine schema', () => {
let resultBad = getParams(formData, schema)
expect(resultBad.success).toBe(false)
expect(resultBad.errors?.['num']).toBe(
'Value should be less than or equal to 10',
'Number must be less than or equal to 10',
)

formData.set('num', 'abc')
Expand Down Expand Up @@ -520,7 +543,7 @@ describe('literal values', () => {
const result = getParams(formData, schema)
expect(result.success).toBe(false)
expect(result.errors!['key']).toBe(
'Expected some literal value, received wrong literal value',
'Invalid literal value, expected "some literal value"',
)
})
})
Expand All @@ -537,7 +560,7 @@ describe('array values', () => {
const result = getParams(formData, schema)
expect(result.success).toBe(true)
const { foo } = result.data!
console.log(foo)

expect(foo).toStrictEqual([1, 2])
})
it('should create array if key has [] even for single value', () => {
Expand All @@ -551,7 +574,7 @@ describe('array values', () => {
const result = getParams(formData, schema)
expect(result.success).toBe(true)
const { foo } = result.data!
console.log(result.data)

expect(foo).toStrictEqual([1])
})
})

0 comments on commit 6657865

Please sign in to comment.