import {CamelCase} from "type-fest"; import {ZodRawShape} from "zod/lib/types"; import {z, ZodDiscriminatedUnion, ZodLiteral, ZodNumber, ZodObject, ZodSchema, ZodString} from "zod"; import { EntityCore, EntityCoreShape, EntityCoreShapeType, EntitySymbol, Extender, UnsafeEntitySymbol, UnsafeZentity, Zentity, ZodShapeType } from "./zentity"; import {Clean, toCamelCase, typedObjectEntries, typedObjectKeys, ValueUnion} from "./utils"; type ExtractType = T extends { [K in D]: infer V } ? { [K in keyof T]: K extends D ? V : T[K] } : never; export type VariantsShape = Record export type KeyedExtender, R extends object> = { [K in keyof KS]?: Extender } type Flatten, D extends string> = ValueUnion<{ [K in keyof O]: ZodShapeType & { [Q in D]: K } }> type CamelCaseOf

= CamelCase<`${P}-${K}`> type VariantShape, VARIANT_KEY extends keyof VARIANTS_SHAPE> = VARIANTS_SHAPE[VARIANT_KEY] & Clean type VariantType, VARIANT_KEY extends keyof VARIANTS_SHAPE> = ZodShapeType> type ReturnType = E extends (arg: any) => infer R ? R : {} type ToZodD, D extends string> = ZodObject } }> & BS & EntityCoreShapeType> type PolymorphicNewFn, D extends string, KSE extends KeyedExtender> = { [key in (keyof KS & string) as CamelCaseOf<`new`, key>]: (params: VariantType & { id: string }) => PolymorphicEntityType } type PolymorphicCreateFn, D extends string, KSE extends KeyedExtender> = { [key in (keyof KS & string) as CamelCaseOf<`create`, key>]: (params: VariantType & EntityCore) => PolymorphicEntityType } type PolymorphicUnsafeFn, D extends string, KSE extends KeyedExtender> = { [key in (keyof KS & string) as CamelCaseOf<`unsafe`, key>]: (params: VariantType & EntityCore) => UnsafePolymorphicEntityType } type PolymorphicGuardFn, D extends string, KSE extends KeyedExtender> = { [key in (keyof KS & string) as CamelCaseOf<'is', key>]: (data: any) => data is PolymorphicEntityType } type PolymorphicShape, D extends string> = { [key in (keyof KS & string) as CamelCaseOf]: VariantShape & { [K in D]: ZodLiteral } & typeof EntityCoreShape } type PolymorphicSchema, D extends string> = { [key in (keyof KS & string) as CamelCaseOf]: ZodObject & { [K in D]: ZodLiteral } & typeof EntityCoreShape> } type PolymorphicEntityType, D extends string, KE extends KeyedExtender, key extends keyof KS> = Zentity<{ [Q in D]: key } & VariantType, ReturnType> type UnsafePolymorphicEntityType, D extends string, KE extends KeyedExtender, key extends keyof KS> = UnsafeZentity<{ [Q in D]: key } & VariantType, ReturnType> export type PolymorphicEntityClass, D extends string, KE extends KeyedExtender> = { name: string, create: >(params: T & EntityCore & ZodShapeType) => Zentity & ExtractType, ReturnType>, new: >(params: T & ZodShapeType & { id: string }) => Zentity, ReturnType>, unsafe: >(params: T & ZodShapeType) => UnsafeZentity, ReturnType>, baseShape: BS & typeof EntityCoreShape, schema: ZodDiscriminatedUnion, ...ToZodD[]]>, variants: (keyof KS)[], } & PolymorphicNewFn & PolymorphicCreateFn & PolymorphicUnsafeFn & PolymorphicGuardFn & PolymorphicSchema & PolymorphicShape export function polymorphicEntity, D extends string, KSE extends KeyedExtender>(options: { name: string, discriminator: D, base: BS, variants: KS, extend: KSE, }): PolymorphicEntityClass { const baseShape = { ...EntityCoreShape, ...options.base, } const unionSchema = z.discriminatedUnion( options.discriminator, typedObjectEntries(options.variants).map(([key, shape]) => { return z.object({ ...shape, ...baseShape, [options.discriminator]: z.literal(key) }) }) ) const genEntityCore = () => ({ createdAt: new Date(), updatedAt: new Date(), }) const createFn = (schema: ZodSchema, params: any, unsafe: boolean = false) => { const data = unsafe ? params : schema.parse(params) return { _brand: UnsafeEntitySymbol, _safe: EntitySymbol, ...data, copy: copyFn(schema, data), validate: validateFn(schema, data), toPlain: () => data, ...options.extend[params[options.discriminator]]?.(data), } } const copyFn = (schema: ZodSchema, params: any) => { return (copyParams: any) => { return createFn(schema, { ...params, ...copyParams, }) } } const validateFn = (schema: ZodSchema, params: any) => () => { return createFn(schema, params) } const stateKeys = typedObjectKeys(options.variants) const stateShapes: PolymorphicShape = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`${String(key)}-shape`)]: { ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), } } }, {}) as any; const stateSchemas: PolymorphicSchema = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`${String(key)}-schema`)]: z.object({ ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), }) } }, {}) as any; const stateCreateFn: PolymorphicCreateFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`create-${String(key)}`)]: (params: VariantType & EntityCore) => { return createFn(z.object({ ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), }), {...params, [options.discriminator]: key}) } } }, {}) as any; const stateNewFn: PolymorphicNewFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`new-${String(key)}`)]: (params: VariantType) => { return createFn(z.object({ ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), }), { ...params, [options.discriminator]: key, ...genEntityCore() }, ) } } }, {}) as any; const stateUnsafeFn: PolymorphicUnsafeFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`unsafe-${String(key)}`)]: (params: VariantType & EntityCore) => { return createFn(z.object({ ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), }), { ...params, [options.discriminator]: key, ...genEntityCore() }, true, ) } } }, {}) as any; const stateIsFn: PolymorphicGuardFn = stateKeys .reduce((acc, key) => { return { ...acc, [toCamelCase(`is-${String(key)}`)]: (params: any): params is PolymorphicEntityType => { const schema = z.object({ ...baseShape, ...options.variants[key], [options.discriminator]: z.literal(key), }) return schema.safeParse(params).success } } }, {}) as any; return { name: options.name, ...stateCreateFn, ...stateNewFn, ...stateUnsafeFn, ...stateShapes, ...stateSchemas, ...stateIsFn, baseShape, schema: unionSchema, variants: Object.keys(options.variants) as (keyof KS)[], create: >(params: T & EntityCore & ZodShapeType) => { return createFn(unionSchema, params) }, new: >(params: T & ZodShapeType & { id: string }) => { return createFn(unionSchema, { ...params, ...genEntityCore(), }) }, unsafe: >(params: T & ZodShapeType) => { return createFn(unionSchema, params, true) }, } } type UnionOfStateShapeTypesWithDiscriminator, D extends string> = ValueUnion<{ [key in keyof KS]: ZodShapeType & { [Q in D]: key } }> export type inferPolymorphicEntityTypeFromClass = T extends PolymorphicEntityClass ? Zentity & ZodShapeType>, ReturnType> : never; export type inferSpecificPolymorphicEntityTypeFromClass = T extends PolymorphicEntityClass ? K extends keyof KS ? Zentity & ZodShapeType, ReturnType> : never : never; const TestClass = polymorphicEntity({ name: 'Test', discriminator: 'status', base: { gameNo: z.number() }, extend: {}, variants: { initial: { indexer: z.string() }, finished: { multiplier: z.number().min(1), } }, }) type Test = inferPolymorphicEntityTypeFromClass type TestInitial = inferSpecificPolymorphicEntityTypeFromClass function testFn(test: Test, initial: TestInitial) { if (test.status == 'finished') { return test.gameNo + test.multiplier } else if (2 > 3) { return test.indexer.split("") } return initial.multiplier * 5 }