Skip to content

Commit

Permalink
chore(zui): standardize the way transforms throw errors (#413)
Browse files Browse the repository at this point in the history
  • Loading branch information
franklevasseur authored Oct 10, 2024
1 parent acb2673 commit e70a098
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 72 deletions.
10 changes: 4 additions & 6 deletions zui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { jsonSchemaToZui } from './transforms/json-schema-to-zui'
import { zuiToJsonSchema } from './transforms/zui-to-json-schema'
import { objectToZui } from './transforms/object-to-zui'
import {
toTypescript,
UntitledDeclarationError,
TypescriptGenerationOptions,
} from './transforms/zui-to-typescript-type'
import { toTypescript, TypescriptGenerationOptions } from './transforms/zui-to-typescript-type'
import { toTypescriptSchema } from './transforms/zui-to-typescript-schema'
import * as transformErrors from './transforms/common/errors'

export * from './ui'
export * from './z'

export const transforms = {
errors: transformErrors,
jsonSchemaToZui,
zuiToJsonSchema,
objectToZui,
toTypescript,
toTypescriptSchema,
}

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

type Transform =
| 'json-schema-to-zui'
| 'object-to-zui'
| 'zui-to-json-schema'
| 'zui-to-typescript-schema'
| 'zui-to-typescript-type'

export abstract class ZuiTransformError extends Error {
public constructor(
public readonly transform: Transform,
message?: string,
) {
super(message)
}
}

// json-schema-to-zui-error
export class JsonSchemaToZuiError extends ZuiTransformError {
public constructor(message?: string) {
super('json-schema-to-zui', message)
}
}

// object-to-zui-error
export class ObjectToZuiError extends ZuiTransformError {
public constructor(message?: string) {
super('object-to-zui', message)
}
}

// zui-to-json-schema-error
export class ZuiToJsonSchemaError extends ZuiTransformError {
public constructor(message?: string) {
super('zui-to-json-schema', message)
}
}
export class UnsupportedZuiToJsonSchemaError extends ZuiToJsonSchemaError {
public constructor(type: ZodFirstPartyTypeKind) {
super(`Zod type ${type} cannot be transformed to JSON Schema.`)
}
}

// zui-to-typescript-schema-error
export class ZuiToTypescriptSchemaError extends ZuiTransformError {
public constructor(message?: string) {
super('zui-to-typescript-schema', message)
}
}
export class UnsupportedZuiToTypescriptSchemaError extends ZuiToTypescriptSchemaError {
public constructor(type: ZodFirstPartyTypeKind) {
super(`Zod type ${type} cannot be transformed to TypeScript schema.`)
}
}

// zui-to-typescript-type-error
export class ZuiToTypescriptTypeError extends ZuiTransformError {
public constructor(message?: string) {
super('zui-to-typescript-type', message)
}
}
export class UnsupportedZuiToTypescriptTypeError extends ZuiToTypescriptTypeError {
public constructor(type: ZodFirstPartyTypeKind | string) {
super(`Zod type ${type} cannot be transformed to TypeScript type.`)
}
}

export class UntitledDeclarationError extends ZuiToTypescriptTypeError {
public constructor() {
super('Schema must have a title to be transformed to a TypeScript type with a declaration.')
}
}

export class UnrepresentableGenericError extends ZuiToTypescriptTypeError {
public constructor() {
super(`${ZodFirstPartyTypeKind.ZodRef} can only be transformed to a TypeScript type with a "type" declaration.`)
}
}
32 changes: 24 additions & 8 deletions zui/src/transforms/common/eval-zui-string.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
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 type EvalZuiStringResult =
| {
sucess: true
value: ZodTypeAny
}
| {
sucess: false
error: string
}

export const evalZuiString = (zuiString: string): EvalZuiStringResult => {
let result: any

try {
result = new Function('z', `return ${zuiString}`)(z)
} catch (thrown) {
const err = thrown instanceof Error ? thrown : new Error(String(thrown))
return { sucess: false, error: `Failed to evaluate schema: ${err.message}` }
}
}

export const evalZuiString = (zuiString: string): ZodTypeAny => {
const result = new Function('z', `return ${zuiString}`)(z)
if (!(result instanceof z.ZodType)) {
throw new InvalidZuiStringError(zuiString)
return { sucess: false, error: `String "${zuiString}" does not evaluate to a Zod schema` }
}

return {
sucess: true,
value: result,
}
return result
}
9 changes: 7 additions & 2 deletions zui/src/transforms/json-schema-to-zui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { parseSchema } from './parsers/parseSchema'
import { ZuiExtensionObject } from '../../ui/types'
import { JSONSchemaExtended } from './types'
import { evalZuiString } from '../common/eval-zui-string'
import * as errors from '../common/errors'

export const jsonSchemaToZodStr = (schema: JSONSchemaExtended): string => {
return parseSchema(schema, {
Expand All @@ -36,7 +37,11 @@ export const jsonSchemaToZodStr = (schema: JSONSchemaExtended): string => {
const jsonSchemaToZod = (schema: any): ZodTypeAny => {
let code = jsonSchemaToZodStr(schema)
code = code.replaceAll('errors: z.ZodError[]', 'errors')
return evalZuiString(code)
const evaluationResult = evalZuiString(code)
if (!evaluationResult.sucess) {
throw new errors.JsonSchemaToZuiError(evaluationResult.error)
}
return evaluationResult.value
}

const applyZuiPropsRecursively = (zodField: ZodTypeAny, jsonSchemaField: any) => {
Expand Down Expand Up @@ -226,7 +231,7 @@ export const traverseZodDefinitions = (
cb(ZodFirstPartyTypeKind.ZodDefault, def, path)
break
default:
throw new Error(`Unknown Zod type: ${(def as any).typeName}`)
throw new errors.JsonSchemaToZuiError(`Unknown Zod type: ${(def as any).typeName}`)
}
}

Expand Down
5 changes: 3 additions & 2 deletions zui/src/transforms/object-to-zui/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z, SomeZodObject, ZodTypeAny } from '../../z/index'
import * as errors from '../common/errors'

// Using a basic regex do determine if it's a date or not to avoid using another lib for that
const dateTimeRegex =
Expand All @@ -8,7 +9,7 @@ export type ObjectToZuiOptions = { optional?: boolean; nullable?: boolean; passt

export const objectToZui = (obj: any, opts?: ObjectToZuiOptions, isRoot = true): ZodTypeAny => {
if (typeof obj !== 'object') {
throw new Error('Input must be an object')
throw new errors.ObjectToZuiError('Input must be an object')
}

const applyOptions = (zodType: any) => {
Expand Down Expand Up @@ -53,7 +54,7 @@ export const objectToZui = (obj: any, opts?: ObjectToZuiOptions, isRoot = true):
}
break
default:
throw new Error(`Unsupported type for key ${key}`)
throw new errors.ObjectToZuiError(`Unsupported type for key ${key}`)
}
}
return acc
Expand Down
3 changes: 2 additions & 1 deletion zui/src/transforms/zui-to-json-schema/parseDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Refs, Seen } from './Refs'
import { parseReadonlyDef } from './parsers/readonly'
import { zuiKey } from '../../ui/constants'
import { JsonSchema7RefType, parseRefDef } from './parsers/ref'
import * as errors from '../common/errors'

type JsonSchema7Meta = {
default?: any
Expand Down Expand Up @@ -204,7 +205,7 @@ const selectParser = (def: any, typeName: ZodFirstPartyTypeKind, refs: Refs): Js
case ZodFirstPartyTypeKind.ZodPipeline:
return parsePipelineDef(def, refs)
case ZodFirstPartyTypeKind.ZodTemplateLiteral:
throw new Error('Template literals are not supported yet')
throw new errors.UnsupportedZuiToJsonSchemaError(ZodFirstPartyTypeKind.ZodTemplateLiteral)
case ZodFirstPartyTypeKind.ZodFunction:
case ZodFirstPartyTypeKind.ZodVoid:
case ZodFirstPartyTypeKind.ZodSymbol:
Expand Down
17 changes: 12 additions & 5 deletions zui/src/transforms/zui-to-typescript-schema/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { describe, expect } from 'vitest'
import { toTypescriptSchema as toTypescript } from '.'
import { evalZuiString } from '../common/eval-zui-string'
import * as errors from '../common/errors'

const assert = (_expected: string) => ({
toGenerateItself: async () => {
const schema = evalZuiString(_expected)
const actual = toTypescript(schema)
const evalResult = evalZuiString(_expected)
if (!evalResult.sucess) {
throw new Error(evalResult.error)
}
const actual = toTypescript(evalResult.value)
await expect(actual).toMatchWithoutFormatting(_expected)
},
toThrowErrorWhenGenerating: async () => {
const schema = evalZuiString(_expected)
const fn = () => toTypescript(schema)
expect(fn).toThrowError()
const evalResult = evalZuiString(_expected)
if (!evalResult.sucess) {
throw new Error(evalResult.error)
}
const fn = () => toTypescript(evalResult.value)
expect(fn).toThrowError(errors.ZuiToTypescriptSchemaError)
},
})

Expand Down
17 changes: 8 additions & 9 deletions zui/src/transforms/zui-to-typescript-schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import z, { util, ZodError } from '../../z'
import { escapeString, getMultilineComment } from '../zui-to-typescript-type/utils'
import { mapValues, toTypesriptPrimitive } from './utils'

export type TypescriptExpressionGenerationOptions = {}
import * as errors from '../common/errors'

/**
*
* @param schema zui schema
* @param options generation options
* @returns a typescript program that would construct the given schema if executed
*/
export function toTypescriptSchema(schema: z.Schema, _options?: TypescriptExpressionGenerationOptions): string {
export function toTypescriptSchema(schema: z.Schema): string {
let wrappedSchema: z.Schema = schema
let dts = sUnwrapZod(wrappedSchema)
return dts
Expand Down Expand Up @@ -125,10 +124,10 @@ function sUnwrapZod(schema: z.Schema): string {
return `${getMultilineComment(def.description)}z.enum([${values.join(', ')}])`.trim()

case z.ZodFirstPartyTypeKind.ZodEffects:
throw new Error('ZodEffects cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodEffects)

case z.ZodFirstPartyTypeKind.ZodNativeEnum:
throw new Error('ZodNativeEnum cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodNativeEnum)

case z.ZodFirstPartyTypeKind.ZodOptional:
return `${getMultilineComment(def.description)}z.optional(${sUnwrapZod(def.innerType)})`.trim()
Expand All @@ -149,13 +148,13 @@ function sUnwrapZod(schema: z.Schema): string {
return `${getMultilineComment(def.description)}z.promise(${sUnwrapZod(def.type)})`.trim()

case z.ZodFirstPartyTypeKind.ZodBranded:
throw new Error('ZodBranded cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodBranded)

case z.ZodFirstPartyTypeKind.ZodPipeline:
throw new Error('ZodPipeline cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodPipeline)

case z.ZodFirstPartyTypeKind.ZodSymbol:
throw new Error('ZodSymbol cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodSymbol)

case z.ZodFirstPartyTypeKind.ZodReadonly:
return `${getMultilineComment(def.description)}z.readonly(${sUnwrapZod(def.innerType)})`.trim()
Expand All @@ -165,7 +164,7 @@ function sUnwrapZod(schema: z.Schema): string {
return `${getMultilineComment(def.description)}z.ref(${uri})`.trim()

case z.ZodFirstPartyTypeKind.ZodTemplateLiteral:
throw new Error('ZodTemplateLiteral cannot be transformed to TypeScript expression yet')
throw new errors.UnsupportedZuiToTypescriptSchemaError(z.ZodFirstPartyTypeKind.ZodTemplateLiteral)

default:
util.assertNever(def)
Expand Down
13 changes: 7 additions & 6 deletions zui/src/transforms/zui-to-typescript-type/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest'
import { UnrepresentableGenericError, UntitledDeclarationError, toTypescript as toTs } from '.'
import { toTypescript as toTs } from '.'
import z, { ZodType } from '../../z'
import * as errors from '../common/errors'

const toTypescript = (schema: ZodType): string => {
const hasTitle = 'title' in schema.ui
Expand All @@ -17,7 +18,7 @@ describe.concurrent('functions', () => {
.args(z.object({ a: z.number(), b: z.number() }))
.returns(z.number())
.describe('Add two numbers together.\nThis is a multiline description')
expect(() => toTs(fn, { declaration: true })).toThrowError(UntitledDeclarationError)
expect(() => toTs(fn, { declaration: true })).toThrowError(errors.ZuiToTypescriptTypeError)
})

it('type delcaration works', async () => {
Expand Down Expand Up @@ -579,10 +580,10 @@ describe.concurrent('objects', () => {
describe.concurrent('generics', () => {
it("can't generate a generic type without type declaration", async () => {
const schema = z.object({ a: z.string(), b: z.ref('T') }).title('MyObject')
expect(() => toTs(schema, { declaration: true })).toThrowError(UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: false })).toThrowError(UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: 'variable' })).toThrowError(UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: 'none' })).toThrowError(UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: true })).toThrowError(errors.UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: false })).toThrowError(errors.UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: 'variable' })).toThrowError(errors.UnrepresentableGenericError)
expect(() => toTs(schema, { declaration: 'none' })).toThrowError(errors.UnrepresentableGenericError)
})

it('can generate a generic type', async () => {
Expand Down
Loading

0 comments on commit e70a098

Please sign in to comment.