Skip to content
20 changes: 15 additions & 5 deletions packages/api/src/parsers/DataItemVariableResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import type {
import { objectMap } from '../util.ts'
import type { VariablesRegistry } from './variablesRegistry.ts'

export const variableStringParser = templateLiteral(['{{', string(), '}}'])
export const variableStringParser = () =>
templateLiteral(['{{', string(), '}}'])
export type VariableStringParser = ZodMiniTemplateLiteral<`{{${string}}}`>

export function DataItemVariableResolverParser(
registry: VariablesRegistry,
parser: $ZodType,
): DataItemVariableResolverParser {
return pipe(
variableStringParser,
variableStringParser(),
transform((input, ctx) =>
stringToVariableResolver(registry, parser, input, ctx)
),
Expand Down Expand Up @@ -65,8 +66,10 @@ export function isVariableResolver(x: unknown): x is VariableResolver {
}

export function resolveVariables(variables: Record<string, any>, x: unknown) {
return typeof x === 'object' && x !== null && !Array.isArray(x)
? recursivelyResolveVariables(variables, x as Record<string, unknown>)
return Array.isArray(x)
? recursivelyResolveArrayVariables(variables, x)
: typeof x === 'object' && x !== null
? recursivelyResolveObjectVariables(variables, x as Record<string, unknown>)
: resolveVariable(variables, x)
}

Expand All @@ -80,7 +83,14 @@ function resolveVariable(variables: Record<string, any>, x: unknown) {
return isVariableResolver(x) ? x(variables) : x
}

function recursivelyResolveVariables(
function recursivelyResolveArrayVariables(
variables: Record<string, any>,
x: unknown[],
): unknown[] {
return x.map((value) => resolveVariables(variables, value))
}

function recursivelyResolveObjectVariables(
variables: Record<string, any>,
x: Record<string, unknown>,
): Record<string, unknown> {
Expand Down
80 changes: 69 additions & 11 deletions packages/api/src/parsers/attachVariableResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type VariableStringParser,
variableStringParser,
} from './DataItemVariableResolver.ts'
import type { $ZodObject, $ZodType } from 'zod/v4/core'
import type { $ZodArray, $ZodObject, $ZodRecord, $ZodType } from 'zod/v4/core'
import { any, type ZodObject } from 'zod'
import { type ZodSwitch, zodSwitch } from './switch.ts'
import type { VariablesRegistry } from './variablesRegistry.ts'
Expand All @@ -31,7 +31,19 @@ export function attachVariableResolver<
return objectVariableResolverParser(
variablesRegistry,
parser as unknown as (ZodObject | ZodMiniObject),
) as any
) as unknown as RecursiveVariableResolver<Parser>

case 'record':
return recordVariableResolverParser(
variablesRegistry,
parser as unknown as $ZodRecord,
) as RecursiveVariableResolver<Parser>

case 'array':
return arrayVariableResolverParser(
variablesRegistry,
parser as unknown as $ZodArray,
) as RecursiveVariableResolver<Parser>

default:
return variableResolverParser(
Expand Down Expand Up @@ -61,13 +73,47 @@ function variableResolverParser<
)
: zodSwitch([
[
variableStringParser,
variableStringParser(),
DataItemVariableResolverParser(variablesRegistry, parser),
],
[any(), parser],
])) as WithVariableResolver<Parser>
}

function arrayVariableResolverParser<Parser extends $ZodArray>(
variablesRegistry: VariablesRegistry,
arrayParser: Parser,
): RecursiveVariableResolver<Parser> {
const $arrayParser = new arrayParser._zod.constr({
...arrayParser._zod.def,
element: attachVariableResolver(
variablesRegistry,
arrayParser._zod.def.element,
),
})
return variableResolverParser(
variablesRegistry,
$arrayParser,
) as RecursiveVariableResolver<Parser>
}

function recordVariableResolverParser<Parser extends $ZodRecord>(
variablesRegistry: VariablesRegistry,
recordParser: Parser,
): RecursiveVariableResolver<Parser> {
const $recordParser = new recordParser._zod.constr({
...recordParser._zod.def,
valueType: attachVariableResolver(
variablesRegistry,
recordParser._zod.def.valueType,
),
})
return variableResolverParser(
variablesRegistry,
$recordParser,
) as RecursiveVariableResolver<Parser>
}

function objectVariableResolverParser<
Parser extends ZodObject | ZodMiniObject,
>(
Expand Down Expand Up @@ -109,16 +155,28 @@ type WithVariableResolver<

export type RecursiveVariableResolver<
Parser extends $ZodType,
> = Parser extends $ZodObject ? WithVariableResolver<
$ZodObject<
{
[K in keyof Parser['_zod']['def']['shape']]: RecursiveVariableResolver<
Parser['_zod']['def']['shape'][K]
>
},
ZodObjectConfig<Parser>
> = Parser extends $ZodArray ? WithVariableResolver<
$ZodArray<
RecursiveVariableResolver<Parser['_zod']['def']['element']>
>
>
: Parser extends $ZodRecord ? WithVariableResolver<
$ZodRecord<
Parser['_zod']['def']['keyType'],
RecursiveVariableResolver<Parser['_zod']['def']['valueType']>
>
>
: Parser extends $ZodObject ? WithVariableResolver<
$ZodObject<
{
[K in keyof Parser['_zod']['def']['shape']]:
RecursiveVariableResolver<
Parser['_zod']['def']['shape'][K]
>
},
ZodObjectConfig<Parser>
>
>
: WithVariableResolver<Parser>

type ZodObjectConfig<T extends $ZodObject> = T extends $ZodObject<any, infer V>
Expand Down
118 changes: 73 additions & 45 deletions packages/api/src/parsers/switch.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,85 @@
import { safeParse, union, type ZodMiniUnion } from 'zod/mini'
import {
custom,
registry,
safeParse,
superRefine,
union,
type ZodMiniCustom,
type ZodMiniUnion,
} from 'zod/mini'
import type { $ZodRegistry, $ZodType } from 'zod/v4/core'
$constructor,
type $ZodCustomDef,
type $ZodCustomInternals,
$ZodType,
type $ZodType as $ZodTypeType,
NEVER,
} from 'zod/v4/core'

/**
* A tuple array representing switch case mappings.
* Each entry is a [condition, parser] pair where the condition determines which parser to use.
*/
export type $ZodSwitchMap = [condition: $ZodType, parser: $ZodType][]
export type $ZodSwitchMap = [condition: $ZodTypeType, parser: $ZodTypeType][]

/**
* A Zod custom schema type that evaluates conditions and applies the corresponding parser.
* The output and input types are derived from the parser schemas in the switch map.
* Definition for a ZodSwitch schema.
* Extends the base custom def with a switchMap and precomputed union.
*/
export interface ZodSwitchDef extends $ZodCustomDef {
switchMap: $ZodSwitchMap
union: ZodMiniUnion
}

/**
* Internal state for a ZodSwitch schema.
*/
export interface ZodSwitchInternals<O = unknown, I = unknown>
extends $ZodCustomInternals<O, I> {
def: ZodSwitchDef
}

/**
* A Zod schema that evaluates conditions and applies the corresponding parser.
* The union metadata is stored directly on the instance (no external registry).
*
* @template SwitchMap - The switch map defining condition-parser pairs
*/
export type ZodSwitch<SwitchMap extends $ZodSwitchMap = $ZodSwitchMap> =
ZodMiniCustom<
export interface ZodSwitch<SwitchMap extends $ZodSwitchMap = $ZodSwitchMap>
extends $ZodType {
_zod: ZodSwitchInternals<
SwitchMap[number][1]['_zod']['output'],
SwitchMap[number][1]['_zod']['input']
>
}

export const ZodSwitch: $constructor<ZodSwitch> = $constructor(
'ZodSwitch',
(inst, def) => {
$ZodType.init(inst, def)
inst._zod.parse = (payload) => {
const input = payload.value
payload.value = NEVER
for (const [condition, parser] of def.switchMap) {
const conditionResult = safeParse(condition, input)
if (conditionResult.success) {
const parseResult = safeParse(parser, conditionResult.data)
if (parseResult.success) {
payload.value = parseResult.data
} else {
for (const issue of parseResult.error.issues) {
payload.issues.push({
...issue,
input: input as any,
})
}
}
break
}
}
return payload
}
},
)

/**
* Registry for switch schemas that stores metadata about the union of all possible parsers.
* Used internally by zodSwitch to register schema information.
* Check if a Zod schema is a ZodSwitch instance.
*/
export const switchRegistry: $ZodRegistry<{
union: ZodMiniUnion
}, ZodSwitch<$ZodSwitchMap>> = registry()
export function isZodSwitch(parser: $ZodTypeType): parser is ZodSwitch {
return parser instanceof ZodSwitch
}

/**
* Create a conditional Zod parser that evaluates different schemas based on conditions.
Expand All @@ -57,30 +103,12 @@ export const switchRegistry: $ZodRegistry<{
export function zodSwitch<SwitchMap extends $ZodSwitchMap>(
switchMap: SwitchMap,
): ZodSwitch<SwitchMap> {
return custom<
SwitchMap[number][1]['_zod']['output'],
SwitchMap[number][1]['_zod']['input']
>().check(
superRefine((input, payload) => {
for (const [condition, parser] of switchMap) {
const conditionResult = safeParse(condition, input)
if (conditionResult.success) {
const parseResult = safeParse(parser, conditionResult.data)
if (parseResult.success) payload.value = parseResult.data
else {
for (const issue of parseResult.error.issues) {
payload.addIssue({
...issue,
input: input as any,
})
}
}
break
}
}
}),
)
.register(switchRegistry, {
union: union(switchMap.map(([, type]) => type)) as any,
})
return new ZodSwitch({
type: 'custom',
check: 'custom',
fn: () => true,
abort: true,
switchMap,
union: union(switchMap.map(([, type]) => type)) as ZodMiniUnion,
}) as ZodSwitch<SwitchMap>
}
Loading
Loading