Skip to content

Commit

Permalink
use constructor.name-based instanceof helper
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed May 31, 2024
1 parent 8a36dd8 commit 495e875
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 13 deletions.
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {type JsonSchema7Type} from 'zod-to-json-schema'
import * as zodValidationError from 'zod-validation-error'
import {flattenedProperties, incompatiblePropertyPairs, getDescription} from './json-schema'
import {lineByLineConsoleLogger} from './logging'
import {looksLikeInstanceof} from './uitl'
import {Logger, TrpcCliParams} from './types'
import {parseProcedureInputs} from './zod-procedure'

Expand Down Expand Up @@ -181,9 +182,9 @@ export const trpcCli = <R extends AnyRouter>({router, ...params}: TrpcCliParams<
type Fail = (message: string, options?: {cause?: unknown; help?: boolean}) => never

function transformError(err: unknown, fail: Fail): unknown {
if (err instanceof TRPCError) {
if (looksLikeInstanceof(err, TRPCError)) {
const cause = err.cause
if (cause instanceof ZodError) {
if (looksLikeInstanceof(cause, ZodError)) {
const originalIssues = cause.issues
try {
cause.issues = cause.issues.map(issue => {
Expand Down
17 changes: 17 additions & 0 deletions src/uitl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Pretty much like the `instanceof` operator, but should work across different realms. Necessary for zod because some installations
* might result in this library using the commonjs zod export, while the user's code uses the esm export.
* https://github.com/mmkal/trpc-cli/issues/7
*
* Tradeoff: It's possible that this function will return false positives if the target class has the same name as an unrelated class in the current realm.
* So, only use it for classes that are unlikely to have name conflicts like `ZodAbc` or `TRPCDef`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const looksLikeInstanceof = <T>(value: unknown, target: new (...args: any[]) => T): value is T => {
let current = value?.constructor
do {
if (current?.name === target.name) return true
current = Object.getPrototypeOf(current) as Function
} while (current?.name)
return false
}
26 changes: 15 additions & 11 deletions src/zod-procedure.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {z} from 'zod'
import zodToJsonSchema from 'zod-to-json-schema'
import type {Result, ParsedProcedure} from './types'
import {looksLikeInstanceof} from './uitl'

function getInnerType(zodType: z.ZodType): z.ZodType {
if (zodType instanceof z.ZodOptional || zodType instanceof z.ZodNullable) {
if (looksLikeInstanceof(zodType, z.ZodOptional) || looksLikeInstanceof(zodType, z.ZodNullable)) {
return getInnerType(zodType._def.innerType as z.ZodType)
}
if (zodType instanceof z.ZodEffects) {
if (looksLikeInstanceof(zodType, z.ZodEffects)) {
return getInnerType(zodType.innerType() as z.ZodType)
}
return zodType
Expand All @@ -20,7 +21,9 @@ export function parseProcedureInputs(inputs: unknown[]): Result<ParsedProcedure>
}
}

const allZodTypes = inputs.every(input => input instanceof z.ZodType)
const allZodTypes = inputs.every(input =>
looksLikeInstanceof(input, z.ZodType as new (...args: unknown[]) => z.ZodType),
)
if (!allZodTypes) {
return {
success: false,
Expand All @@ -38,7 +41,7 @@ export function parseProcedureInputs(inputs: unknown[]): Result<ParsedProcedure>
return parseLiteralInput(mergedSchema)
}

if (mergedSchema instanceof z.ZodTuple) {
if (looksLikeInstanceof(mergedSchema, z.ZodTuple)) {
return parseTupleInput(mergedSchema as z.ZodTuple<never>)
}

Expand Down Expand Up @@ -222,16 +225,17 @@ const parameterName = (s: z.ZodType, position: number) => {
* acceptsString(z.intersection(z.string(), z.number())) // false
* acceptsString(z.intersection(z.string(), z.string().max(10))) // true
*/
export function accepts(target: z.ZodType) {
export function accepts<ZodTarget extends z.ZodType>(target: ZodTarget) {
const test = (zodType: z.ZodType): boolean => {
const innerType = getInnerType(zodType)
if (innerType instanceof target.constructor) return true
if (innerType instanceof z.ZodLiteral) return target.safeParse(innerType.value).success
if (innerType instanceof z.ZodEnum) return (innerType.options as unknown[]).some(o => target.safeParse(o).success)
if (innerType instanceof z.ZodUnion) return (innerType.options as z.ZodType[]).some(test)
if (innerType instanceof z.ZodIntersection)
if (looksLikeInstanceof(innerType, target.constructor as new (...args: unknown[]) => ZodTarget)) return true
if (looksLikeInstanceof(innerType, z.ZodLiteral)) return target.safeParse(innerType.value).success
if (looksLikeInstanceof(innerType, z.ZodEnum))
return (innerType.options as unknown[]).some(o => target.safeParse(o).success)
if (looksLikeInstanceof(innerType, z.ZodUnion)) return innerType.options.some(test)
if (looksLikeInstanceof(innerType, z.ZodIntersection))
return test(innerType._def.left as z.ZodType) && test(innerType._def.right as z.ZodType)
if (innerType instanceof z.ZodEffects) return test(innerType.innerType() as z.ZodType)
if (looksLikeInstanceof(innerType, z.ZodEffects)) return test(innerType.innerType() as z.ZodType)
return false
}
return test
Expand Down

0 comments on commit 495e875

Please sign in to comment.