import { BaseSchema, MaybeValid, SchemaType } from "../../Schema"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { keys } from "../../utils/keys"; import { MaybePromise } from "../../utils/MaybePromise"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { enum_ } from "../enum"; import { ObjectSchema } from "../object"; import { getObjectLikeUtils, ObjectLikeSchema } from "../object-like"; import { getSchemaUtils } from "../schema-utils"; import { Discriminant } from "./discriminant"; import { inferParsedDiscriminant, inferParsedUnion, inferRawDiscriminant, inferRawUnion, UnionSubtypes } from "./types"; export function union, U extends UnionSubtypes>( discriminant: D, union: U ): ObjectLikeSchema, inferParsedUnion> { const rawDiscriminant = typeof discriminant === "string" ? discriminant : (discriminant.rawDiscriminant as inferRawDiscriminant); const parsedDiscriminant = typeof discriminant === "string" ? discriminant : (discriminant.parsedDiscriminant as inferParsedDiscriminant); const discriminantValueSchema = enum_(keys(union) as string[]); const baseSchema: BaseSchema, inferParsedUnion> = { parse: async (raw, opts) => { return transformAndValidateUnion({ value: raw, discriminant: rawDiscriminant, transformedDiscriminant: parsedDiscriminant, transformDiscriminantValue: (discriminantValue) => discriminantValueSchema.parse(discriminantValue, { allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawDiscriminant], }), getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => additionalPropertiesSchema.parse(additionalProperties, opts), breadcrumbsPrefix: opts?.breadcrumbsPrefix, }); }, json: async (parsed, opts) => { return transformAndValidateUnion({ value: parsed, discriminant: parsedDiscriminant, transformedDiscriminant: rawDiscriminant, transformDiscriminantValue: (discriminantValue) => discriminantValueSchema.json(discriminantValue, { allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedDiscriminant], }), getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => additionalPropertiesSchema.json(additionalProperties, opts), breadcrumbsPrefix: opts?.breadcrumbsPrefix, }); }, getType: () => SchemaType.UNION, }; return { ...maybeSkipValidation(baseSchema), ...getSchemaUtils(baseSchema), ...getObjectLikeUtils(baseSchema), }; } async function transformAndValidateUnion< TransformedDiscriminant extends string, TransformedDiscriminantValue extends string, TransformedAdditionalProperties >({ value, discriminant, transformedDiscriminant, transformDiscriminantValue, getAdditionalPropertiesSchema, allowUnrecognizedUnionMembers = false, transformAdditionalProperties, breadcrumbsPrefix = [], }: { value: unknown; discriminant: string; transformedDiscriminant: TransformedDiscriminant; transformDiscriminantValue: (discriminantValue: unknown) => MaybePromise>; getAdditionalPropertiesSchema: (discriminantValue: string) => ObjectSchema | undefined; allowUnrecognizedUnionMembers: boolean | undefined; transformAdditionalProperties: ( additionalProperties: unknown, additionalPropertiesSchema: ObjectSchema ) => MaybePromise>; breadcrumbsPrefix: string[] | undefined; }): Promise< MaybeValid & TransformedAdditionalProperties> > { if (!isPlainObject(value)) { return { ok: false, errors: [ { path: breadcrumbsPrefix, message: getErrorMessageForIncorrectType(value, "object"), }, ], }; } const { [discriminant]: discriminantValue, ...additionalProperties } = value; if (discriminantValue == null) { return { ok: false, errors: [ { path: breadcrumbsPrefix, message: `Missing discriminant ("${discriminant}")`, }, ], }; } const transformedDiscriminantValue = await transformDiscriminantValue(discriminantValue); if (!transformedDiscriminantValue.ok) { return { ok: false, errors: transformedDiscriminantValue.errors, }; } const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); if (additionalPropertiesSchema == null) { if (allowUnrecognizedUnionMembers) { return { ok: true, value: { [transformedDiscriminant]: transformedDiscriminantValue.value, ...additionalProperties, } as Record & TransformedAdditionalProperties, }; } else { return { ok: false, errors: [ { path: [...breadcrumbsPrefix, discriminant], message: "Unexpected discriminant value", }, ], }; } } const transformedAdditionalProperties = await transformAdditionalProperties( additionalProperties, additionalPropertiesSchema ); if (!transformedAdditionalProperties.ok) { return transformedAdditionalProperties; } return { ok: true, value: { [transformedDiscriminant]: discriminantValue, ...transformedAdditionalProperties.value, } as Record & TransformedAdditionalProperties, }; }