Skip to content

Commit

Permalink
feat(zui): add toTypescriptSchema transform (#385)
Browse files Browse the repository at this point in the history
  • Loading branch information
franklevasseur authored Oct 7, 2024
1 parent e238825 commit 1ee96a3
Show file tree
Hide file tree
Showing 33 changed files with 507 additions and 68 deletions.
4 changes: 3 additions & 1 deletion zui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
toTypescript,
UntitledDeclarationError,
TypescriptGenerationOptions,
} from './transforms/zui-to-typescript-next'
} from './transforms/zui-to-typescript-type'
import { toTypescriptSchema } from './transforms/zui-to-typescript-schema'

export * from './ui'
export * from './z'
Expand All @@ -15,6 +16,7 @@ export const transforms = {
zuiToJsonSchema,
objectToZui,
toTypescript,
toTypescriptSchema,
}

export { UntitledDeclarationError, type TypescriptGenerationOptions }
15 changes: 15 additions & 0 deletions zui/src/transforms/common/eval-zui-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import z, { ZodTypeAny } from '../../z'

export class InvalidZuiStringError extends Error {
public constructor(public readonly zuiString: string) {
super(`String "${zuiString}" does not evaluate to a Zod type`)
}
}

export const evalZuiString = (zuiString: string): ZodTypeAny => {
const result = new Function('z', `return ${zuiString}`)(z)
if (!(result instanceof z.ZodType)) {
throw new InvalidZuiStringError(zuiString)
}
return result
}
3 changes: 2 additions & 1 deletion zui/src/transforms/json-schema-to-zui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { JsonSchema7Type } from '../zui-to-json-schema/parseDef'
import { parseSchema } from './parsers/parseSchema'
import { ZuiExtensionObject } from '../../ui/types'
import { JSONSchemaExtended } from './types'
import { evalZuiString } from '../common/eval-zui-string'

export const jsonSchemaToZodStr = (schema: JSONSchemaExtended): string => {
return parseSchema(schema, {
Expand All @@ -35,7 +36,7 @@ export const jsonSchemaToZodStr = (schema: JSONSchemaExtended): string => {
const jsonSchemaToZod = (schema: any): ZodTypeAny => {
let code = jsonSchemaToZodStr(schema)
code = code.replaceAll('errors: z.ZodError[]', 'errors')
return new Function('z', `return ${code}`)(z) as ZodTypeAny
return evalZuiString(code)
}

const applyZuiPropsRecursively = (zodField: ZodTypeAny, jsonSchemaField: any) => {
Expand Down
194 changes: 194 additions & 0 deletions zui/src/transforms/zui-to-typescript-schema/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { describe, expect } from 'vitest'
import { toTypescriptSchema as toTypescript } from '.'
import { evalZuiString } from '../common/eval-zui-string'

const assert = (_expected: string) => ({
toGenerateItself: async () => {
const schema = evalZuiString(_expected)
const actual = toTypescript(schema)
await expect(actual).toMatchWithoutFormatting(_expected)
},
toThrowErrorWhenGenerating: async () => {
const schema = evalZuiString(_expected)
const fn = () => toTypescript(schema)
expect(fn).toThrowError()
},
})

describe('toTypescriptZuiString', () => {
test('string', async () => {
const schema = `z.string()`
await assert(schema).toGenerateItself()
})
test('number', async () => {
const schema = `z.number()`
await assert(schema).toGenerateItself()
})
test('nan', async () => {
const schema = `z.nan()`
await assert(schema).toGenerateItself()
})
test('bigint', async () => {
const schema = `z.bigint()`
await assert(schema).toGenerateItself()
})
test('boolean', async () => {
const schema = `z.boolean()`
await assert(schema).toGenerateItself()
})
test('date', async () => {
const schema = `z.date()`
await assert(schema).toGenerateItself()
})
test('undefined', async () => {
const schema = `z.undefined()`
await assert(schema).toGenerateItself()
})
test('null', async () => {
const schema = `z.null()`
await assert(schema).toGenerateItself()
})
test('any', async () => {
const schema = `z.any()`
await assert(schema).toGenerateItself()
})
test('unknown', async () => {
const schema = `z.unknown()`
await assert(schema).toGenerateItself()
})
test('never', async () => {
const schema = `z.never()`
await assert(schema).toGenerateItself()
})
test('void', async () => {
const schema = `z.void()`
await assert(schema).toGenerateItself()
})
test('array', async () => {
const schema = `z.array(z.string())`
await assert(schema).toGenerateItself()
})
test('object', async () => {
const schema = `z.object({
a: z.string(),
b: z.number(),
})`
await assert(schema).toGenerateItself()
})
test('union', async () => {
const schema = `z.union([z.string(), z.number(), z.boolean()])`
await assert(schema).toGenerateItself()
})
test('discriminatedUnion', async () => {
const schema = `z.discriminatedUnion("type", [
z.object({ type: z.literal("A"), a: z.string() }),
z.object({ type: z.literal("B"), b: z.number() }),
])`
await assert(schema).toGenerateItself()
})
test('intersection', async () => {
const schema = `z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() }))`
await assert(schema).toGenerateItself()
})
test('tuple', async () => {
const schema = `z.tuple([z.string(), z.number()])`
await assert(schema).toGenerateItself()
})
test('record', async () => {
const schema = `z.record(z.string(), z.number())`
await assert(schema).toGenerateItself()
})
test('map', async () => {
const schema = `z.map(z.string(), z.number())`
await assert(schema).toGenerateItself()
})
test('set', async () => {
const schema = `z.set(z.string())`
await assert(schema).toGenerateItself()
})
test('function with no argument', async () => {
const schema = `z.function().returns(z.void())`
await assert(schema).toGenerateItself()
})
test('function with multiple arguments', async () => {
const schema = `z.function().args(z.number(), z.string()).returns(z.boolean())`
await assert(schema).toGenerateItself()
})
test('lazy', async () => {
const schema = `z.lazy(() => z.string())`
await assert(schema).toGenerateItself()
})
test('literal string', async () => {
const schema = `z.literal("banana")`
await assert(schema).toGenerateItself()
})
test('literal number', async () => {
const schema = `z.literal(42)`
await assert(schema).toGenerateItself()
})
test('literal boolean', async () => {
const schema = `z.literal(true)`
await assert(schema).toGenerateItself()
})
test('enum', async () => {
const schema = `z.enum(["banana", "apple", "orange"])`
await assert(schema).toGenerateItself()
})
test('effects', async () => {
const schema = `z.string().transform((s) => s.toUpperCase())`
await assert(schema).toThrowErrorWhenGenerating()
})
test('nativeEnum', async () => {
const schema = `z.nativeEnum({
Banana: 'banana',
Apple: 'apple',
Orange: 'orange',
})
`
await assert(schema).toThrowErrorWhenGenerating()
})
test('optional', async () => {
const schema = `z.optional(z.string())`
await assert(schema).toGenerateItself()
})
test('nullable', async () => {
const schema = `z.nullable(z.string())`
await assert(schema).toGenerateItself()
})
test('default', async () => {
const schema = `z.string().default('banana')` // TODO: should use `z.default(z.string(), 'banana')` for uniformity
await assert(schema).toGenerateItself()
})
test('catch', async () => {
const schema = `z.string().catch('banana')` // TODO: should use `z.catch(z.string(), 'banana')` for uniformity
await assert(schema).toGenerateItself()
})
test('promise', async () => {
const schema = `z.promise(z.string())`
await assert(schema).toGenerateItself()
})
test('branded', async () => {
const schema = `z.string().brand('MyString')` // TODO: should use `z.brand(z.string(), 'MyString')` for uniformity
await assert(schema).toThrowErrorWhenGenerating()
})
test('pipeline', async () => {
const schema = `z.pipeline(z.string(), z.number())`
await assert(schema).toThrowErrorWhenGenerating()
})
test('symbol', async () => {
const schema = `z.symbol()`
await assert(schema).toThrowErrorWhenGenerating()
})
test('readonly', async () => {
const schema = `z.readonly(z.string())`
await assert(schema).toGenerateItself()
})
test('ref', async () => {
const schema = `z.ref("#item")`
await assert(schema).toGenerateItself()
})
test('templateLiteral', async () => {
const schema = `z.templateLiteral()`
await assert(schema).toThrowErrorWhenGenerating()
})
})
Loading

0 comments on commit 1ee96a3

Please sign in to comment.