UNPKG

lean4-code-actions

Version:

Refactorings and snippets for Lean 4

165 lines (140 loc) 6.56 kB
import { difference, equals, isFunction } from 'remeda' import { RefinementCtx, SafeParseReturnType, ZodArray, ZodError, ZodIssueCode, ZodSchema, ZodType, ZodTypeDef, z } from 'zod' import { ArrayCardinality } from 'zod/lib/types' import { ensure, ensureByIndex } from './ensure' import { isEqualByD } from './lodash' import { Uid, byUid } from './uid' export interface ZodFlatError { formErrors: string[] fieldErrors: { [k: string]: string[] } } export type GetUid<UidHolder> = (holder: UidHolder) => Uid export type Validate<Obj> = (object: Obj) => Obj export type Insert<Obj> = (object: Obj) => Obj export interface Model<Obj, UidHolder> { schema: ZodSchema<Obj>, validate: Validate<Obj> getUid: GetUid<Obj> } export interface Stat { uid: Uid count: number } export type GetUniqueValue<Obj> = (object: Obj) => unknown export function getSchemaDescription<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodType<Output, Def, Input>) { return ensure(schema.description, () => { let schemaIdentifier: string // console.log('schema', stringify(schema)) if ('shape' in schema._def && isFunction(schema._def.shape)) { schemaIdentifier = JSON.stringify(schema._def.shape()) } else { schemaIdentifier = JSON.stringify(schema) } return new Error(`Schema does not have a description (use .describe() to add it): ${schemaIdentifier}`) }) } export function getArraySchema<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, getUniqueValue: GetUniqueValue<Output>) { const $description = getSchemaDescription(schema) + 'Array' const $schema = z.array(schema).describe($description) return withDuplicatesRefinement($schema, getUniqueValue) } export function getNonEmptyArraySchema<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, getUniqueValue: GetUniqueValue<Output>) { const $description = getSchemaDescription(schema) + 'NonEmptyArray' const $schema = z.array(schema).nonempty().describe($description) return withDuplicatesRefinement($schema, getUniqueValue) } export function withDuplicatesRefinement<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output, Cardinality extends ArrayCardinality = 'many'>(schema: ZodArray<ZodSchema<Output, Def, Input>, Cardinality>, getUniqueValue: GetUniqueValue<Output>) { return schema.superRefine(getDuplicatesRefinement(getSchemaDescription(schema), getUniqueValue)) } export function getDuplicatesRefinement<Obj>(name: string, getUniqueValue: GetUniqueValue<Obj>) { return function (objects: Obj[], context: RefinementCtx) { try { const stats = getDuplicateStats(objects, getUniqueValue) stats.map(stat => context.addIssue({ code: ZodIssueCode.custom, params: stat, message: `Found ${name} duplicates: ${JSON.stringify(stat)}`, })) } catch (error) { context.addIssue({ code: ZodIssueCode.custom, params: { error }, message: `Error while counting ${name} duplicates`, }) } } } export function getDuplicateStats<Obj>(objects: Obj[], getUniqueValue: GetUniqueValue<Obj>) { const stats = getUniqueCountStats(objects, getUniqueValue) return stats.filter(stat => stat.count > 1) } export function getUniqueCountStats<Obj>(objects: Obj[], getUniqueValue: GetUniqueValue<Obj>): Stat[] { const stats: Stat[] = [] return objects.reduce<Stat[]>((stats, value) => { const uid = getUniqueValue(value) const index = stats.findIndex(s => equals(s.uid, uid)) const stat = stats[index] if (stat) { stat.count++ } else { stats.push({ uid, count: 1 }) } return stats }, stats) } export const insert = (name: string) => <Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodType<Output, Def, Input>) => (getUid: GetUid<Output>) => (array: Array<Output>) => (object: Input) => { try { const $object = schema.parse(object) const duplicate = array.find(o => isEqualByD(o, $object, getUid)) if (duplicate) throw new Error(`Duplicate ${name} found: ${JSON.stringify(getUid(duplicate))}`) array.push($object) return $object } catch (error) { throw { object, error } } } export function getGenericInserter<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(name: string, schema: ZodType<Output, Def, Input>, getUid: GetUid<Output>) { return insert(name)(schema)(getUid) } export function getInserter<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(name: string, schema: ZodType<Output, Def, Input>, getUid: GetUid<Output>, array: Array<Output>) { return insert(name)(schema)(getUid)(array) } export function getMultiInserter<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(name: string, schema: ZodType<Output, Def, Input>, getUid: GetUid<Output>, array: Array<Output>) { const inserter = insert(name)(schema)(getUid)(array) return (objects: Array<Input>) => objects.map(inserter) } // export function getInserterWithDefaults<Output extends object, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(name: string, schema: ZodType<Output, Def, Input>, getUid: GetUid<Output>, array: Array<Output>, defaults: Partial<Input>) { // const doInsert = insert(name)(schema)(getUid)(array) // return (object: Input) => doInsert(merge({}, defaults, object)) // } export function getFinder<UidHolder, Output extends UidHolder>(getUid: GetUid<UidHolder>, array: Array<Output>) { return function (uidHolder: UidHolder) { return array.find(byUid(getUid, uidHolder)) } } export function getName<T>(schema: ZodSchema<T>) { const description = ensure(schema.description, new Error(`Cannot get schema name: ${JSON.stringify(schema)}`)) const splinters = description.split(' ') return ensureByIndex(splinters, 0) } export const mustIncludeAllOf = <El>(required: El[]) => (elements: El[]) => difference(required, elements).length === 0 export function getErrorReports<Output, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(results: SafeParseReturnType<Input, Output>[], schema: ZodType<Output, Def, Input>) { const errors = results.reduce(function (reports: ErrorReport<Input>[], result, index) { if (result.success) { return reports } else { const report = { index, error: result.error, } return reports.concat([report]) } }, []) return errors } export interface ErrorReport<Product> { index: number, error: ZodError<Product> }