import {inspect} from 'node:util' import type {AnyFunc, CommonSignature, GenSigs} from '@/interfaces/type' // A base type that roughly describes the dependencies of a single factory // for implementations of one operation. It is an object whose keys are the // identifiers are dependencies, and whose values describe that dependency. // In the value for a given key, the 'is' property gives the name of the // operation that dependency should be an instance of, defaulting to the key // itself when not present, and the 'sig' property gives the desired // signature for that operation. When the 'sig' property is not present, // the signature will default to some ambient ensemble of signatures. export type RawDependencies = Record // The following type transform fills in any unspecified signatures in RD // with the corresponding signatures from SomeSigs: type PatchedDepSpec = { [K in keyof RD]: RD[K] extends {sig: AnyFunc} ? RD[K] : K extends keyof SomeSigs ? (RD[K] & {sig: SomeSigs[K]}) : RD[K] } // A factory for building dependency specifications from the ensemble of // common signatures for a specific type (and perhaps auxiliary type). This // is typically used when describing implementation factories for one type // that depend on the common signatures for a *different* type. export function commonSpecs< T, Aux = T, CommonSigs extends GenSigs = CommonSignature >() { return ( rd: RD ): PatchedDepSpec => Object.fromEntries( Object.keys(rd).map(k => [ k, 'sig' in rd[k] ? rd[k] : {...rd[k], sig: (() => undefined)} ]) ) as PatchedDepSpec } // Further constraint on a dependency specification that means it is ready // to use with a given set of signatures: type DepSpec = { [K in Needs]: K extends keyof Signatures ? {sig?: AnyFunc} : {is: keyof Signatures, sig: AnyFunc} } // Just checks if an RawDependencies is really a DepSpec, and blanks it out if not type DepCheck< RD extends RawDependencies, Signatures extends GenSigs, Needs extends string = keyof RD & string > = RD extends DepSpec ? RD : { [K in Needs]: K extends keyof Signatures ? {} : {is: never, sig: (q: boolean) => void} } // The actual type of a dependency, given a dependency specification type DepType< Signatures extends GenSigs, DS extends DepSpec > = {[K in keyof DS]: DS[K] extends {sig: AnyFunc} ? DS[K]['sig'] : K extends keyof Signatures ? Signatures[K] : never } // A collection of dependency specifications for some of the operations in // an ensemble of Signatures: type Specifications< Signatures extends GenSigs, NeedKeys extends keyof Signatures & string, NeedList extends Record > = {[K in NeedKeys]: DepSpec} // The type of a factory function for implementations of a dependent operation, // given a dependency specification: type FactoryType< Signatures extends GenSigs, K extends (keyof Signatures) & string, DS extends DepSpec > = (dep: DepType) => Signatures[K] // The type of an implementation specification for an operation given its // dependency specification: either directly the implementation if there // are actually no dependencies, or a factory function and collection of // dependency names otherwise: type ImpType< Signatures extends GenSigs, K extends (keyof Signatures) & string, DS extends DepSpec > = DS extends null ? {implementation: Signatures[K]} : {factory: FactoryType, dependencies: DS} // A collection of implementations for some operations of an ensemble of // Signatures, matching a given collection of dependency specifications type Implementations< Signatures extends GenSigs, NeedKeys extends keyof Signatures & string, NeedList extends Record, Specs extends Specifications > = {[K in NeedKeys]: ImpType} // The builder interface that lets us assemble narrowly-typed Implementations: interface ImplementationBuilder< Signatures extends GenSigs, NeedKeys extends keyof Signatures & string, NeedList extends Record, Specs extends Specifications > { independent( independentImps: {[K in NewKeys]: Signatures[K]} ): ImplementationBuilder< Signatures, NeedKeys | NewKeys, NeedList & {[K in NewKeys]: never}, Specs & {[K in NewKeys]: null} > dependent< RD extends RawDependencies, // Easier to infer NewKeys extends (keyof Signatures) & string, DepKeys extends string = keyof RD & string >( depSpec: RD, imps: { [K in NewKeys]: FactoryType> } ): ImplementationBuilder< Signatures, NeedKeys | NewKeys, NeedList & {[K in NewKeys]: DepKeys}, Specs & {[K in NewKeys]: DepCheck} > done(): Implementations } // And a function that actually provides the builder interface: function impBuilder< Signatures extends GenSigs, NeedKeys extends keyof Signatures & string, NeedList extends Record, Specs extends Specifications >( sofar: Implementations ): ImplementationBuilder { return { independent( imps: {[K in NewKeys]: Signatures[K]}) { return impBuilder({ ...sofar, ...Object.fromEntries(Object.keys(imps).map(k => [k, { implementation: imps[k] }])) } as Implementations< Signatures, NeedKeys | NewKeys, NeedList & {[K in NewKeys]: never}, Specs & {[K in NewKeys]: null} >) }, dependent< RD extends RawDependencies, NewKeys extends (keyof Signatures) & string, DepKeys extends string = keyof RD & string >( depSpec: RD, imps: { [K in NewKeys]: FactoryType> } ) { return impBuilder({ ...sofar, ...Object.fromEntries(Object.keys(imps).map(k => [k, { factory: imps[k], dependencies: depSpec }])) }) as unknown as ImplementationBuilder< Signatures, NeedKeys | NewKeys, NeedList & {[K in NewKeys]: DepKeys}, Specs & {[K in NewKeys]: DepCheck} > }, done() { return sofar } } } // A convenience function that gives you an implementation builder: export function implementations( ): ImplementationBuilder { return impBuilder({}) } // Now we turn to creating a Dispatcher itself. For this we use loose types, // and rely on the type annotations from our special build for runtime type // identification. type Callable = (...args: any) => any type Implementation = {implementation: Callable} type Factory = {factory: Callable, dependencies: Record} type Reflected = {_reflectedType5: any} type TypeSpec = { name: string, before?: string[], test: Callable, from: Record, infer?: Callable } type ImpItem = Implementation | Factory type ImpGroup = Record // When this is being compiled, TypeScript can't tell that the // ImpSpecification entities will have been reflected: type ImpSpecification = (ImpGroup | (() => ImpGroup) | TypeSpec) // & Reflected interface ImpSpecs extends Record {} type ImpHolder = { implementations: Record, // Key is a signature factories: Record } type DispatcherInstance = { implementationData: Record, // Key is an operation name types: TypeSpec[], // Order is order to try types in behaviors: Record, // Key is opname; actually executable! } function newDispatcherInstance(): DispatcherInstance { return { implementationData: {}, types: [], behaviors: {} } } function isTypeSpec(spec: ImpSpecification | ImpSpecs): spec is TypeSpec { if ('name' in spec && typeof spec.name === 'string' && 'test' in spec && 'from' in spec ) { return true } return false } // The assemble function creates a dispatcher from a mess of specifications export function assemble(specifications: ImpSpecs, into?: DispatcherInstance ) { if (into === undefined) { into = newDispatcherInstance() const show = inspect(specifications, {depth: 18, colors: true}) console.log('Specifications are', show) } for (const specName in specifications) { console.log('Processing', specName) const spec = specifications[specName] if ('_reflectedType5' in spec) { if (isTypeSpec(spec)) { registerTypeSpec(spec, into) continue } // implementations that we need to deal with console.log('Need to incorporate', Object.keys(spec)) } else { // Just another layer of specification assemble(spec as ImpSpecs, into) } } into.behaviors.typeOf = (a: unknown) => whichType(a, into.types) return into.behaviors } function registerTypeSpec(typeSpec: TypeSpec, into: DispatcherInstance) { let position = into.types.length if ('before' in typeSpec) { for (const typeName of typeSpec.before) { const typeIndex = into.types.findIndex(t => t.name = typeName) if (typeIndex >= 0 && typeIndex < position) position = typeIndex } } into.types.splice(position, 0, typeSpec) // likely there will be more to do in the long run } // Returns the string name of the type of _a_ per the type specifications // in _types_, or 'unknown' if no type matches function whichType(a: unknown, types: TypeSpec[]) { for (const typeSpec of types) { const typeName = typeSpec.name // First check if this is a ground type or a generic: const typeSpecType = (typeSpec as TypeSpec & Reflected)._reflectedType5 if (!('_typeParameters' in typeSpecType.test)) { // ground type, just test it if (typeSpec.test(a)) return typeName continue } // Generic type. In this case, the test will be a factory, dependent // on a test for each of the type parameters. So assemble those // dependencies: const typePars = typeSpecType.test._typeParameters const permissiveTests = Object.fromEntries(typePars.map(k => [`test${k}`, x => true])) const testAllUnknown = typeSpec.test(permissiveTests) if (!testAllUnknown(a)) continue // Here, a seems to be in some instantiation of this generic type. // Need to infer which instantiation const thisInfer = typeSpec.infer({typeOf: x => whichType(x, types)}) const typeArguments = thisInfer(a) return `${typeName}<${typePars.map(k => typeArguments[k]).join(',')}>` } return 'unknown' }