import { Evaluate, Func, KeyValuate, transform, TreeOf, ValueOf, Exact, toString } from "@re-do/utils" import { ExtractableDefinition } from "./internal.js" import { Root } from "./root.js" import { TypeSet } from "./typeSet" import { ValidationErrors, unknownTypeError } from "./errors.js" import { Recursible } from "./recursible/index.js" export type MatchesArgs = { definition: DefType typeSet: TypeSet.Definition } export type ParseContext = { typeSet: TypeSet.Definition path: string[] seen: string[] shallowSeen: string[] } export type ParseArgs = [definition: DefType, context: ParseContext] export type AllowsOptions = { ignoreExtraneousKeys?: boolean } export type ReferencesOptions = { includeBuiltIn?: boolean } export type GenerateOptions = { // By default, we will throw if we encounter a cyclic required type // If this options is provided, we will return its value instead onRequiredCycle?: any } export type ParserInput< DefType, Parent, Children extends DefType[], Components > = { type: DefType parent: () => { meta: Parent } matches: DefinitionMatcher components?: (...args: ParseArgs) => Components children?: () => Children // What to do if no children match (defaults to throwing unparsable error) fallback?: (...args: ParseArgs) => any } export type DefinitionMatcher = ( ...args: ParseArgs> ) => boolean export type HandlesArg = Children extends never[] ? [handles: Required] : [handles?: Handles] export type HandlesContext = [ args: { def: DefType ctx: ParseContext } & (unknown extends Components ? {} : { components: Components }) ] export type HandlesMethods = { allows?: ( ...args: [ ...args: HandlesContext, valueType: ExtractableDefinition, options: AllowsOptions ] ) => ValidationErrors references?: ( ...args: [ ...args: HandlesContext, options: ReferencesOptions ] ) => DefType extends Recursible.Definition ? TreeOf : string[] generate?: ( ...args: [ ...args: HandlesContext, options: GenerateOptions ] ) => any } export type UnhandledMethods = Omit< HandlesMethods, keyof GetHandledMethods > export type ParseFunction = ( ...args: ParseArgs ) => ParseResult export type ParseResult = { def: DefType ctx: ParseContext } & CoreMethods export type CoreMethods = { [MethodName in CoreMethodName]-?: TransformInputMethod< NonNullable[MethodName]> > } export type TransformInputMethod< Method extends ValueOf> > = Method extends ( ...args: [infer ParseResult, ...infer Rest, infer Opts] ) => infer Return ? (...args: [...rest: Rest, opts?: Opts]) => Return : Method export type GetHandledMethods = (KeyValuate< Parent, "inherits" > extends () => infer Return ? Return : {}) & KeyValuate export type ParserMetadata< DefType, Parent, Handles extends HandlesMethods > = Evaluate<{ meta: { type: DefType inherits: () => GetHandledMethods handles: unknown extends Handles ? {} : Handles matches: DefinitionMatcher } }> export type Parser = Evaluate< ParserMetadata & ParseFunction > export type CoreMethodName = keyof HandlesMethods const coreMethodNames = ["allows", "references", "generate"] as CoreMethodName[] // Re:Root, reroot its root by rerouting to reroot export const reroot = { meta: { type: {} as Root.Definition, inherits: () => {}, handles: {} } } type AnyParser = Parser export const createParser = < Input, Handles, DefType, Parent, Components, Children extends DefType[] = [] >( ...args: [ Exact>, ...HandlesArg< Children, Exact> > ] ): Parser => { const input = args[0] as ParserInput const handles = (args[1] as HandlesMethods) ?? {} // Need to wait until parse is called to access parent to avoid it being undefined // due to circular import const getInherited = () => { const parent = input.parent() as any as ParserMetadata return { ...parent.meta.inherits(), ...parent.meta.handles } as HandlesMethods } const getChildren = (): AnyParser[] => (input.children?.() as any) ?? [] const cachedComponents: Record = {} const getComponents = (def: DefType, ctx: ParseContext) => { const memoKey = toString({ def, typeSet: ctx.typeSet, shallowSeen: ctx.shallowSeen, seen: ctx.seen, path: ctx.path }) if (!cachedComponents[memoKey]) { cachedComponents[memoKey] = input.components?.(def, ctx) ?? undefined } return cachedComponents[memoKey] } const transformCoreMethod = ( name: CoreMethodName, inputMethod: Func, def: DefType, ctx: ParseContext, components: Components ) => (...providedArgs: Parameters>>) => { if (name === "allows") { return inputMethod( { def, ctx, components }, providedArgs[0], providedArgs[1] ?? {} ) } return inputMethod( { def, ctx, components }, providedArgs[0] ?? {} ) } const delegateCoreMethod = ( methodName: CoreMethodName, def: DefType, ctx: ParseContext ) => { const children = getChildren() if (!children.length) { throw new Error( `${methodName} was never implemented on the current component's branch.` ) } const match = children.find((child) => child.meta.matches(def, ctx)) if (!match) { if (input.fallback) { return input.fallback(def, ctx) } throw new Error(unknownTypeError(def, ctx.path)) } return match(def, ctx)[methodName] } const getCoreMethods = ( def: DefType, ctx: ParseContext, components: any ) => { return transform(coreMethodNames, ([i, methodName]) => [ methodName, handles[methodName] ? transformCoreMethod( methodName, handles[methodName]!, def, ctx, components ) : getInherited()[methodName] ? transformCoreMethod( methodName, getInherited()[methodName]!, def, ctx, components ) : delegateCoreMethod(methodName, def, ctx) ]) as CoreMethods } const parse = (def: DefType, ctx: ParseContext): ParseResult => { const components = getComponents(def, ctx) return { def, ctx, ...getCoreMethods(def, ctx, components) } } return Object.assign(parse, { meta: { type: input.type, inherits: getInherited, handles, matches: input.matches } }) as any }