Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(zui): toTypescriptSchema transform supports checks for most types like string and number #557

Merged
merged 3 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion zui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bpinternal/zui",
"version": "0.15.0",
"version": "0.15.1",
"description": "A fork of Zod with additional features",
"type": "module",
"source": "./src/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions zui/src/transforms/common/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ type BaseZuiJsonSchema<Def extends Partial<z.ZodDef> = {}> = util.Satisfies<
type _StringSchema = util.Satisfies<{ type: 'string' }, JSONSchema7> // TODO: support all string checks
type _NumberSchema = util.Satisfies<{ type: 'number' | 'integer' }, JSONSchema7> // TODO: support all number checks
type _BigIntSchema = util.Satisfies<{ type: 'integer' }, JSONSchema7> // TODO: support all bigint checks
type _BooleanSchema = util.Satisfies<{ type: 'boolean' }, JSONSchema7> // TODO: support all boolean checks
type _DateSchema = util.Satisfies<{ type: 'string'; format: 'date-time' }, JSONSchema7>
type _BooleanSchema = util.Satisfies<{ type: 'boolean' }, JSONSchema7>
type _DateSchema = util.Satisfies<{ type: 'string'; format: 'date-time' }, JSONSchema7> // TODO: support all date checks
type _NullSchema = util.Satisfies<{ type: 'null' }, JSONSchema7>
type _UndefinedSchema = util.Satisfies<{ not: true }, JSONSchema7>
type _NeverSchema = util.Satisfies<{ not: true }, JSONSchema7>
Expand Down
19 changes: 19 additions & 0 deletions zui/src/transforms/zui-to-typescript-schema/array-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { primitiveToTypescriptValue as toTs } from '../common/utils'
import { ZodArrayDef } from '../../z/types/array'

export const generateArrayChecks = (def: ZodArrayDef): string => {
const checks: string[] = []
if (def.exactLength) {
const { value, message } = def.exactLength
checks.push(`.length(${toTs(value)}, ${toTs(message)})`)
}
if (def.minLength) {
const { value, message } = def.minLength
checks.push(`.min(${toTs(value)}, ${toTs(message)})`)
}
if (def.maxLength) {
const { value, message } = def.maxLength
checks.push(`.max(${toTs(value)}, ${toTs(message)})`)
}
return checks.join('')
}
25 changes: 25 additions & 0 deletions zui/src/transforms/zui-to-typescript-schema/bigint-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { primitiveToTypescriptValue as toTs } from '../common/utils'
import { ZodBigIntCheck, ZodBigIntDef } from '../../z/types/bigint'
import { util } from '../../z'

export const generateBigIntChecks = (def: ZodBigIntDef): string => {
const checks = def.checks
if (checks.length === 0) {
return ''
}
return checks.map(_generateBigIntCheck).join('')
}

const _generateBigIntCheck = (check: ZodBigIntCheck): string => {
switch (check.kind) {
case 'min':
return `.min(${toTs(check.value)}, ${toTs(check.message)})`
case 'max':
return `.max(${toTs(check.value)}, ${toTs(check.message)})`
case 'multipleOf':
return `.multipleOf(${toTs(check.value)}, ${toTs(check.message)})`
default:
type _assertion = util.AssertNever<typeof check>
return ''
}
}
29 changes: 29 additions & 0 deletions zui/src/transforms/zui-to-typescript-schema/date-checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { primitiveToTypescriptValue as toTs } from '../common/utils'
import { ZodDateCheck, ZodDateDef } from '../../z/types/date'
import { util } from '../../z'

export const generateDateChecks = (def: ZodDateDef): string => {
const checks = def.checks
if (checks.length === 0) {
return ''
}
return checks.map(_generateDateCheck).join('')
}

const _generateDateCheck = (check: ZodDateCheck): string => {
switch (check.kind) {
case 'min':
const minDate = dateTs(check.value)
return `.min(${minDate}, ${toTs(check.message)})`
case 'max':
const maxDate = dateTs(check.value)
return `.max(${maxDate}, ${toTs(check.message)})`
default:
type _assertion = util.AssertNever<typeof check>
return ''
}
}

const dateTs = (d: number): string => {
return `new Date(${d})`
}
197 changes: 178 additions & 19 deletions zui/src/transforms/zui-to-typescript-schema/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const assert = (source: ZodType) => ({
toGenerateItself() {
const actual = toTypescript(source)
const destination = evalZui(actual)
expect(source.isEqual(destination)).toBe(true)
let msg: string | undefined
try {
msg = `Expected ${JSON.stringify(source._def)} to equal ${JSON.stringify(destination._def)}`
} catch {}
expect(source.isEqual(destination), msg).toBe(true)
},
toThrowErrorWhenGenerating() {
const fn = () => toTypescript(source)
Expand All @@ -26,29 +30,155 @@ const assert = (source: ZodType) => ({
})

describe.concurrent('toTypescriptZuiString', () => {
test('string', () => {
const schema = z.string()
assert(schema).toGenerateItself()
describe.concurrent('string', () => {
test('no checks', () => {
const schema = z.string()
assert(schema).toGenerateItself()
})
test('min', () => {
const schema = z.string().min(42)
assert(schema).toGenerateItself()
})
test('max', () => {
const schema = z.string().max(42)
assert(schema).toGenerateItself()
})
test('length', () => {
const schema = z.string().length(42)
assert(schema).toGenerateItself()
})
test('email', () => {
const schema = z.string().email()
assert(schema).toGenerateItself()
})
test('url', () => {
const schema = z.string().url()
assert(schema).toGenerateItself()
})
test('emoji', () => {
const schema = z.string().emoji()
assert(schema).toGenerateItself()
})
test('uuid', () => {
const schema = z.string().uuid()
assert(schema).toGenerateItself()
})
test('cuid', () => {
const schema = z.string().cuid()
assert(schema).toGenerateItself()
})
test('cuid2', () => {
const schema = z.string().cuid2()
assert(schema).toGenerateItself()
})
test('ulid', () => {
const schema = z.string().ulid()
assert(schema).toGenerateItself()
})
test('includes', () => {
const schema = z.string().includes('banana')
assert(schema).toGenerateItself()
})
test('startsWith', () => {
const schema = z.string().startsWith('banana')
assert(schema).toGenerateItself()
})
test('endsWith', () => {
const schema = z.string().endsWith('banana')
assert(schema).toGenerateItself()
})
test('regex', () => {
const schema = z.string().regex(/banana/g)
assert(schema).toGenerateItself()
})
test('trim', () => {
const schema = z.string().trim()
assert(schema).toGenerateItself()
})
test('toLowerCase', () => {
const schema = z.string().toLowerCase()
assert(schema).toGenerateItself()
})
test('toUpperCase', () => {
const schema = z.string().toUpperCase()
assert(schema).toGenerateItself()
})
test('datetime', () => {
const schema = z.string().datetime()
assert(schema).toGenerateItself()
})
test('ip', () => {
const schema = z.string().ip()
assert(schema).toGenerateItself()
})
})
test('number', () => {
const schema = z.number()
assert(schema).toGenerateItself()
describe.concurrent('number', () => {
test('no checks', () => {
const schema = z.number()
assert(schema).toGenerateItself()
})
test('min', () => {
const schema = z.number().min(42)
assert(schema).toGenerateItself()
})
test('max', () => {
const schema = z.number().max(42)
assert(schema).toGenerateItself()
})
test('int', () => {
const schema = z.number().int()
assert(schema).toGenerateItself()
})
test('multipleOf', () => {
const schema = z.number().multipleOf(42)
assert(schema).toGenerateItself()
})
test('finite', () => {
const schema = z.number().finite()
assert(schema).toGenerateItself()
})
})
test('nan', () => {
const schema = z.nan()
assert(schema).toGenerateItself()
})
test('bigint', () => {
const schema = z.bigint()
assert(schema).toGenerateItself()
describe.concurrent('bigint', () => {
test('no checks', () => {
const schema = z.bigint()
assert(schema).toGenerateItself()
})
test('min', () => {
const schema = z.bigint().min(BigInt(42))
assert(schema).toGenerateItself()
})
test('max', () => {
const schema = z.bigint().min(BigInt(42))
assert(schema).toGenerateItself()
})
test('multipleOf', () => {
const schema = z.bigint().min(BigInt(42))
assert(schema).toGenerateItself()
})
})
test('boolean', () => {
const schema = z.boolean()
assert(schema).toGenerateItself()
})
test('date', () => {
const schema = z.date()
assert(schema).toGenerateItself()
describe.concurrent('date', () => {
test('no checks', () => {
const schema = z.date()
assert(schema).toGenerateItself()
})

test('min', () => {
const schema = z.date().min(new Date())
assert(schema).toGenerateItself()
})

test('max', () => {
const schema = z.date().max(new Date())
assert(schema).toGenerateItself()
})
})
test('undefined', () => {
const schema = z.undefined()
Expand All @@ -74,9 +204,26 @@ describe.concurrent('toTypescriptZuiString', () => {
const schema = z.void()
assert(schema).toGenerateItself()
})
test('array', () => {
const schema = z.array(z.string())
assert(schema).toGenerateItself()
describe.concurrent('array', () => {
test('no checks', () => {
const schema = z.array(z.string())
assert(schema).toGenerateItself()
})

test('min', () => {
const schema = z.array(z.string()).min(42)
assert(schema).toGenerateItself()
})

test('max', () => {
const schema = z.array(z.string()).max(42)
assert(schema).toGenerateItself()
})

test('length', () => {
const schema = z.array(z.string()).length(42)
assert(schema).toGenerateItself()
})
})
test('object', () => {
const schema = z.object({
Expand Down Expand Up @@ -112,9 +259,21 @@ describe.concurrent('toTypescriptZuiString', () => {
const schema = z.map(z.string(), z.number())
assert(schema).toGenerateItself()
})
test('set', () => {
const schema = z.set(z.string())
assert(schema).toGenerateItself()
describe.concurrent('set', () => {
test('no checks', () => {
const schema = z.set(z.string())
assert(schema).toGenerateItself()
})

test('min', () => {
const schema = z.set(z.string()).min(42)
assert(schema).toGenerateItself()
})

test('max', () => {
const schema = z.set(z.string()).max(42)
assert(schema).toGenerateItself()
})
})
test('function with no argument', () => {
const schema = z.function().returns(z.void())
Expand Down
18 changes: 12 additions & 6 deletions zui/src/transforms/zui-to-typescript-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {
} from '../common/utils'
import * as errors from '../common/errors'
import { zuiKey } from '../../ui/constants'
import { generateStringChecks } from './string-checks'
import { generateNumberChecks } from './number-checks'
import { generateBigIntChecks } from './bigint-checks'
import { generateDateChecks } from './date-checks'
import { generateArrayChecks } from './array-checks'
import { generateSetChecks } from './set-checks'

/**
*
Expand All @@ -27,22 +33,22 @@ function sUnwrapZod(schema: z.Schema): string {

switch (def.typeName) {
case z.ZodFirstPartyTypeKind.ZodString:
return `z.string()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
return `z.string()${generateStringChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodNumber:
return `z.number()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
return `z.number()${generateNumberChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodNaN:
return `z.nan()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodBigInt:
return `z.bigint()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
return `z.bigint()${generateBigIntChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodBoolean:
return `z.boolean()${_addZuiExtensions(def)}${_maybeDescribe(schema._def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodDate:
return `z.date()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
return `z.date()${generateDateChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodUndefined:
return `z.undefined()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
Expand All @@ -63,7 +69,7 @@ function sUnwrapZod(schema: z.Schema): string {
return `z.void()${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodArray:
return `z.array(${sUnwrapZod(def.type)})${_addZuiExtensions(def)}${_maybeDescribe(def)}`
return `z.array(${sUnwrapZod(def.type)})${generateArrayChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`

case z.ZodFirstPartyTypeKind.ZodObject:
const props = mapValues(def.shape(), (value) => {
Expand Down Expand Up @@ -110,7 +116,7 @@ function sUnwrapZod(schema: z.Schema): string {
return `z.map(${mapKeyType}, ${mapValueType})${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodSet:
return `z.set(${sUnwrapZod(def.valueType)})${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()
return `z.set(${sUnwrapZod(def.valueType)})${generateSetChecks(def)}${_addZuiExtensions(def)}${_maybeDescribe(def)}`.trim()

case z.ZodFirstPartyTypeKind.ZodFunction:
const args = def.args.items.map(sUnwrapZod)
Expand Down
Loading