From 6b912bc07b5c6812aa388f000575790569727db2 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 25 Nov 2024 09:43:58 -0500 Subject: [PATCH] refactor(lib/anyware): data type oriented builders --- ...output_preset__standard-graphql.output.txt | 2 +- src/client/builderExtensions/anyware.ts | 6 +- src/client/gql/gql.ts | 4 +- src/client/handleOutput.ts | 5 +- .../requestMethods/requestMethods.ts | 2 +- src/extension/extension.ts | 4 +- src/extensions/Throws/Throws.ts | 4 +- src/lib/anyware/ExecutableStep.ts | 6 - src/lib/anyware/Extension/Builder.ts | 47 +- src/lib/anyware/Extension/Extension.ts | 25 -- src/lib/anyware/Extension/States.ts | 7 + src/lib/anyware/Extension/Updaters.ts | 14 + src/lib/anyware/Extension/_.ts | 3 + src/lib/anyware/Extension/__.ts | 8 +- .../anyware/Interceptor/Interceptor.test-d.ts | 36 +- src/lib/anyware/Interceptor/Interceptor.ts | 32 +- src/lib/anyware/Overload/Builder.ts | 131 +++--- src/lib/anyware/Overload/BuilderCallback.ts | 12 + src/lib/anyware/Overload/Overload.ts | 49 --- src/lib/anyware/Overload/States.ts | 8 + src/lib/anyware/Overload/Updaters.ts | 22 + src/lib/anyware/Overload/_.ts | 4 + src/lib/anyware/Overload/__.ts | 13 +- .../anyware/Pipeline/ExecutablePipeline.ts | 17 - src/lib/anyware/Pipeline/Pipeline.test-d.ts | 91 ++++ src/lib/anyware/Pipeline/Pipeline.ts | 173 ++++++++ src/lib/anyware/Pipeline/Spec.test-d.ts | 48 --- src/lib/anyware/Pipeline/Spec.ts | 52 --- src/lib/anyware/Pipeline/_.ts | 6 - src/lib/anyware/Pipeline/__.ts | 1 - src/lib/anyware/Pipeline/builder.ts | 408 ------------------ .../anyware/Pipeline/createWithSpec.test-d.ts | 30 -- src/lib/anyware/Pipeline/createWithSpec.ts | 102 ----- src/lib/anyware/Pipeline/run.test-d.ts | 22 - .../{Pipeline => PipelineDef}/Config.ts | 0 src/lib/anyware/PipelineDef/States.ts | 8 + src/lib/anyware/PipelineDef/Updaters.ts | 26 ++ src/lib/anyware/PipelineDef/Utilities.ts | 17 + src/lib/anyware/PipelineDef/_.ts | 9 + src/lib/anyware/PipelineDef/__.ts | 12 + .../builder.test-d.ts | 109 +---- src/lib/anyware/PipelineDef/builder.ts | 205 +++++++++ .../PipelineSpecification/Spec.test-d.ts | 48 +++ src/lib/anyware/PipelineSpecification/Spec.ts | 54 +++ .../createWithSpec.test-d.ts | 30 ++ .../PipelineSpecification/createWithSpec.ts | 98 +++++ src/lib/anyware/{Pipeline => }/Result.ts | 5 +- src/lib/anyware/Step.ts | 66 +-- src/lib/anyware/StepDef.ts | 66 +++ src/lib/anyware/StepRunner.ts | 13 + src/lib/anyware/_.ts | 5 +- src/lib/anyware/__.entrypoint.test.ts | 6 +- src/lib/anyware/__.test-helpers.ts | 41 +- src/lib/anyware/run/getEntrypoint.ts | 8 +- src/lib/anyware/run/run.test-d.ts | 28 ++ src/lib/anyware/run/run.test.ts | 64 +-- src/lib/anyware/run/run.ts | 9 +- src/lib/anyware/run/runPipeline.ts | 8 +- src/lib/anyware/run/runStep.ts | 8 +- src/lib/anyware/run/runner.ts | 22 +- .../config-manager/ConfigManager.test-d.ts | 26 +- src/lib/config-manager/ConfigManager.ts | 65 ++- src/lib/prelude.ts | 6 + src/requestPipeline/RequestPipeline.ts | 21 +- .../extensions/httpTransport.ts | 6 +- .../extensions/memoryTransport.ts | 6 +- tests/_/SpyExtension.ts | 8 +- 67 files changed, 1335 insertions(+), 1162 deletions(-) delete mode 100644 src/lib/anyware/ExecutableStep.ts create mode 100644 src/lib/anyware/Extension/States.ts create mode 100644 src/lib/anyware/Extension/Updaters.ts create mode 100644 src/lib/anyware/Extension/_.ts create mode 100644 src/lib/anyware/Overload/BuilderCallback.ts delete mode 100644 src/lib/anyware/Overload/Overload.ts create mode 100644 src/lib/anyware/Overload/States.ts create mode 100644 src/lib/anyware/Overload/Updaters.ts create mode 100644 src/lib/anyware/Overload/_.ts delete mode 100644 src/lib/anyware/Pipeline/ExecutablePipeline.ts create mode 100644 src/lib/anyware/Pipeline/Pipeline.test-d.ts create mode 100644 src/lib/anyware/Pipeline/Pipeline.ts delete mode 100644 src/lib/anyware/Pipeline/Spec.test-d.ts delete mode 100644 src/lib/anyware/Pipeline/Spec.ts delete mode 100644 src/lib/anyware/Pipeline/_.ts delete mode 100644 src/lib/anyware/Pipeline/__.ts delete mode 100644 src/lib/anyware/Pipeline/builder.ts delete mode 100644 src/lib/anyware/Pipeline/createWithSpec.test-d.ts delete mode 100644 src/lib/anyware/Pipeline/createWithSpec.ts delete mode 100644 src/lib/anyware/Pipeline/run.test-d.ts rename src/lib/anyware/{Pipeline => PipelineDef}/Config.ts (100%) create mode 100644 src/lib/anyware/PipelineDef/States.ts create mode 100644 src/lib/anyware/PipelineDef/Updaters.ts create mode 100644 src/lib/anyware/PipelineDef/Utilities.ts create mode 100644 src/lib/anyware/PipelineDef/_.ts create mode 100644 src/lib/anyware/PipelineDef/__.ts rename src/lib/anyware/{Pipeline => PipelineDef}/builder.test-d.ts (62%) create mode 100644 src/lib/anyware/PipelineDef/builder.ts create mode 100644 src/lib/anyware/PipelineSpecification/Spec.test-d.ts create mode 100644 src/lib/anyware/PipelineSpecification/Spec.ts create mode 100644 src/lib/anyware/PipelineSpecification/createWithSpec.test-d.ts create mode 100644 src/lib/anyware/PipelineSpecification/createWithSpec.ts rename src/lib/anyware/{Pipeline => }/Result.ts (58%) create mode 100644 src/lib/anyware/StepDef.ts create mode 100644 src/lib/anyware/StepRunner.ts create mode 100644 src/lib/anyware/run/run.test-d.ts diff --git a/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt b/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt index bb61a324f..b53f1c272 100644 --- a/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt +++ b/examples/__outputs__/20_output/output_preset__standard-graphql.output.txt @@ -21,7 +21,7 @@ ContextualError: There was an error in the core implementation of hook "exchange at new URL (node:internal/url:XX:XX) at new Request (node:internal/deps/undici/undici:XX:XX) at Object.run (/some/path/to/httpTransport.ts:XX:XX:27) - at Object.run (/some/path/to/builder.ts:XX:XX:53) + at Object.run (/some/path/to/Pipeline.ts:XX:XX:51) at runStep (/some/path/to/runStep.ts:XX:XX:37) at runPipeline (/some/path/to/runPipeline.ts:XX:XX:8) at runPipeline (/some/path/to/runPipeline.ts:XX:XX:20) diff --git a/src/client/builderExtensions/anyware.ts b/src/client/builderExtensions/anyware.ts index 2284f7230..3c634ad1b 100644 --- a/src/client/builderExtensions/anyware.ts +++ b/src/client/builderExtensions/anyware.ts @@ -15,15 +15,13 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters, + interceptor: AnywareLib.Interceptor.InferFromPipeline, ) => Builder.Definition.MaterializeWith<$Arguments['definition'], $Arguments['context']> } export const builderExtensionAnyware = Builder.Extension.create((builder, context) => { const properties = { - anyware: (interceptor: AnywareLib.Interceptor.InferConstructor) => { + anyware: (interceptor: AnywareLib.Interceptor.InferFromPipeline) => { return builder({ ...context, extensions: [ diff --git a/src/client/gql/gql.ts b/src/client/gql/gql.ts index b6dc0dccc..b2190bd69 100644 --- a/src/client/gql/gql.ts +++ b/src/client/gql/gql.ts @@ -70,9 +70,9 @@ export const builderExtensionGql = Builder.Extension.create url, schema, request: analyzedRequest, - } as RequestPipeline['spec']['input'] + } as RequestPipeline['input'] - const result = await Anyware.Pipeline.run(requestPipeline, { + const result = await Anyware.PipelineDef.run(requestPipeline, { initialInput, // retryingExtension: context.retry as any, interceptors: context.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/client/handleOutput.ts b/src/client/handleOutput.ts index a38776774..7a48638b0 100644 --- a/src/client/handleOutput.ts +++ b/src/client/handleOutput.ts @@ -2,6 +2,7 @@ import type { GraphQLError } from 'graphql' import type { Simplify } from 'type-fest' import type { SimplifyDeepExcept } from '../documentBuilder/Simplify.js' import type { RunTypeHookOnRequestResult } from '../extension/extension.js' +import type { Anyware } from '../lib/anyware/__.js' import { Errors } from '../lib/errors/__.js' import type { Grafaid } from '../lib/grafaid/__.js' import type { SomeObjectData } from '../lib/grafaid/graphql.js' @@ -13,7 +14,7 @@ import { type GetOrNever, type Values, } from '../lib/prelude.js' -import type { RequestPipeline, requestPipeline } from '../requestPipeline/RequestPipeline.js' +import type { requestPipeline, RequestPipelineSpec } from '../requestPipeline/RequestPipeline.js' import type { GlobalRegistry } from '../types/GlobalRegistry/GlobalRegistry.js' import type { TransportHttp } from '../types/Transport.js' import type { Context } from './context.js' @@ -52,7 +53,7 @@ export type GraffleExecutionResultEnvelope<$Config extends Config = Config> = export const handleOutput = ( state: Context, - result: RequestPipeline['output'], + result: Anyware.PipelineDef.Utilities.InferResult, ) => { if (isContextConfigTraditionalGraphQLOutput(state.config)) { if (result instanceof Error) throw result diff --git a/src/documentBuilder/requestMethods/requestMethods.ts b/src/documentBuilder/requestMethods/requestMethods.ts index a01ec10e5..1415aa7ab 100644 --- a/src/documentBuilder/requestMethods/requestMethods.ts +++ b/src/documentBuilder/requestMethods/requestMethods.ts @@ -137,7 +137,7 @@ const executeDocument = async ( request, } as RequestPipeline['input'] - const result = await Anyware.Pipeline.run(requestPipeline, { + const result = await Anyware.PipelineDef.run(requestPipeline, { initialInput, // retryingExtension: state.retry as any, interceptors: state.extensions.filter(_ => _.onRequest !== undefined).map(_ => _.onRequest!) as any, diff --git a/src/extension/extension.ts b/src/extension/extension.ts index cd469faaf..6e24d6f00 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -57,7 +57,7 @@ export interface Extension< /** * Anyware executed on every request. */ - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferFromPipeline /** * Manipulate the builder. * You can extend the builder with new properties at both runtime AND buildtime (types, TypeScript). @@ -168,7 +168,7 @@ export const createExtension = < custom?: $Custom create: (params: { config: $Config }) => { builder?: $BuilderExtension - onRequest?: Anyware.Interceptor.InferConstructor + onRequest?: Anyware.Interceptor.InferFromPipeline typeHooks?: () => $TypeHooks } }, diff --git a/src/extensions/Throws/Throws.ts b/src/extensions/Throws/Throws.ts index 66af456b0..d8871f198 100644 --- a/src/extensions/Throws/Throws.ts +++ b/src/extensions/Throws/Throws.ts @@ -40,7 +40,7 @@ interface BuilderExtension_<$Args extends Builder.Extension.Parameters Builder.Definition.MaterializeWith< $Args['definition'], - ConfigManager.UpdateAtKey< + ConfigManager.SetKey< $Args['context'], 'config', // @ts-expect-error fixme @@ -49,7 +49,7 @@ interface BuilderExtension_<$Args extends Builder.Extension.Parameters } -type ThrowsifyConfig<$BuilderConfig extends BuilderConfig> = ConfigManager.SetAtKeyPath< +type ThrowsifyConfig<$BuilderConfig extends BuilderConfig> = ConfigManager.SetKeyAtPath< $BuilderConfig, ['output', 'errors'], { other: 'throw'; execution: 'throw' } diff --git a/src/lib/anyware/ExecutableStep.ts b/src/lib/anyware/ExecutableStep.ts deleted file mode 100644 index b38a66d45..000000000 --- a/src/lib/anyware/ExecutableStep.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Step } from './Step.js' - -export interface ExecutableStep extends Omit { - slots?: Step.Slots - run: Step.Runner -} diff --git a/src/lib/anyware/Extension/Builder.ts b/src/lib/anyware/Extension/Builder.ts index 56563ae35..705e20459 100644 --- a/src/lib/anyware/Extension/Builder.ts +++ b/src/lib/anyware/Extension/Builder.ts @@ -1,31 +1,40 @@ -import type { ConfigManager } from '../../config-manager/__.js' -import type { Overload } from '../Overload/__.js' -import type { Pipeline } from '../Pipeline/__.js' +import { Overload } from '../Overload/__.js' +import type { PipelineDef } from '../PipelineDef/__.js' +import type { Extension } from './__.js' + +type Create = <$Pipeline extends PipelineDef>() => Builder<$Pipeline, Extension.States.Empty> export interface Builder< - $PipelineContext extends Pipeline.Context = Pipeline.Context, - $Context extends Context = Context, + $Pipeline extends PipelineDef = PipelineDef, + $Extension extends Extension = Extension, > { - context: $Context + type: $Extension /** * TODO */ - overload: <$OverloadBuilder extends Overload.Builder<$PipelineContext>>( - overloadBuilder: Overload.BuilderCallback<$PipelineContext, $OverloadBuilder>, + overload: <$OverloadBuilder extends Overload.Builder<$Pipeline>>( + overloadBuilderCallback: Overload.BuilderCallback<$Pipeline, $OverloadBuilder>, ) => Builder< - $PipelineContext, - ConfigManager.AppendAtKey< - $Context, - 'overloads', - $OverloadBuilder['context'] - > + $Pipeline, + Extension.Updaters.AddOverload<$Extension, $OverloadBuilder['type']> > } -export interface Context { - overloads: Overload.BuilderContext[] -} +export namespace Builder { + export const create: Create = () => { + const extension: Extension = { + overloads: [], + } + + const builder: Builder = { + type: extension, + overload: (builderCallback) => { + const overload = builderCallback({ create: Overload.create }) + extension.overloads.push(overload.type) + return builder as any + }, + } -export interface ContextEmpty extends Context { - overloads: [] + return builder as any + } } diff --git a/src/lib/anyware/Extension/Extension.ts b/src/lib/anyware/Extension/Extension.ts index 6827c5e4d..e69de29bb 100644 --- a/src/lib/anyware/Extension/Extension.ts +++ b/src/lib/anyware/Extension/Extension.ts @@ -1,25 +0,0 @@ -import { __ } from '../../prelude.js' -import { Overload } from '../Overload/__.js' -import type { Pipeline } from '../Pipeline/__.js' -import type { Builder, Context, ContextEmpty } from './Builder.js' - -export * from './Builder.js' - -export const create: Create = () => { - const context: Context = { - overloads: [], - } - - const builder: Builder = { - context, - overload: (builderCallback) => { - const overload = builderCallback({ create: Overload.create }) - context.overloads.push(overload.context) - return builder as any - }, - } - - return builder as any -} - -type Create = <$PipelineContext extends Pipeline.Context>() => Builder<$PipelineContext, ContextEmpty> diff --git a/src/lib/anyware/Extension/States.ts b/src/lib/anyware/Extension/States.ts new file mode 100644 index 000000000..f839ebef5 --- /dev/null +++ b/src/lib/anyware/Extension/States.ts @@ -0,0 +1,7 @@ +import type { Extension } from './__.js' + +export namespace States { + export interface Empty extends Extension { + overloads: [] + } +} diff --git a/src/lib/anyware/Extension/Updaters.ts b/src/lib/anyware/Extension/Updaters.ts new file mode 100644 index 000000000..c453b7c31 --- /dev/null +++ b/src/lib/anyware/Extension/Updaters.ts @@ -0,0 +1,14 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import type { Overload } from '../Overload/__.js' +import type { Extension } from './__.js' + +export namespace Updaters { + export type AddOverload< + $Extension extends Extension, + $Overload extends Overload, + > = ConfigManager.UpdateKeyWithAppend< + $Extension, + 'overloads', + $Overload + > +} diff --git a/src/lib/anyware/Extension/_.ts b/src/lib/anyware/Extension/_.ts new file mode 100644 index 000000000..7f9fdc020 --- /dev/null +++ b/src/lib/anyware/Extension/_.ts @@ -0,0 +1,3 @@ +export * from './Builder.js' +export * from './States.js' +export * from './Updaters.js' diff --git a/src/lib/anyware/Extension/__.ts b/src/lib/anyware/Extension/__.ts index 36702cdb0..2ddc4e5be 100644 --- a/src/lib/anyware/Extension/__.ts +++ b/src/lib/anyware/Extension/__.ts @@ -1 +1,7 @@ -export * as Extension from './Extension.js' +import type { Overload } from '../Overload/__.js' + +export * as Extension from './_.js' + +export interface Extension { + overloads: Overload[] +} diff --git a/src/lib/anyware/Interceptor/Interceptor.test-d.ts b/src/lib/anyware/Interceptor/Interceptor.test-d.ts index d783f3d36..330420075 100644 --- a/src/lib/anyware/Interceptor/Interceptor.test-d.ts +++ b/src/lib/anyware/Interceptor/Interceptor.test-d.ts @@ -1,22 +1,24 @@ import { describe, expectTypeOf, test } from 'vitest' import { _, type ExcludeUndefined } from '../../prelude.js' import type { Interceptor } from '../_.js' -import { Pipeline } from '../_.js' +import { Pipeline, PipelineDef } from '../_.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots } from '../__.test-helpers.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' -const b0 = Pipeline.create() +const b0 = PipelineDef.create().input() describe(`interceptor constructor`, () => { test(`receives keyword arguments, a step trigger for each step`, () => { - const p1 = b0 + const b1 = b0 .step({ name: `a`, run: () => results.a }) .step({ name: `b`, run: () => results.b }) .step({ name: `c`, run: () => results.c }) - .done() - type i1 = Interceptor.InferConstructor + const p1 = Pipeline.create(b1.type) + type i1 = Interceptor.InferFromPipeline expectTypeOf>().toMatchTypeOf<[steps: { a: any; b: any; c: any }]>() + // const x: Parameters['0']['a'] = _ + // x({input:{x:1}}).then(r => r.b({input:{}})) expectTypeOf>().toMatchTypeOf<[steps: { a: (params: { input?: initialInput }) => Promise<{ b: (params: { input?: results['a'] }) => any }> b: (params: { input?: results['a'] }) => Promise<{ c: (params: { input?: results['b'] }) => any }> @@ -28,13 +30,13 @@ describe(`interceptor constructor`, () => { test(`original input on self`, () => { const p = b0.step({ name: `a`, run: () => results.a }).done() - type triggerA = GetTriggerFromPipeline + type triggerA = PipelineGetTrigger expectTypeOf().toMatchTypeOf() }) test(`trigger arguments are optional`, () => { const p = b0.step({ name: `a`, run: () => results.a }).done() - type triggerA = GetTriggerFromPipeline + type triggerA = PipelineGetTrigger expectTypeOf<[]>().toMatchTypeOf>() }) @@ -42,8 +44,8 @@ describe(`interceptor constructor`, () => { test(`trigger accepts slots if definition has them, otherwise does NOT so much as accept the slots key`, () => { const p = b0.step({ name: `a`, slots, run: () => results.a }).step({ name: `b`, run: () => results.b }).done() - type triggerA = GetTriggerFromPipeline - type triggerB = GetTriggerFromPipeline + type triggerA = PipelineGetTrigger + type triggerB = PipelineGetTrigger expectTypeOf>().toEqualTypeOf<[params?: { input?: initialInput using?: { @@ -56,14 +58,14 @@ describe(`interceptor constructor`, () => { test(`slots are optional`, () => { const p = b0.step({ name: `a`, slots, run: () => results.a }).done() - type triggerA = GetTriggerFromPipeline + type triggerA = PipelineGetTrigger type triggerASlotInputs = ExcludeUndefined[0]>['using']> expectTypeOf<{ m?: any; n?: any }>().toMatchTypeOf() }) test(`slot function can return undefined (falls back to default slot)`, () => { const p = b0.step({ name: `a`, slots, run: () => results.a }).done() - type triggerA = GetTriggerFromPipeline + type triggerA = PipelineGetTrigger type triggerASlotMOutput = ReturnType< ExcludeUndefined[0]>['using']>['m']> > @@ -78,23 +80,23 @@ describe(`interceptor constructor`, () => { // test(`can return pipeline output or a step envelope`, () => { const p = b0.step({ name: `a`, run: () => results.a }).done() - type i = GetReturnTypeFromPipeline + type i = PipelineGetReturnType expectTypeOf().toEqualTypeOf>() }) test(`return type awaits pipeline output`, () => { const p = b0.step({ name: `a`, run: () => Promise.resolve(results.a) }).done() - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) // --- Helpers --- // dprint-ignore -type GetTriggerFromPipeline<$Pipeline extends Pipeline.ExecutablePipeline, $TriggerName extends string> = +type PipelineGetTrigger<$Pipeline extends Pipeline, $TriggerName extends string> = // @ts-expect-error - Parameters>[0][$TriggerName] + Parameters>[0][$TriggerName] // dprint-ignore -type GetReturnTypeFromPipeline<$Pipeline extends Pipeline.ExecutablePipeline> = - ReturnType> +type PipelineGetReturnType<$Pipeline extends Pipeline> = + ReturnType> diff --git a/src/lib/anyware/Interceptor/Interceptor.ts b/src/lib/anyware/Interceptor/Interceptor.ts index c950ed49a..9b685941b 100644 --- a/src/lib/anyware/Interceptor/Interceptor.ts +++ b/src/lib/anyware/Interceptor/Interceptor.ts @@ -1,7 +1,7 @@ import type { Simplify } from 'type-fest' import type { Deferred, MaybePromise } from '../../prelude.js' -import type { PipelineSpec } from '../_.js' -import type { ResultSuccess } from '../Pipeline/Result.js' +import type { Pipeline } from '../_.js' +import type { ResultSuccess } from '../Result.js' import type { Step } from '../Step.js' import type { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' @@ -11,30 +11,38 @@ export type InterceptorOptions = { } export namespace Interceptor { - export interface InferConstructor< - $PipelineSpec extends PipelineSpec = PipelineSpec, + export interface InferFromPipeline< + $Pipeline extends Pipeline = Pipeline, > // $Options extends InterceptorOptions = InterceptorOptions, { - (steps: Simplify>): Promise< - | $PipelineSpec['output'] + (steps: Simplify>): Promise< + | $Pipeline['output'] | StepTriggerEnvelope > } - type InferConstructorKeywordArguments< - $PipelineSpec extends PipelineSpec, - > = InferConstructorKeywordArguments_<$PipelineSpec['steps'], Awaited<$PipelineSpec['output']>> + type InferKeywordArguments< + $Pipeline extends Pipeline, + > = InferKeywordArguments_< + $Pipeline['steps'], + $Pipeline['output'] + > // dprint-ignore - type InferConstructorKeywordArguments_< + type InferKeywordArguments_< $Steps extends Step[], $PipelineOutput, > = $Steps extends [infer $NextStep extends Step, ...infer $NextNextSteps extends Step[]] ? & { - [_ in $NextStep['name']]: StepTrigger.Infer<$NextStep, $NextNextSteps, $PipelineOutput> + [_ in $NextStep['name']]: + StepTrigger.Infer< + $NextStep, + $NextNextSteps, + $PipelineOutput + > } - & InferConstructorKeywordArguments_<$NextNextSteps, $PipelineOutput> + & InferKeywordArguments_<$NextNextSteps, $PipelineOutput> : {} } diff --git a/src/lib/anyware/Overload/Builder.ts b/src/lib/anyware/Overload/Builder.ts index ae1d465b7..afbd2b32b 100644 --- a/src/lib/anyware/Overload/Builder.ts +++ b/src/lib/anyware/Overload/Builder.ts @@ -1,47 +1,84 @@ import type { ConfigManager } from '../../config-manager/__.js' import type { Tuple } from '../../prelude.js' -import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step.js' +import type { PipelineDef } from '../PipelineDef/__.js' +import type { StepDef } from '../StepDef.js' +import type { Overload } from './__.js' + +export const create: Create = (parameters) => { + const context_: Omit = { + discriminant: parameters.discriminant, + steps: {}, + } + const overload = context_ as Overload + + const builder: Builder = { + type: overload, + extendInput: () => builder as any, + stepWithExtendedInput: () => builder.step as any, + step: (name, spec) => { + overload.steps[name] = { + name, + ...spec, + } as unknown as StepDef + return builder as any + }, + } + + return builder as any +} + +export type Create<$Pipeline extends PipelineDef = PipelineDef> = < + const $DiscriminantSpec extends Overload['discriminant'], +>( + parameters: { discriminant: $DiscriminantSpec }, +) => Builder< + $Pipeline, + { + discriminant: $DiscriminantSpec + input: {} + steps: {} + } +> export interface Builder< - $RootContext extends Pipeline.Context = Pipeline.Context, - $Context extends BuilderContext = BuilderContextEmpty, + $Pipeline extends PipelineDef = PipelineDef, + $Overload extends Overload = Overload.States.Empty, > { - context: $Context + type: $Overload /** * TODO */ - step: BuilderStep<$RootContext, $Context> + step: StepMethod<$Pipeline, $Overload> /** * TODO */ extendInput: <$InputExtension extends object>() => Builder< - $RootContext, - ConfigManager.UpdateAtKey<$Context, 'input', $InputExtension> + $Pipeline, + Overload.Updaters.SetInput<$Overload, $InputExtension> > /** * TODO */ - stepWithExtendedInput: <$InputExtension extends object>() => BuilderStep< - $RootContext, - $Context, + stepWithExtendedInput: <$InputExtension extends object>() => StepMethod< + $Pipeline, + $Overload, $InputExtension > } -interface BuilderStep< - $RootContext extends Pipeline.Context, - $Context extends BuilderContext, +interface StepMethod< + $Pipeline extends PipelineDef, + $Overload extends Overload, $InputExtension extends object = {}, > { < - $Name extends $RootContext['steps'][number]['name'], - $Slots extends undefined | Step.Slots = undefined, + $Name extends $Pipeline['steps'][number]['name'], + $Slots extends undefined | StepDef.Slots = undefined, $Input = & InferStepInput< - $Context, - Extract<$RootContext['steps'][number], { name: $Name }>, - Tuple.PreviousItem<$RootContext['steps'], { name: $Name }> + $Overload, + Extract<$Pipeline['steps'][number], { name: $Name }>, + Tuple.PreviousItem<$Pipeline['steps'], { name: $Name }> > & $InputExtension, $Output = unknown, @@ -52,52 +89,30 @@ interface BuilderStep< run: (input: $Input, slots: $Slots) => $Output }, ): Builder< - $RootContext, - ConfigManager.UpdateAtKey< - $Context, - 'steps', - & $Context['steps'] - & { - [_ in $Name]: { - name: $Name - input: $Input - output: Awaited<$Output> - slots: ConfigManager.OrDefault2<$Slots, {}> - } - } - > + $Pipeline, + Overload.Updaters.AddStep<$Overload, $Name, { + name: $Name + input: $Input + output: Awaited<$Output> + slots: ConfigManager.OrDefault2<$Slots, {}> + }> > } // dprint-ignore type InferStepInput< - $OverloadContext extends BuilderContext, - $CurrentStep extends Step, - $PreviousStep extends Step | undefined, + $Overload extends Overload, + $CurrentStep extends StepDef, + $PreviousStep extends StepDef | undefined, > = - $PreviousStep extends Step - ? $PreviousStep['name'] extends keyof $OverloadContext['steps'] - ? $OverloadContext['steps'][$PreviousStep['name']]['output'] + $PreviousStep extends StepDef + ? $PreviousStep['name'] extends keyof $Overload['steps'] + ? $Overload['steps'][$PreviousStep['name']]['output'] : & $CurrentStep['input'] - & $OverloadContext['input'] - & { [_ in $OverloadContext['discriminant'][0]]: $OverloadContext['discriminant'][1] } + & $Overload['input'] + & { [_ in $Overload['discriminant'][0]]: $Overload['discriminant'][1] } : & $CurrentStep['input'] - & $OverloadContext['input'] - & { [_ in $OverloadContext['discriminant'][0]]: $OverloadContext['discriminant'][1] } - -export interface BuilderContext { - discriminant: DiscriminantSpec - input: object - steps: Record -} - -export type DiscriminantSpec = readonly [string, DiscriminantPropertyValue] - -export type DiscriminantPropertyValue = string | number | symbol - -interface BuilderContextEmpty extends BuilderContext { - input: {} - steps: {} -} + & $Overload['input'] + & { [_ in $Overload['discriminant'][0]]: $Overload['discriminant'][1] } diff --git a/src/lib/anyware/Overload/BuilderCallback.ts b/src/lib/anyware/Overload/BuilderCallback.ts new file mode 100644 index 000000000..5e5f34028 --- /dev/null +++ b/src/lib/anyware/Overload/BuilderCallback.ts @@ -0,0 +1,12 @@ +import type { PipelineDef } from '../PipelineDef/__.js' +import type { Create } from './Builder.js' +import type { Builder } from './Builder.js' + +export type BuilderCallback< + $Pipeline extends PipelineDef, + $OverloadBuilder extends Builder<$Pipeline>, +> = ( + OverloadBuilder: { + create: Create<$Pipeline> + }, +) => $OverloadBuilder diff --git a/src/lib/anyware/Overload/Overload.ts b/src/lib/anyware/Overload/Overload.ts deleted file mode 100644 index 0e4d1ca55..000000000 --- a/src/lib/anyware/Overload/Overload.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Pipeline } from '../Pipeline/__.js' -import type { Step } from '../Step.js' -import type { Overload } from './__.js' -import type { Builder, BuilderContext, DiscriminantSpec } from './Builder.js' - -export * from './Builder.js' - -export const create: Create = (parameters) => { - const context_: Omit = { - discriminant: parameters.discriminant, - steps: {}, - } - const context = context_ as BuilderContext - - const builder: Builder = { - context, - extendInput: () => builder as any, - stepWithExtendedInput: () => builder.step as any, - step: (name, spec) => { - context.steps[name] = { - name, - ...spec, - } as unknown as Step - return builder as any - }, - } - - return builder as any -} - -export type Create<$RootContext extends Pipeline.Context = Pipeline.Context> = < - const $DiscriminantSpec extends DiscriminantSpec, ->( - parameters: { discriminant: $DiscriminantSpec }, -) => Builder< - $RootContext, - { - discriminant: $DiscriminantSpec - input: {} - steps: {} - } -> - -export type BuilderCallback< - $Context extends Pipeline.Context, - $OverloadBuilder extends Builder<$Context>, -> = ( - overloadBuilder: { create: Overload.Create<$Context> }, -) => $OverloadBuilder diff --git a/src/lib/anyware/Overload/States.ts b/src/lib/anyware/Overload/States.ts new file mode 100644 index 000000000..6d76e2fc1 --- /dev/null +++ b/src/lib/anyware/Overload/States.ts @@ -0,0 +1,8 @@ +import type { Overload } from './__.js' + +export namespace States { + export interface Empty extends Overload { + input: {} + steps: {} + } +} diff --git a/src/lib/anyware/Overload/Updaters.ts b/src/lib/anyware/Overload/Updaters.ts new file mode 100644 index 000000000..08fbd2001 --- /dev/null +++ b/src/lib/anyware/Overload/Updaters.ts @@ -0,0 +1,22 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import type { StepDef } from '../StepDef.js' +import type { Overload } from './__.js' + +export namespace Updaters { + export type SetInput< + $Overload extends Overload, + $InputExtension extends object, + > = ConfigManager.SetKey<$Overload, 'input', $InputExtension> + + export type AddStep< + $Overload extends Overload, + $Name extends string, + $Step extends StepDef, + > = ConfigManager.UpdateKeyWithIntersection< + $Overload, + 'steps', + { + [_ in $Name]: $Step + } + > +} diff --git a/src/lib/anyware/Overload/_.ts b/src/lib/anyware/Overload/_.ts new file mode 100644 index 000000000..108520aed --- /dev/null +++ b/src/lib/anyware/Overload/_.ts @@ -0,0 +1,4 @@ +export * from './Builder.js' +export * from './BuilderCallback.js' +export * from './States.js' +export * from './Updaters.js' diff --git a/src/lib/anyware/Overload/__.ts b/src/lib/anyware/Overload/__.ts index 3bcdc509b..e0c8d82eb 100644 --- a/src/lib/anyware/Overload/__.ts +++ b/src/lib/anyware/Overload/__.ts @@ -1 +1,12 @@ -export * as Overload from './Overload.js' +import type { DiscriminantPropertyValue } from '../../prelude.js' +import type { StepDef } from '../StepDef.js' + +export * as Overload from './_.js' + +export interface Overload { + discriminant: DiscriminantSpec + input: object + steps: Record +} + +type DiscriminantSpec = readonly [string, DiscriminantPropertyValue] diff --git a/src/lib/anyware/Pipeline/ExecutablePipeline.ts b/src/lib/anyware/Pipeline/ExecutablePipeline.ts deleted file mode 100644 index 31ff84869..000000000 --- a/src/lib/anyware/Pipeline/ExecutablePipeline.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ExecutableStep } from '../ExecutableStep.js' -import type { Step } from '../Step.js' -import type { Config } from './Config.js' -import type { Result } from './Result.js' -import type { PipelineSpec } from './Spec.js' - -export interface ExecutablePipeline { - spec: PipelineSpec - config: Config - input: object - output: Result - steps: ExecutableStep[] - stepsIndex: StepsIndex -} - -export type StepsIndex<$Name extends Step.Name = Step.Name, $ExecutableStep extends ExecutableStep = ExecutableStep> = - Map<$Name, $ExecutableStep> diff --git a/src/lib/anyware/Pipeline/Pipeline.test-d.ts b/src/lib/anyware/Pipeline/Pipeline.test-d.ts new file mode 100644 index 000000000..fdb936841 --- /dev/null +++ b/src/lib/anyware/Pipeline/Pipeline.test-d.ts @@ -0,0 +1,91 @@ +import { expectTypeOf, test } from 'vitest' +import { PipelineDef } from '../_.js' +import type { results } from '../__.test-helpers.js' +import { type initialInput, slots, stepA } from '../__.test-helpers.js' +import type { Config } from '../PipelineDef/Config.js' +import type { Step } from '../Step.js' +import type { StepRunner } from '../StepRunner.js' +import { Pipeline } from './Pipeline.js' + +const b0 = PipelineDef.create().input() + +test(`returns a pipeline`, () => { + const p0 = b0.type + const exP0 = Pipeline.create(p0) + expectTypeOf().toMatchTypeOf< + { config: Config; steps: Step[]; input: initialInput; output: initialInput } + >() + const p1 = b0.step(stepA).type + const exP1 = Pipeline.create(p1) + expectTypeOf().toMatchTypeOf< + { config: Config; steps: Step[]; input: initialInput; output: results['a'] } + >() +}) + +// Overloads Merging Into Pipeline + +const dName = `_` +type dName = typeof dName +const dValue = 1 +type dValue = typeof dValue +const d = [dName, dValue] as const +type d = typeof d +type dObject = { [_ in dName]: dValue } +const dValue2 = 2 +type dValue2 = typeof dValue2 +const d2 = [dName, dValue2] as const +type d2 = typeof d2 +type dObject2 = { [_ in dName]: dValue2 } + +test(`overload input extensions become a pipeline union input`, () => { + const pDef = b0 + .step(`a`) + .overload(o => o.create({ discriminant: d }).extendInput<{ ol1: 1 }>()) + .overload(o => o.create({ discriminant: d2 }).extendInput<{ ol2: 2 }>()) + .type + const p = Pipeline.create(pDef) + expectTypeOf(p.input).toMatchTypeOf< + | (initialInput & dObject & { ol1: 1 }) + | (initialInput & dObject2 & { ol2: 2 }) + >() +}) + +test(`overload step input/output becomes union to step input/output`, () => { + const pDef = + b0.step(`a`).overload(o => + o.create({ discriminant: [dName, dValue] }).step(`a`, { run: () => ({ olb: 1 as const }) }) + ) + .type + const p = Pipeline.create(pDef) + expectTypeOf(p.steps).toMatchTypeOf<[{ + name: 'a' + input: dObject & initialInput + output: dObject & { olb: 1 } + slots: {} + run: StepRunner + }]>() +}) +test(`overloads steps slots all merge onto respective pipeline step (no unions)`, () => { + const pDef = b0 + .step(`a`) + .overload(o => + o.create({ discriminant: d }).step(`a`, { + slots: { m: slots.m }, + run: () => {}, + }) + ) + .overload(o => + o.create({ discriminant: d2 }).step(`a`, { + slots: { n: slots.n }, + run: () => {}, + }) + ) + .type + const p = Pipeline.create(pDef) + expectTypeOf(p.steps).toMatchTypeOf<[ + { + name: 'a' + slots: { m: slots['m']; n: slots['n'] } + }, + ]>() +}) diff --git a/src/lib/anyware/Pipeline/Pipeline.ts b/src/lib/anyware/Pipeline/Pipeline.ts new file mode 100644 index 000000000..1ea1e4d33 --- /dev/null +++ b/src/lib/anyware/Pipeline/Pipeline.ts @@ -0,0 +1,173 @@ +import type { IsUnknown, Simplify } from 'type-fest' +import type { ExcludeUndefined, Tuple } from '../../prelude.js' +import type { PipelineDef } from '../_.js' +import type { Overload } from '../Overload/__.js' +import type { Config } from '../PipelineDef/Config.js' +import type { Step } from '../Step.js' +import type { StepDef } from '../StepDef.js' +import type { StepRunner } from '../StepRunner.js' + +export interface Pipeline { + config: Config + input: object + steps: Step[] + stepsIndex: StepsIndex + output: any +} + +export type StepsIndex< + $Name extends Step['name'] = Step['name'], + $ExecutableStep extends Step = Step, +> = Map<$Name, $ExecutableStep> + +export const createStepsIndex = <$Steps extends Step[]>(steps: $Steps): StepsIndex => { + return new Map(steps.map(step => [step.name, step])) +} + +export namespace Pipeline { + export const create = <$PipelineSpec extends PipelineDef>( + pipeline: $PipelineSpec, + ): Simplify> => { + let steps = pipeline.steps as unknown as Step[] + if (pipeline.overloads.length > 0) { + steps = steps.map((step): Step => { + const stepOverloads = pipeline.overloads + .map(overload => { + const stepOverload = overload.steps[step.name] + if (!stepOverload) return null + return { + ...(stepOverload as unknown as Step), + discriminant: overload.discriminant, + } + }) + .filter(_ => _ !== null) + + const stepWithOverloads = { + name: step.name, + run: (...args: Parameters) => { + const input = args[0] as Record + const stepOverload = stepOverloads.find(stepOverload => { + return input[stepOverload.discriminant[0]] === stepOverload.discriminant[1] + }) + if (stepOverload) return stepOverload.run(...args) + return step.run(...args) + }, + slots: { + ...step.slots, + ...stepOverloads.reduce((acc, stepOverload) => { + return { + ...acc, + ...stepOverload.slots, + } + }, {}), + }, + } + + return stepWithOverloads as Step + }) + } + + const stepsIndex = createStepsIndex(steps) + + return { + ...pipeline, + steps, // todo: it is a bug to not have this here, but removing it and no anyware test breaks! + stepsIndex, + } as any + } + + // dprint-ignore + export type InferFromDefinition<$PipelineDef extends PipelineDef> = + InferFromDefinition_<$PipelineDef, InferSteps<$PipelineDef>> + // dprint-ignore + export type InferFromDefinition_< + $PipelineDef extends PipelineDef, + $Steps extends Step[], + > = { + config: $PipelineDef['config'] + steps: $Steps + stepsIndex: StepsIndex< + $PipelineDef['steps'][number]['name'], + $Steps[number] + > + input: $Steps extends Tuple.NonEmpty + ? $Steps[0]['input'] + : $PipelineDef['input'] + output: + $Steps extends Tuple.NonEmpty + ? Awaited['output']> + : $PipelineDef['input'] + } +} + +// dprint-ignore +type InferSteps<$PipelineDef extends PipelineDef> = + InferSteps_<$PipelineDef['steps'], $PipelineDef> +// dprint-ignore +type InferSteps_< + $StepDefs extends StepDef[], + $PipelineDef extends PipelineDef, +> = { + [$Index in keyof $StepDefs]: { + name: $StepDefs[$Index]['name'] + input: Simplify< + Tuple.IsEmpty<$PipelineDef['overloads']> extends true + ? $StepDefs[$Index]['input'] + : InferStepInput< + $Index, + $StepDefs[$Index], + $PipelineDef['overloads'][number] + > + > + output: Simplify< + Tuple.IsEmpty<$PipelineDef['overloads']> extends true + ? $StepDefs[$Index]['output'] + : InferStepOutput< + $StepDefs[$Index], + $PipelineDef['overloads'][number] + > + > + slots: Tuple.IsEmpty<$PipelineDef['overloads']> extends true + ? $StepDefs[$Index]['slots'] + : InferStepSlots< + $StepDefs[$Index], + $PipelineDef['overloads'] + > + run: IsUnknown<$StepDefs[$Index]['run']> extends true + ? StepRunner + : ExcludeUndefined<$StepDefs[$Index]['run']> + } +} + +type InferStepSlots<$Step extends StepDef, $Overloads extends Overload[]> = + & $Step['slots'] + & InferStepSlots_<$Step, $Overloads> +// dprint-ignore +type InferStepSlots_<$Step extends StepDef, $Overloads extends Overload[]> = + Tuple.IntersectItems<{ + [$Index in keyof $Overloads]: + IsUnknown<$Overloads[$Index]['steps'][$Step['name']]> extends true + ? unknown + : $Overloads[$Index]['steps'][$Step['name']]['slots'] + }> + +// dprint-ignore +type InferStepOutput<$Step extends StepDef, $Overload extends Overload> = $Overload extends never ? never : + & $Step['output'] + & { [_ in $Overload['discriminant'][0]]: $Overload['discriminant'][1] } + & $Overload['steps'][$Step['name']]['output'] + +// dprint-ignore +type InferStepInput< + $StepIndex extends Tuple.IndexKey, + $StepDef extends StepDef, + $Overload extends Overload, +> = $Overload extends never ? never : + & $StepDef['input'] + // Overload Contributions: + // 1. The discriminant: + & { [_ in $Overload['discriminant'][0]]: $Overload['discriminant'][1] } + // 2. This specific step: + & $Overload['steps'][$StepDef['name']]['input'] + // 3. If this is the first step, then the pipeline input contributions, if any: + & ($StepIndex extends '0' ? $Overload['input'] : {}) diff --git a/src/lib/anyware/Pipeline/Spec.test-d.ts b/src/lib/anyware/Pipeline/Spec.test-d.ts deleted file mode 100644 index 5d7ba9e38..000000000 --- a/src/lib/anyware/Pipeline/Spec.test-d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { assertEqual } from '../../assert-equal.js' -import type { MaybePromise } from '../../prelude.js' -import type { PipelineSpecFromSteps } from './Spec.js' - -assertEqual< - PipelineSpecFromSteps<[]>, - { steps: []; input: object; output: unknown } ->() - -assertEqual< - PipelineSpecFromSteps<[{ name: 'a' }]>, - { steps: [{ name: 'a'; slots: {}; input: object; output: unknown }]; input: object; output: unknown } ->() - -assertEqual< - PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]>, - { - steps: [{ name: 'a'; slots: {}; input: object; output: MaybePromise<1> }] - input: object - output: 1 - // ^^^^^^ pipeline output inferred from last step output - } ->() - -assertEqual< - PipelineSpecFromSteps<[{ name: 'a' }, { name: 'b'; input: { x: 1 } }]>, - { - steps: [ - { name: 'a'; slots: {}; input: object; output: MaybePromise<{ x: 1 }> }, - // step output inferred from next step input ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - { name: 'b'; slots: {}; input: { x: 1 }; output: unknown }, - ] - input: object - output: unknown - } ->() - -assertEqual< - PipelineSpecFromSteps<[{ name: 'a'; output: Promise<1> }]>, - // ^^^^^^^^^^^^^^^^^^ Step promised output flattened within MaybePromise - { - steps: [{ name: 'a'; slots: {}; input: object; output: MaybePromise<1> }] - // ^^^^^^^^^^^^^^^ - input: object - output: 1 - // ^^^^^^^^^ Pipeline output strips Promise type - } ->() diff --git a/src/lib/anyware/Pipeline/Spec.ts b/src/lib/anyware/Pipeline/Spec.ts deleted file mode 100644 index 49fa6cb7c..000000000 --- a/src/lib/anyware/Pipeline/Spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { IsUnknown } from 'type-fest' -import type { ConfigManager } from '../../config-manager/__.js' -import type { MaybePromise, Tuple } from '../../prelude.js' -import type { Step } from '../Step.js' - -// dprint-ignore -export type PipelineSpecFromSteps<$StepSpecInputs extends Step.SpecInput[] = Step.SpecInput[]> = PipelineSpec> - -// dprint-ignore -export interface PipelineSpec<$StepSpecs extends Step[] = Step[]> { - steps: $StepSpecs - input: $StepSpecs extends Tuple.NonEmpty - ? $StepSpecs[0]['input'] - : object - output: Awaited< - $StepSpecs extends Tuple.NonEmpty - ? Tuple.GetLastValue<$StepSpecs>['output'] - : unknown - > -} - -type InferStepSpecs<$StepSpecInputs extends Step.SpecInput[]> = InferStepSpecs_ - -// dprint-ignore -type InferStepSpecs_<$StepSpecPrevious extends Step| undefined, $StepSpecInputs extends Step.SpecInput[]> = - $StepSpecInputs extends [infer $StepSpecInput extends Step.SpecInput, ...infer $StepSpecInputsRest extends Step.SpecInput[]] - ? InferStepSpecs__<{ - name: $StepSpecInput['name'] - slots: ConfigManager.OrDefault2<$StepSpecInput['slots'],{}> - input: IsUnknown<$StepSpecInput['input']> extends true - ? $StepSpecPrevious extends Step - ? $StepSpecPrevious['output'] - : object - : $StepSpecInput['input'] - output: MaybePromise< - Awaited< - IsUnknown<$StepSpecInput['output']> extends true - ? $StepSpecInputsRest extends Tuple.NonEmpty - ? $StepSpecInputsRest[0]['input'] extends undefined - ? unknown - : $StepSpecInputsRest[0]['input'] - : unknown - : $StepSpecInput['output'] - > - > - }, $StepSpecInputsRest> - : [] - -type InferStepSpecs__<$StepSpec extends Step, $StepSpecInputsRest extends Step.SpecInput[]> = [ - $StepSpec, - ...InferStepSpecs_<$StepSpec, $StepSpecInputsRest>, -] diff --git a/src/lib/anyware/Pipeline/_.ts b/src/lib/anyware/Pipeline/_.ts deleted file mode 100644 index 2fc873a19..000000000 --- a/src/lib/anyware/Pipeline/_.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from '../run/run.js' -export * from './builder.js' -export * from './createWithSpec.js' -export * from './ExecutablePipeline.js' -export * from './Result.js' -export * from './Spec.js' diff --git a/src/lib/anyware/Pipeline/__.ts b/src/lib/anyware/Pipeline/__.ts deleted file mode 100644 index 5ffe95bb1..000000000 --- a/src/lib/anyware/Pipeline/__.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Pipeline from './_.js' diff --git a/src/lib/anyware/Pipeline/builder.ts b/src/lib/anyware/Pipeline/builder.ts deleted file mode 100644 index c9c8cf256..000000000 --- a/src/lib/anyware/Pipeline/builder.ts +++ /dev/null @@ -1,408 +0,0 @@ -import type { IsUnknown, Simplify } from 'type-fest' -import type { ConfigManager } from '../../config-manager/__.js' -import type { ExcludeUndefined } from '../../prelude.js' -import { type Tuple } from '../../prelude.js' -import type { ExecutableStep } from '../ExecutableStep.js' -import type { Extension } from '../Extension/__.js' -import { Overload } from '../Overload/__.js' -import type { Step } from '../Step.js' -import { type Config, type Options, resolveOptions } from './Config.js' -import { createExecutableStepsIndex } from './createWithSpec.js' -import type { StepsIndex } from './ExecutablePipeline.js' -import type { Result } from './Result.js' - -interface ContextStep extends Step { - /** - * Tracking the run signature type is useful for deriving the executable step. - * For example if Vitest mocks were used for the step run functions, their type - * would be carried through to the executable step. This is useful for testing. - * - * If we only relied on the spec types, which don't track the given run type itself, - * they Vitest mock type would not be carried through. - * - * The executable step is not design for public use. Testing is an exception. - * - * This run signature is NOT used for deriving the specification step. - */ - run?: Step.Runner -} - -export interface Context { - config: Config - input: object - steps: ContextStep[] - overloads: Overload.BuilderContext[] -} - -export interface ContextEmpty extends Context { - input: object - steps: [] - config: Config - overloads: [] -} - -/** - * Get the `input` parameter for a step that would be appended to the given Pipeline. - * - * Recall that non-first steps have input corresponding to the output of the previous step. - * - * So this returns: - * - If the pipeline has no steps then the pipeline input itself. - * - Otherwise the last step's output. - */ -// dprint-ignore -type GetNextStepParameterInput<$Context extends Context> = - $Context['steps'] extends Tuple.NonEmpty - ? Awaited['output']> - : $Context['input'] - -export interface Builder<$Context extends Context = Context> { - context: $Context - /** - * TODO - */ - stepWithRunnerType: <$Runner extends Step.Runner>() => < - const $Name extends string, - $Slots extends - | undefined - | Step.Slots = undefined, - >( - name: $Name, - parameters?: { - slots?: $Slots - run?: $Runner - }, - ) => Builder< - ConfigManager.UpdateAtKey< - $Context, - 'steps', - [ - ...$Context['steps'], - { - name: $Name - input: Parameters<$Runner>[0] - output: ConfigManager.OrDefault2, {}> - slots: ConfigManager.OrDefault2<$Slots, {}> - run: $Runner - }, - ] - > - > - /** - * TODO - */ - step: BuilderStep<$Context> - /** - * TODO - */ - overload: <$OverloadBuilder extends Overload.Builder<$Context>>( - overloadBuilder: Overload.BuilderCallback<$Context, $OverloadBuilder>, - ) => Builder< - ConfigManager.AppendAtKey<$Context, 'overloads', $OverloadBuilder['context']> - > - /** - * TODO - */ - // todo test this - use: <$Extension extends Extension.Builder>( - extension: $Extension, - ) => Builder< - ConfigManager.AppendManyAtKey< - $Context, - 'overloads', - $Extension['context']['overloads'] - > - > - /** - * TODO - */ - done: () => InferPipelineFromContext<$Context> -} - -interface BuilderStep<$Context extends Context> { - < - const $Name extends string, - $Slots extends - | undefined - | Step.Slots = undefined, - $Input = GetNextStepParameterInput<$Context>, - $Output = unknown, - >( - name: $Name, - parameters?: { - slots?: $Slots - run?: (input: $Input, slots: $Slots, previous: GetNextStepParameterPrevious<$Context>) => $Output - }, - ): Builder< - ConfigManager.UpdateAtKey< - $Context, - 'steps', - [ - ...$Context['steps'], - { - name: $Name - input: $Input - output: ConfigManager.OrDefault2<$Output, {}> - slots: ConfigManager.OrDefault2<$Slots, {}> - }, - ] - > - > - < - const $Name extends string, - $Slots extends - | undefined - | Step.Slots = undefined, - $Input extends object = GetNextStepParameterInput<$Context>, - $Output = unknown, - >( - parameters: { - name: $Name - slots?: $Slots - run?: (input: $Input, slots: $Slots, previous: GetNextStepParameterPrevious<$Context>) => $Output - }, - ): Builder< - ConfigManager.UpdateAtKey< - $Context, - 'steps', - [ - ...$Context['steps'], - { - name: $Name - input: $Input - output: ConfigManager.OrDefault2<$Output, {}> - slots: ConfigManager.OrDefault2<$Slots, {}> - }, - ] - > - > -} - -// dprint-ignore -export type GetNextStepParameterPrevious<$Context extends Context> = - $Context['steps'] extends Tuple.NonEmpty - ? GetNextStepPrevious_<$Context['steps']> - : undefined - -type GetNextStepPrevious_<$Steps extends Step[]> = Tuple.IntersectItems< - { - [$Index in keyof $Steps]: { - [$StepName in $Steps[$Index]['name']]: { - input: Awaited<$Steps[$Index]['input']> - output: Awaited<$Steps[$Index]['output']> - } - } - } -> - -export type InferPipeline<$Builder extends Builder> = InferPipelineFromContext<$Builder['context']> - -// dprint-ignore -type InferPipelineFromContext<$Context extends Context> = - & { - spec: { - config: $Context['config'] - input: InferInput<$Context> - output: InferOutput<$Context> - steps: InferSteps<$Context> - } - config: $Context['config'] - input: InferInput<$Context> - steps: InferExecutableSteps<$Context> - stepsIndex: StepsIndex<$Context['steps'][number]['name'], InferExecutableSteps<$Context>[number]> - output: Result> - } - -// dprint-ignore -type InferOutput<$Context extends Context> = Awaited< - $Context['steps'] extends Tuple.NonEmpty - ? Tuple.GetLastValue<$Context['steps']>['output'] - : $Context['input'] -> - -type InferExecutableSteps<$Context extends Context> = InferExecutableSteps_<$Context['steps'], $Context> - -type InferExecutableSteps_<$Steps extends ContextStep[], _$Context extends Context> = { - [$Index in keyof $Steps]: { - name: $Steps[$Index]['name'] - slots: $Steps[$Index]['slots'] - // We exclude undefined here because at the type level it could be coming from the overloads which isn't considered here. - // We know that at runtime an executable function will be defined. - run: IsUnknown<$Steps[$Index]['run']> extends true ? Step.Runner - : ExcludeUndefined<$Steps[$Index]['run']> - } -} - -type InferSteps<$Context extends Context> = InferSteps_<$Context['steps'], $Context> - -// dprint-ignore -type InferSteps_<$Steps extends Step[], $Context extends Context> = { - [$Index in keyof $Steps]: - $Context['overloads'] extends [] - ? $Steps[$Index] - : { - name: $Steps[$Index]['name'] - input: Simplify> - output: Simplify> - slots: Simplify> - } -} - -type InferStepSlots<$Step extends Step, $Overloads extends Overload.BuilderContext[]> = - & $Step['slots'] - & InferStepSlots_<$Step, $Overloads> - -// dprint-ignore -type InferStepSlots_<$Step extends Step, $Overloads extends Overload.BuilderContext[]> = - Tuple.IntersectItems<{ - [$Index in keyof $Overloads]: - IsUnknown<$Overloads[$Index]['steps'][$Step['name']]> extends true - ? unknown - : $Overloads[$Index]['steps'][$Step['name']]['slots'] - }> - -type InferStepOutput<$Step extends Step, $Overloads extends Overload.BuilderContext[]> = { - [$Index in keyof $Overloads]: - & $Step['output'] - & { - [_ in $Overloads[$Index]['discriminant'][0]]: $Overloads[$Index]['discriminant'][1] - } - & $Overloads[$Index]['steps'][$Step['name']]['output'] -}[number] - -type InferStepInput<$Step extends Step, $Overloads extends Overload.BuilderContext[]> = { - [$Index in keyof $Overloads]: - & $Step['input'] - & { - [_ in $Overloads[$Index]['discriminant'][0]]: $Overloads[$Index]['discriminant'][1] - } - & $Overloads[$Index]['steps'][$Step['name']]['input'] -}[number] - -// dprint-ignore -type InferInput<$Context extends Context> = Simplify< - $Context['overloads'] extends [] - ? $Context['input'] - // // todo: is this needed? - // : $Context['overloads'] extends $Context['overloads'] - // ? $Context['input'] - // // todo maybe just this. - : InferInput_<$Context['input'], $Context['overloads']> -> -type InferInput_<$BaseInput extends object, $Overloads extends Overload.BuilderContext[]> = { - [$Index in keyof $Overloads]: - & $BaseInput - & $Overloads[$Index]['input'] - & { - [_ in $Overloads[$Index]['discriminant'][0]]: $Overloads[$Index]['discriminant'][1] - } -}[number] - -/** - * TODO - */ -export const create = <$Input extends object>(options?: Options): Builder<{ - input: $Input - steps: [] - config: Config - overloads: [] -}> => { - const config = resolveOptions(options) - return recreate({ - steps: [], - config, - overloads: [], - } as any as Context) as any -} - -const recreate = <$Context extends Context>(context: $Context): Builder<$Context> => { - const builder: Builder<$Context> = { - context, - stepWithRunnerType: () => builder.step as any, - step: (...args: any[]) => { - const step = typeof args[0] === `string` - ? { - name: args[0], - run: passthroughStep, - ...(args[1] as undefined | object), - } - : { - run: passthroughStep, - ...args[0], - } - - return recreate({ - ...context, - steps: [ - ...context.steps, - step, - ], - } as any) - }, - use: (extension) => { - return recreate({ - ...context, - overloads: [ - ...context.overloads, - ...extension.context.overloads, - ], - } as any) - }, - overload: (builderCallback) => { - const overload = builderCallback({ create: Overload.create }) - // todo why mutating here? stop it. make like use extension. - context.overloads.push(overload.context) - - return recreate(context) as any - }, - done: () => { - let steps = context.steps as unknown as ExecutableStep[] - if (context.overloads.length > 0) { - steps = steps.map((step): ExecutableStep => { - const stepOverloads = context.overloads - .map(overload => { - const stepOverload = overload.steps[step.name] - if (!stepOverload) return null - return { - ...(stepOverload as unknown as ExecutableStep), - discriminant: overload.discriminant, - } - }) - .filter(_ => _ !== null) - return { - name: step.name, - run: (...args: Parameters) => { - const input = args[0] as Record - const stepOverload = stepOverloads.find(stepOverload => { - return input[stepOverload.discriminant[0]] === stepOverload.discriminant[1] - }) - if (stepOverload) return stepOverload.run(...args) - return step.run(...args) - }, - slots: { - ...step.slots, - ...stepOverloads.reduce((acc, stepOverload) => { - return { - ...acc, - ...stepOverload.slots, - } - }, {}), - }, - } - }) - } - - const stepsIndex = createExecutableStepsIndex(steps) - - return { - ...context, - steps, // todo: it is a bug to not have this here, but removing it and no anyware test breaks! - stepsIndex, - } as any - }, - } - - return builder -} - -const passthroughStep = (params: { input: object }) => params.input diff --git a/src/lib/anyware/Pipeline/createWithSpec.test-d.ts b/src/lib/anyware/Pipeline/createWithSpec.test-d.ts deleted file mode 100644 index 188bc1f23..000000000 --- a/src/lib/anyware/Pipeline/createWithSpec.test-d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { assertEqual } from '../../assert-equal.js' -import type { MaybePromise } from '../../prelude.js' -import type { Config } from './Config.js' -import { createWithSpec } from './createWithSpec.js' -import type { StepsIndex } from './ExecutablePipeline.js' -import type { PipelineSpecFromSteps } from './Spec.js' - -{ - type Spec = PipelineSpecFromSteps<[]> - const p = createWithSpec({ steps: [] }) - assertEqual() -} - -{ - type Spec = PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]> - const p = createWithSpec({ steps: [{ name: `a`, run: () => 1 }] }) - type p = typeof p - assertEqual< - p, - { - config: Config - input: object - output: 1 - steps: [ - { run: (...arg: any[]) => any; name: `a`; input: object; output: MaybePromise<1> }, - ] - stepsIndex: StepsIndex - } - >() -} diff --git a/src/lib/anyware/Pipeline/createWithSpec.ts b/src/lib/anyware/Pipeline/createWithSpec.ts deleted file mode 100644 index d8cd965ea..000000000 --- a/src/lib/anyware/Pipeline/createWithSpec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { ConfigManager } from '../../config-manager/__.js' -import type { Objekt, Tuple } from '../../prelude.js' -import type { ExecutableStep } from '../ExecutableStep.js' -import type { Step } from '../Step.js' -import { type Config, type Options, resolveOptions } from './Config.js' -import type { StepsIndex } from './ExecutablePipeline.js' -import type { PipelineSpec } from './Spec.js' - -export const createExecutableStepsIndex = <$Steps extends ExecutableStep[]>(steps: $Steps): StepsIndex => { - return new Map(steps.map(step => [step.name, step])) -} - -export const createWithSpec = <$PipelineSpec extends PipelineSpec>( - input: { - options?: Options - steps: InferStepsInput<$PipelineSpec['steps']> - }, -): InferPipelineExecutable<$PipelineSpec> => { - const config = resolveOptions(input.options) - const stepsIndex = createExecutableStepsIndex(input.steps) - return { - config, - stepsIndex, - steps: input.steps, - } as any -} - -type InferPipelineExecutable<$PipelineSpec extends PipelineSpec> = { - input: $PipelineSpec['input'] - output: $PipelineSpec['output'] - steps: InferExecutableSteps<$PipelineSpec['steps']> - stepsIndex: StepsIndex - config: Config -} - -type InferStepsInput<$NextStepDefinitions extends Step[]> = InferExecutableSteps_< - [], - $NextStepDefinitions, - { types: false } -> - -type InferExecutableSteps<$NextStepDefinitions extends Step[]> = InferExecutableSteps_< - [], - $NextStepDefinitions, - { types: true } -> - -// dprint-ignore -type InferExecutableSteps_< - $PreviousStepSpecs extends Step[], - $NextStepSpecs extends Step[], - $Options extends { types: boolean }, -> = - $NextStepSpecs extends [infer $StepSpec extends Step, ...infer $RestStepSpecs extends Step[]] - ? [ - & ( - $Options['types'] extends true - ? { - input: $StepSpec['input'] - output: $StepSpec['output'] - } - : {} - ) - & { - name: $StepSpec['name'] - run: (parameters: - & { - input: $StepSpec['input'] - previous: GetParameterPrevious<$PreviousStepSpecs> - } - & ( - $StepSpec['slots'] extends Step.Slots - ? { - slots: $StepSpec['slots'] - } - : {} - ) - ) => $StepSpec['output'] - } - & ( - Objekt.IsEmpty<$StepSpec['slots']> extends true - ? {} - : { - slots: $StepSpec['slots'] - } - ) - , ...InferExecutableSteps_<[...$PreviousStepSpecs, $StepSpec], $RestStepSpecs, $Options>] - : [] - -export type GetParameterPrevious<$StepDefinitions extends Step[]> = Tuple.IntersectItems< - { - [$Index in keyof $StepDefinitions]: { - [$StepName in $StepDefinitions[$Index]['name']]: { - input: $StepDefinitions[$Index]['input'] - output: ConfigManager.OrDefault< - $StepDefinitions[$Index]['output'], - Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] - > - } - } - } -> diff --git a/src/lib/anyware/Pipeline/run.test-d.ts b/src/lib/anyware/Pipeline/run.test-d.ts deleted file mode 100644 index cb1ba19c7..000000000 --- a/src/lib/anyware/Pipeline/run.test-d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expectTypeOf, test } from 'vitest' -import type { initialInput } from '../__.test-helpers.js' -import { Pipeline } from './__.js' -import type { Result } from './Result.js' - -test(`returns input if no steps`, async () => { - const p = Pipeline.create().done() - const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf>() -}) - -test(`returns last step output if steps`, async () => { - const p = Pipeline.create().step({ name: `a`, run: () => 2 as const }).done() - const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf>() -}) - -test(`can return a promise`, async () => { - const p = Pipeline.create().step({ name: `a`, run: () => Promise.resolve(2 as const) }).done() - const r = await Pipeline.run(p) - expectTypeOf(r).toEqualTypeOf>() -}) diff --git a/src/lib/anyware/Pipeline/Config.ts b/src/lib/anyware/PipelineDef/Config.ts similarity index 100% rename from src/lib/anyware/Pipeline/Config.ts rename to src/lib/anyware/PipelineDef/Config.ts diff --git a/src/lib/anyware/PipelineDef/States.ts b/src/lib/anyware/PipelineDef/States.ts new file mode 100644 index 000000000..caa8991c5 --- /dev/null +++ b/src/lib/anyware/PipelineDef/States.ts @@ -0,0 +1,8 @@ +import type { PipelineDef } from './__.js' + +export namespace States { + export interface Empty extends PipelineDef { + steps: [] + overloads: [] + } +} diff --git a/src/lib/anyware/PipelineDef/Updaters.ts b/src/lib/anyware/PipelineDef/Updaters.ts new file mode 100644 index 000000000..d1358a8a1 --- /dev/null +++ b/src/lib/anyware/PipelineDef/Updaters.ts @@ -0,0 +1,26 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import type { Overload } from '../Overload/__.js' +import type { StepDef } from '../StepDef.js' +import type { PipelineDef } from './__.js' + +export namespace Updaters { + export type SetInput< + $Pipeline extends PipelineDef, + $Input extends object, + > = ConfigManager.SetKey<$Pipeline, 'input', $Input> + + export type AddStep< + $Pipeline extends PipelineDef, + $Step extends StepDef, + > = ConfigManager.UpdateKeyWithAppend<$Pipeline, 'steps', $Step> + + export type AddOverload< + $Pipeline extends PipelineDef, + $Overload extends Overload, + > = ConfigManager.UpdateKeyWithAppend<$Pipeline, 'overloads', $Overload> + + export type AddOverloads< + $Pipeline extends PipelineDef, + $Overloads extends Overload[], + > = ConfigManager.UpdateKeyWithAppendMany<$Pipeline, 'overloads', $Overloads> +} diff --git a/src/lib/anyware/PipelineDef/Utilities.ts b/src/lib/anyware/PipelineDef/Utilities.ts new file mode 100644 index 000000000..8307c8d5a --- /dev/null +++ b/src/lib/anyware/PipelineDef/Utilities.ts @@ -0,0 +1,17 @@ +import type { Tuple } from '../../prelude.js' +import type { Result } from '../Result.js' +import type { PipelineDef } from './__.js' + +export namespace Utilities { + // dprint-ignore + export type InferOutput<$PipelineDef extends PipelineDef> = + Awaited< + $PipelineDef['steps'] extends Tuple.NonEmpty + ? Tuple.GetLastValue<$PipelineDef['steps']>['output'] + : $PipelineDef['input'] + > + + // dprint-ignore + export type InferResult<$PipelineDef extends PipelineDef> = + Result> +} diff --git a/src/lib/anyware/PipelineDef/_.ts b/src/lib/anyware/PipelineDef/_.ts new file mode 100644 index 000000000..1f91ca8fc --- /dev/null +++ b/src/lib/anyware/PipelineDef/_.ts @@ -0,0 +1,9 @@ +export * from '../Pipeline/Pipeline.js' +export * from '../PipelineSpecification/createWithSpec.js' +export * from '../PipelineSpecification/Spec.js' +export * from '../Result.js' +export * from '../run/run.js' +export * from './builder.js' +export * from './States.js' +export * from './Updaters.js' +export * from './Utilities.js' diff --git a/src/lib/anyware/PipelineDef/__.ts b/src/lib/anyware/PipelineDef/__.ts new file mode 100644 index 000000000..7fb8db05d --- /dev/null +++ b/src/lib/anyware/PipelineDef/__.ts @@ -0,0 +1,12 @@ +import type { Overload } from '../Overload/__.js' +import type { StepDef } from '../StepDef.js' +import type { Config } from './Config.js' + +export * as PipelineDef from './_.js' + +export interface PipelineDef { + config: Config + input: object + steps: StepDef[] + overloads: Overload[] +} diff --git a/src/lib/anyware/Pipeline/builder.test-d.ts b/src/lib/anyware/PipelineDef/builder.test-d.ts similarity index 62% rename from src/lib/anyware/Pipeline/builder.test-d.ts rename to src/lib/anyware/PipelineDef/builder.test-d.ts index 463b10b12..35fbf3371 100644 --- a/src/lib/anyware/Pipeline/builder.test-d.ts +++ b/src/lib/anyware/PipelineDef/builder.test-d.ts @@ -1,18 +1,15 @@ -import type { Simplify } from 'type-fest' import { describe, expectTypeOf, test } from 'vitest' import { _ } from '../../prelude.js' import type { initialInput } from '../__.test-helpers.js' import { results, slots, stepA, stepB } from '../__.test-helpers.js' -import type { ExecutableStep } from '../ExecutableStep.js' -import { Pipeline } from './__.js' +import { PipelineDef } from './__.js' import type { Config } from './Config.js' -import type { Result } from './Result.js' -const b0 = Pipeline.create() -const b1 = Pipeline.create().step(stepA) +const b0 = PipelineDef.create().input() +const b1 = PipelineDef.create().input().step(stepA) test(`initial context`, () => { - expectTypeOf(b0.context).toEqualTypeOf<{ input: initialInput; steps: []; config: Config; overloads: [] }>() + expectTypeOf(b0.type).toMatchTypeOf<{ input: initialInput; steps: []; config: Config; overloads: [] }>() }) test(`first step definition`, () => { @@ -23,17 +20,17 @@ test(`first step definition`, () => { test(`can force an input type while inferring rest`, () => { const b1 = b0.step(`a`, { run: (_: { x: 9 }) => {} }) - expectTypeOf(b1.context.steps[0].name).toEqualTypeOf<'a'>() - expectTypeOf(b1.context.steps[0].input).toEqualTypeOf<{ x: 9 }>() + expectTypeOf(b1.type.steps[0].name).toEqualTypeOf<'a'>() + expectTypeOf(b1.type.steps[0].input).toEqualTypeOf<{ x: 9 }>() }) test(`step can omit run, output defaults to object`, () => { const b1 = b0.step(`a`) - expectTypeOf(b1.context.steps[0].input).toEqualTypeOf<{ readonly x: 1 }>() - expectTypeOf(b1.context.steps[0].output).toEqualTypeOf<{}>() + expectTypeOf(b1.type.steps[0].input).toEqualTypeOf<{ readonly x: 1 }>() + expectTypeOf(b1.type.steps[0].output).toEqualTypeOf<{}>() const b2 = b0.step(`a`).step(`b`) - expectTypeOf(b2.context.steps[1].input).toEqualTypeOf<{}>() - expectTypeOf(b2.context.steps[1].output).toEqualTypeOf<{}>() + expectTypeOf(b2.type.steps[1].input).toEqualTypeOf<{}>() + expectTypeOf(b2.type.steps[1].output).toEqualTypeOf<{}>() }) test(`second step definition`, () => { @@ -50,7 +47,7 @@ test(`second step definition`, () => { }, ) => any >() - expectTypeOf(p1.context).toMatchTypeOf< + expectTypeOf(p1.type).toMatchTypeOf< { input: initialInput steps: [{ name: 'a'; slots: {} }] @@ -80,7 +77,7 @@ test(`step definition with slots`, () => { return results.a }, }) - expectTypeOf(p1.context).toMatchTypeOf< + expectTypeOf(p1.type).toMatchTypeOf< { input: initialInput config: Config @@ -89,17 +86,6 @@ test(`step definition with slots`, () => { >() }) -test(`.done() returns a pipeline`, () => { - const p0 = b0.done() - expectTypeOf().toMatchTypeOf< - { config: Config; steps: ExecutableStep[]; input: initialInput; output: Result } - >() - const p1 = b0.step(stepA).done() - expectTypeOf().toMatchTypeOf< - { config: Config; steps: ExecutableStep[]; input: initialInput; output: Result } - >() -}) - describe(`overload`, () => { const dName = `_` type dName = typeof dName @@ -108,11 +94,6 @@ describe(`overload`, () => { const d = [dName, dValue] as const type d = typeof d type dObject = { [_ in dName]: dValue } - const dValue2 = 2 - type dValue2 = typeof dValue2 - const d2 = [dName, dValue2] as const - type d2 = typeof d2 - type dObject2 = { [_ in dName]: dValue2 } // constructor @@ -122,7 +103,7 @@ describe(`overload`, () => { b0.overload(o => o.create()) }) test(`overload constructor with discriminant`, () => { - expectTypeOf(b0.overload(o => o.create({ discriminant: d })).context.overloads).toMatchTypeOf< + expectTypeOf(b0.overload(o => o.create({ discriminant: d })).type.overloads).toMatchTypeOf< [{ discriminant: d; input: {}; steps: {} }] >() }) @@ -131,7 +112,7 @@ describe(`overload`, () => { // overload extends input test(`overload constructor with input and discriminant`, () => { - expectTypeOf(b0.overload(o => o.create({ discriminant: d }).extendInput<{ x: 1 }>()).context.overloads) + expectTypeOf(b0.overload(o => o.create({ discriminant: d }).extendInput<{ x: 1 }>()).type.overloads) .toMatchTypeOf<[{ discriminant: d; input: { x: 1 }; steps: {} }]>() }) @@ -153,7 +134,7 @@ describe(`overload`, () => { o .create({ discriminant: d }) .step(`a`, { run: (input) => ({ ...input, ola: 1 as const }) }) - ).context.overloads, + ).type.overloads, ) .toMatchTypeOf<[ { @@ -179,7 +160,7 @@ describe(`overload`, () => { expectTypeOf(input).toEqualTypeOf() }, }) - ).context.overloads, + ).type.overloads, ) .toMatchTypeOf<[ { @@ -207,7 +188,7 @@ describe(`overload`, () => { }, }) ) - expectTypeOf(b1o.context.overloads[0].steps.a.slots).toEqualTypeOf<{}>() + expectTypeOf(b1o.type.overloads[0].steps.a.slots).toEqualTypeOf<{}>() }) test(`slots available to run and added to overload context`, () => { @@ -219,7 +200,7 @@ describe(`overload`, () => { }, }) ) - expectTypeOf(b1o.context.overloads).toMatchTypeOf<[{ + expectTypeOf(b1o.type.overloads).toMatchTypeOf<[{ steps: { a: { name: 'a' @@ -242,58 +223,4 @@ describe(`overload`, () => { }) ) }) - - // Overloads Merging Into Pipeline - - test(`overload input extensions become a pipeline union input`, () => { - const p = b0 - .step(`a`) - .overload(o => o.create({ discriminant: d }).extendInput<{ ol1: 1 }>()) - .overload(o => o.create({ discriminant: d2 }).extendInput<{ ol2: 2 }>()) - .done() - expectTypeOf(p.input).toMatchTypeOf< - | (initialInput & dObject & { ol1: 1 }) - | (initialInput & dObject2 & { ol2: 2 }) - >() - expectTypeOf(p.spec.input).toMatchTypeOf< - | (initialInput & dObject & { ol1: 1 }) - | (initialInput & dObject2 & { ol2: 2 }) - >() - }) - - test(`overload step input/output becomes union to step input/output`, () => { - const p = b0.step(`a`).overload(o => - o.create({ discriminant: [dName, dValue] }).step(`a`, { run: () => ({ olb: 1 as const }) }) - ) - .done() - expectTypeOf(p.spec.steps).toEqualTypeOf<[{ - name: 'a' - input: Simplify - output: Simplify - slots: {} - }]>() - }) - test(`overloads steps slots all merge onto respective pipeline step (no unions)`, () => { - const p = b0 - .step(`a`) - .overload(o => - o.create({ discriminant: d }).step(`a`, { - slots: { m: slots.m }, - run: () => {}, - }) - ) - .overload(o => - o.create({ discriminant: d2 }).step(`a`, { - slots: { n: slots.n }, - run: () => {}, - }) - ) - .done() - expectTypeOf(p.spec.steps).toMatchTypeOf<[ - { - name: 'a' - slots: { m: slots['m']; n: slots['n'] } - }, - ]>() - }) }) diff --git a/src/lib/anyware/PipelineDef/builder.ts b/src/lib/anyware/PipelineDef/builder.ts new file mode 100644 index 000000000..9b4e9653c --- /dev/null +++ b/src/lib/anyware/PipelineDef/builder.ts @@ -0,0 +1,205 @@ +import type { ConfigManager } from '../../config-manager/__.js' +import { type Tuple } from '../../prelude.js' +import type { Extension } from '../Extension/__.js' +import { Overload } from '../Overload/__.js' +import type { StepDef } from '../StepDef.js' +import type { StepRunner } from '../StepRunner.js' +import { Pipeline } from './_.js' +import type { PipelineDef } from './__.js' +import { type Options, resolveOptions } from './Config.js' + +/** + * Get the `input` parameter for a step that would be appended to the given Pipeline. + * + * Recall that non-first steps have input corresponding to the output of the previous step. + * + * So this returns: + * - If the pipeline has no steps then the pipeline input itself. + * - Otherwise the last step's output. + */ +// dprint-ignore +type GetNextStepParameterInput<$Context extends PipelineDef> = + $Context['steps'] extends Tuple.NonEmpty + ? Awaited['output']> + : $Context['input'] + +export interface Builder<$PipelineDef extends PipelineDef = PipelineDef> { + type: $PipelineDef + input: <$Input extends object>() => Builder< + PipelineDef.Updaters.SetInput<$PipelineDef, $Input> + > + /** + * TODO + */ + stepWithRunnerType: <$Runner extends StepRunner>() => < + const $Name extends string, + $Slots extends + | undefined + | StepDef.Slots = undefined, + >( + name: $Name, + parameters?: { + slots?: $Slots + run?: $Runner + }, + ) => Builder< + PipelineDef.Updaters.AddStep<$PipelineDef, { + name: $Name + input: Parameters<$Runner>[0] + output: ConfigManager.OrDefault2, {}> + slots: ConfigManager.OrDefault2<$Slots, {}> + run: $Runner + }> + > + /** + * TODO + */ + step: StepMethod<$PipelineDef> + /** + * TODO + */ + overload: <$OverloadBuilder extends Overload.Builder<$PipelineDef>>( + overloadBuilder: Overload.BuilderCallback<$PipelineDef, $OverloadBuilder>, + ) => Builder< + PipelineDef.Updaters.AddOverload<$PipelineDef, $OverloadBuilder['type']> + > + /** + * TODO + */ + // todo test this + use: <$Extension extends Extension.Builder>( + extension: $Extension, + ) => Builder< + PipelineDef.Updaters.AddOverloads<$PipelineDef, $Extension['type']['overloads']> + > + done: () => Pipeline.InferFromDefinition<$PipelineDef> +} + +interface StepMethod<$Context extends PipelineDef> { + < + const $Name extends string, + $Slots extends + | undefined + | StepDef.Slots = undefined, + $Input = GetNextStepParameterInput<$Context>, + $Output = unknown, + >( + name: $Name, + parameters?: { + slots?: $Slots + run?: (input: $Input, slots: $Slots, previous: GetNextStepParameterPrevious<$Context>) => $Output + }, + ): Builder< + PipelineDef.Updaters.AddStep<$Context, { + name: $Name + input: $Input + output: ConfigManager.OrDefault2<$Output, {}> + slots: ConfigManager.OrDefault2<$Slots, {}> + }> + > + < + const $Name extends string, + $Slots extends + | undefined + | StepDef.Slots = undefined, + $Input extends object = GetNextStepParameterInput<$Context>, + $Output = unknown, + >( + parameters: { + name: $Name + slots?: $Slots + run?: (input: $Input, slots: $Slots, previous: GetNextStepParameterPrevious<$Context>) => $Output + }, + ): Builder< + PipelineDef.Updaters.AddStep<$Context, { + name: $Name + input: $Input + output: ConfigManager.OrDefault2<$Output, {}> + slots: ConfigManager.OrDefault2<$Slots, {}> + }> + > +} + +// dprint-ignore +export type GetNextStepParameterPrevious<$Context extends PipelineDef> = + $Context['steps'] extends Tuple.NonEmpty + ? GetNextStepPrevious_<$Context['steps']> + : undefined + +type GetNextStepPrevious_<$Steps extends StepDef[]> = Tuple.IntersectItems< + { + [$Index in keyof $Steps]: { + [$StepName in $Steps[$Index]['name']]: { + input: Awaited<$Steps[$Index]['input']> + output: Awaited<$Steps[$Index]['output']> + } + } + } +> + +export type InferPipeline<$Builder extends Builder> = InferPipelineFromContext<$Builder['type']> + +// dprint-ignore +type InferPipelineFromContext<$Pipeline extends PipelineDef> = + $Pipeline + +/** + * TODO + */ +export const create = (options?: Options): Builder => { + const config = resolveOptions(options) + return recreate({ + steps: [], + config, + overloads: [], + } as any as PipelineDef) as any +} + +const recreate = <$Pipeline extends PipelineDef>(pipeline: $Pipeline): Builder<$Pipeline> => { + const builder: Builder<$Pipeline> = { + type: pipeline, + input: () => builder as any, + done: () => Pipeline.create(pipeline), + stepWithRunnerType: () => builder.step as any, + step: (...args: any[]) => { + const step = typeof args[0] === `string` + ? { + name: args[0], + run: passthroughStep, + ...(args[1] as undefined | object), + } + : { + run: passthroughStep, + ...args[0], + } + + return recreate({ + ...pipeline, + steps: [ + ...pipeline.steps, + step, + ], + } as any) + }, + use: (extension) => { + return recreate({ + ...pipeline, + overloads: [ + ...pipeline.overloads, + ...extension.type.overloads, + ], + } as any) + }, + overload: (builderCallback) => { + const overload = builderCallback({ create: Overload.create }) + // todo why mutating here? stop it. make like use extension. + pipeline.overloads.push(overload.type) + + return recreate(pipeline) as any + }, + } + + return builder +} + +const passthroughStep = (params: { input: object }) => params.input diff --git a/src/lib/anyware/PipelineSpecification/Spec.test-d.ts b/src/lib/anyware/PipelineSpecification/Spec.test-d.ts new file mode 100644 index 000000000..0b4e80a2e --- /dev/null +++ b/src/lib/anyware/PipelineSpecification/Spec.test-d.ts @@ -0,0 +1,48 @@ +// import { assertEqual } from '../../assert-equal.js' +// import type { MaybePromise } from '../../prelude.js' +// import type { PipelineSpecFromSteps } from '../PipelineSpecification/Spec.js' + +// assertEqual< +// PipelineSpecFromSteps<[]>, +// { steps: []; input: object; output: unknown } +// >() + +// assertEqual< +// PipelineSpecFromSteps<[{ name: 'a' }]>, +// { steps: [{ name: 'a'; slots: {}; input: object; output: unknown }]; input: object; output: unknown } +// >() + +// assertEqual< +// PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]>, +// { +// steps: [{ name: 'a'; slots: {}; input: object; output: MaybePromise<1> }] +// input: object +// output: 1 +// // ^^^^^^ pipeline output inferred from last step output +// } +// >() + +// assertEqual< +// PipelineSpecFromSteps<[{ name: 'a' }, { name: 'b'; input: { x: 1 } }]>, +// { +// steps: [ +// { name: 'a'; slots: {}; input: object; output: MaybePromise<{ x: 1 }> }, +// // step output inferred from next step input ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// { name: 'b'; slots: {}; input: { x: 1 }; output: unknown }, +// ] +// input: object +// output: unknown +// } +// >() + +// assertEqual< +// PipelineSpecFromSteps<[{ name: 'a'; output: Promise<1> }]>, +// // ^^^^^^^^^^^^^^^^^^ Step promised output flattened within MaybePromise +// { +// steps: [{ name: 'a'; slots: {}; input: object; output: MaybePromise<1> }] +// // ^^^^^^^^^^^^^^^ +// input: object +// output: 1 +// // ^^^^^^^^^ Pipeline output strips Promise type +// } +// >() diff --git a/src/lib/anyware/PipelineSpecification/Spec.ts b/src/lib/anyware/PipelineSpecification/Spec.ts new file mode 100644 index 000000000..e954bbe55 --- /dev/null +++ b/src/lib/anyware/PipelineSpecification/Spec.ts @@ -0,0 +1,54 @@ +// import type { IsUnknown } from 'type-fest' +// import type { ConfigManager } from '../../config-manager/__.js' +// import type { MaybePromise, Tuple } from '../../prelude.js' +// import type { StepDef } from '../StepDef.js' + +// // dprint-ignore +// export interface PipelineSpecification<$StepSpecs extends StepDef[] = StepDef[]> { +// steps: $StepSpecs +// input: $StepSpecs extends Tuple.NonEmpty +// ? $StepSpecs[0]['input'] +// : object +// output: Awaited< +// $StepSpecs extends Tuple.NonEmpty +// ? Tuple.GetLastValue<$StepSpecs>['output'] +// : unknown +// > +// } + +// // // dprint-ignore +// // type InferFromStepDefinitions< +// // $StepSpecInputs extends StepDef.SpecInput[] = StepDef.SpecInput[] +// // > = PipelineSpecification> + +// // type InferStepSpecs<$StepSpecInputs extends StepDef.SpecInput[]> = InferStepSpecs_ + +// // dprint-ignore +// type InferStepSpecs_<$StepSpecPrevious extends StepDef| undefined, $StepSpecInputs extends StepDef.SpecInput[]> = +// $StepSpecInputs extends [infer $StepSpecInput extends StepDef.SpecInput, ...infer $StepSpecInputsRest extends StepDef.SpecInput[]] +// ? InferStepSpecs__<{ +// name: $StepSpecInput['name'] +// slots: ConfigManager.OrDefault2<$StepSpecInput['slots'],{}> +// input: IsUnknown<$StepSpecInput['input']> extends true +// ? $StepSpecPrevious extends StepDef +// ? $StepSpecPrevious['output'] +// : object +// : $StepSpecInput['input'] +// output: MaybePromise< +// Awaited< +// IsUnknown<$StepSpecInput['output']> extends true +// ? $StepSpecInputsRest extends Tuple.NonEmpty +// ? $StepSpecInputsRest[0]['input'] extends undefined +// ? unknown +// : $StepSpecInputsRest[0]['input'] +// : unknown +// : $StepSpecInput['output'] +// > +// > +// }, $StepSpecInputsRest> +// : [] + +// type InferStepSpecs__<$StepSpec extends StepDef, $StepSpecInputsRest extends StepDef.SpecInput[]> = [ +// $StepSpec, +// ...InferStepSpecs_<$StepSpec, $StepSpecInputsRest>, +// ] diff --git a/src/lib/anyware/PipelineSpecification/createWithSpec.test-d.ts b/src/lib/anyware/PipelineSpecification/createWithSpec.test-d.ts new file mode 100644 index 000000000..7a24d2aa8 --- /dev/null +++ b/src/lib/anyware/PipelineSpecification/createWithSpec.test-d.ts @@ -0,0 +1,30 @@ +// import { assertEqual } from '../../assert-equal.js' +// import type { MaybePromise } from '../../prelude.js' +// import type { StepsIndex } from '../Pipeline/Pipeline.js' +// import type { Config } from '../PipelineDef/Config.js' +// import { createWithSpec } from './createWithSpec.js' +// import type { PipelineSpecFromSteps } from './Spec.js' + +// { +// type Spec = PipelineSpecFromSteps<[]> +// const p = createWithSpec({ steps: [] }) +// assertEqual() +// } + +// { +// type Spec = PipelineSpecFromSteps<[{ name: 'a'; output: 1 }]> +// const p = createWithSpec({ steps: [{ name: `a`, run: () => 1 }] }) +// type p = typeof p +// assertEqual< +// p, +// { +// config: Config +// input: object +// output: 1 +// steps: [ +// { run: (...arg: any[]) => any; name: `a`; input: object; output: MaybePromise<1> }, +// ] +// stepsIndex: StepsIndex +// } +// >() +// } diff --git a/src/lib/anyware/PipelineSpecification/createWithSpec.ts b/src/lib/anyware/PipelineSpecification/createWithSpec.ts new file mode 100644 index 000000000..b6dc5b914 --- /dev/null +++ b/src/lib/anyware/PipelineSpecification/createWithSpec.ts @@ -0,0 +1,98 @@ +// import type { ConfigManager } from '../../config-manager/__.js' +// import type { Objekt, Tuple } from '../../prelude.js' +// import { createStepsIndex, type StepsIndex } from '../Pipeline/Pipeline.js' +// import { type Config, type Options, resolveOptions } from '../PipelineDef/Config.js' +// import type { Step } from '../Step.js' +// import type { StepDef } from '../StepDef.js' +// import type { PipelineSpecification } from './Spec.js' + +// export const createWithSpec = <$PipelineSpec extends PipelineSpecification>( +// input: { +// options?: Options +// steps: InferStepsInput<$PipelineSpec['steps']> +// }, +// ): InferPipelineExecutable<$PipelineSpec> => { +// const config = resolveOptions(input.options) +// const stepsIndex = createStepsIndex(input.steps) +// return { +// config, +// stepsIndex, +// steps: input.steps, +// } as any +// } + +// type InferPipelineExecutable<$PipelineSpec extends PipelineSpecification> = { +// input: $PipelineSpec['input'] +// output: $PipelineSpec['output'] +// steps: InferExecutableSteps<$PipelineSpec['steps']> +// stepsIndex: StepsIndex +// config: Config +// } + +// type InferStepsInput<$NextStepDefinitions extends StepDef[]> = InferExecutableSteps_< +// [], +// $NextStepDefinitions, +// { types: false } +// > + +// type InferExecutableSteps<$NextStepDefinitions extends StepDef[]> = InferExecutableSteps_< +// [], +// $NextStepDefinitions, +// { types: true } +// > + +// // dprint-ignore +// type InferExecutableSteps_< +// $PreviousStepSpecs extends StepDef[], +// $NextStepSpecs extends StepDef[], +// $Options extends { types: boolean }, +// > = +// $NextStepSpecs extends [infer $StepSpec extends StepDef, ...infer $RestStepSpecs extends StepDef[]] +// ? [ +// & ( +// $Options['types'] extends true +// ? { +// input: $StepSpec['input'] +// output: $StepSpec['output'] +// } +// : {} +// ) +// & { +// name: $StepSpec['name'] +// run: (parameters: +// & { +// input: $StepSpec['input'] +// previous: GetParameterPrevious<$PreviousStepSpecs> +// } +// & ( +// $StepSpec['slots'] extends Step.Slots +// ? { +// slots: $StepSpec['slots'] +// } +// : {} +// ) +// ) => $StepSpec['output'] +// } +// & ( +// Objekt.IsEmpty<$StepSpec['slots']> extends true +// ? {} +// : { +// slots: $StepSpec['slots'] +// } +// ) +// , ...InferExecutableSteps_<[...$PreviousStepSpecs, $StepSpec], $RestStepSpecs, $Options>] +// : [] + +// type GetParameterPrevious<$StepDefinitions extends StepDef[]> = Tuple.IntersectItems< +// { +// [$Index in keyof $StepDefinitions]: { +// [$StepName in $StepDefinitions[$Index]['name']]: { +// input: $StepDefinitions[$Index]['input'] +// output: ConfigManager.OrDefault< +// $StepDefinitions[$Index]['output'], +// Tuple.GetAtNextIndex<$StepDefinitions, $Index>['input'] +// > +// } +// } +// } +// > diff --git a/src/lib/anyware/Pipeline/Result.ts b/src/lib/anyware/Result.ts similarity index 58% rename from src/lib/anyware/Pipeline/Result.ts rename to src/lib/anyware/Result.ts index 6f56f5f8e..aef907615 100644 --- a/src/lib/anyware/Pipeline/Result.ts +++ b/src/lib/anyware/Result.ts @@ -1,7 +1,4 @@ -import type { Errors } from '../../errors/__.js' -import type { PipelineSpec } from './Spec.js' - -export type InferResultFromSpec<$PipelineSpec extends PipelineSpec> = Result<$PipelineSpec['output']> +import type { Errors } from '../errors/__.js' export type ResultFailure = Errors.ContextualAggregateError diff --git a/src/lib/anyware/Step.ts b/src/lib/anyware/Step.ts index 24b8f2258..a8bbb33c2 100644 --- a/src/lib/anyware/Step.ts +++ b/src/lib/anyware/Step.ts @@ -1,64 +1,14 @@ -import type { SomeFunction } from '../prelude.js' +import type { StepDef } from './StepDef.js' +import type { StepRunner } from './StepRunner.js' -// todo: rename to Spec -export interface Step< - $Name extends string = string, -> { - name: $Name - slots: Step.Slots - input: any +export interface Step { + name: string + slots?: StepDef.Slots + input: Step.Input output: any + run: StepRunner } export namespace Step { - export interface SpecInput { - name: string - slots?: Step.Slots - input?: object - output?: unknown - } - - /** - * todo - */ - export const createWithInput = < - $Input extends Input = Input, - >() => - < - const $Name extends string, - $Run extends Runner<$Input>, - $Slots extends undefined | Step.Slots, - >( - parameters: { - name: $Name - slots?: $Slots - run: $Run - }, - ): { - name: $Name - run: $Run - input: $Input - output: ReturnType<$Run> - slots: undefined extends $Slots ? undefined : $Slots - } => { - return parameters as any - } - - export type Runner< - $Input extends Input = Input, - // $Slots extends undefined | Step.Slots = undefined, - $Slots extends undefined | object = undefined, - $Previous extends undefined | object = undefined, - $Output = any, - > = ( - input: $Input, - slots: $Slots, - previous: $Previous, - ) => $Output - - export type Input = object - - export type Slots = Record - - export type Name = string + export type Input = StepDef.Input } diff --git a/src/lib/anyware/StepDef.ts b/src/lib/anyware/StepDef.ts new file mode 100644 index 000000000..d75d51c67 --- /dev/null +++ b/src/lib/anyware/StepDef.ts @@ -0,0 +1,66 @@ +import type { SomeFunction } from '../prelude.js' +import type { StepRunner } from './StepRunner.js' + +export interface StepDef< + $Name extends string = string, +> { + name: $Name + slots: StepDef.Slots + input: any + output: any + /** + * Tracking the run signature type is useful for deriving the executable step. + * + * For example if Vitest mocks were used for the step run functions, their type + * would be carried through to the executable step. This is useful for testing. + * + * If we only relied on the spec types, which don't track the given run type itself, + * they Vitest mock type would not be carried through. + * + * The executable step is not designed for public use. Testing is an exception. + * + * This run signature is NOT used for deriving the specification step. + */ + run?: StepRunner +} + +export namespace StepDef { + export interface SpecInput { + name: string + slots?: StepDef.Slots + input?: object + output?: unknown + } + + /** + * todo + */ + export const createWithInput = < + $Input extends Input = Input, + >() => + < + const $Name extends string, + $Run extends StepRunner<$Input>, + $Slots extends undefined | StepDef.Slots, + >( + parameters: { + name: $Name + slots?: $Slots + run: $Run + }, + ): { + name: $Name + run: $Run + input: $Input + output: ReturnType<$Run> + slots: undefined extends $Slots ? undefined : $Slots + } => { + return parameters as any + } + + export type Input = object + + export type Slots = Record + + export type Name = string +} diff --git a/src/lib/anyware/StepRunner.ts b/src/lib/anyware/StepRunner.ts new file mode 100644 index 000000000..9db3e97f7 --- /dev/null +++ b/src/lib/anyware/StepRunner.ts @@ -0,0 +1,13 @@ +import type { StepDef } from './StepDef.js' + +export type StepRunner< + $Input extends StepDef.Input = StepDef.Input, + // $Slots extends undefined | Step.Slots = undefined, + $Slots extends undefined | object = undefined, + $Previous extends undefined | object = undefined, + $Output = any, +> = ( + input: $Input, + slots: $Slots, + previous: $Previous, +) => $Output diff --git a/src/lib/anyware/_.ts b/src/lib/anyware/_.ts index 4ff96ecbb..5a472c6ae 100644 --- a/src/lib/anyware/_.ts +++ b/src/lib/anyware/_.ts @@ -1,5 +1,6 @@ export * from './Extension/__.js' export * from './Interceptor/Interceptor.js' -export * from './Pipeline/__.js' -export * from './Pipeline/Spec.js' +export * from './Pipeline/Pipeline.js' +export * from './PipelineDef/__.js' +export * from './PipelineSpecification/Spec.js' export * from './run/runner.js' diff --git a/src/lib/anyware/__.entrypoint.test.ts b/src/lib/anyware/__.entrypoint.test.ts index 9502c29c7..4a7ec8d50 100644 --- a/src/lib/anyware/__.entrypoint.test.ts +++ b/src/lib/anyware/__.entrypoint.test.ts @@ -3,12 +3,12 @@ import { describe, expect, test } from 'vitest' import type { ContextualAggregateError } from '../errors/ContextualAggregateError.js' import { _ } from '../prelude.js' -import { Pipeline } from './_.js' +import { PipelineDef } from './_.js' import { initialInput, stepA, stepB } from './__.test-helpers.js' const run = async (interceptor: (...args: any[]) => any) => { - const pipeline = Pipeline.create().step(stepA).step(stepB).done() - return Pipeline.run(pipeline, { + const pipeline = PipelineDef.create().step(stepA).step(stepB).done() + return PipelineDef.run(pipeline, { initialInput, interceptors: [interceptor], }) diff --git a/src/lib/anyware/__.test-helpers.ts b/src/lib/anyware/__.test-helpers.ts index fc0af067b..a8f1aa416 100644 --- a/src/lib/anyware/__.test-helpers.ts +++ b/src/lib/anyware/__.test-helpers.ts @@ -1,8 +1,9 @@ import { beforeEach, vi } from 'vitest' -import { Pipeline } from './_.js' +import { PipelineDef } from './_.js' import type { Interceptor, NonRetryingInterceptorInput } from './Interceptor/Interceptor.js' -import type { Options } from './Pipeline/Config.js' -import { Step } from './Step.js' +import { Pipeline } from './Pipeline/Pipeline.js' +import type { Options } from './PipelineDef/Config.js' +import { StepDef } from './StepDef.js' export const initialInput = { x: 1 } as const export type initialInput = typeof initialInput @@ -16,9 +17,9 @@ export const results = { } as const export type results = typeof results -export const stepA = Step.createWithInput()({ name: `a`, run: () => results[`a`] }) -export const stepB = Step.createWithInput()({ name: `b`, run: () => results[`b`] }) -export const stepC = Step.createWithInput()({ name: `c`, run: () => results[`c`] }) +export const stepA = StepDef.createWithInput()({ name: `a`, run: () => results[`a`] }) +export const stepB = StepDef.createWithInput()({ name: `b`, run: () => results[`b`] }) +export const stepC = StepDef.createWithInput()({ name: `c`, run: () => results[`c`] }) export const slots = { m: () => Promise.resolve(`m` as const), @@ -26,7 +27,7 @@ export const slots = { } export type slots = typeof slots -export const createPipeline = (options?: Options) => { +export const createPipelineDef = (options?: Options) => { type Append = (hookName: string) => string type AppendExtra = () => string @@ -61,8 +62,9 @@ export const createPipeline = (options?: Options) => { type StepBRunner = typeof stepBRunner - return Pipeline - .create<{ value: string }>(options) + return PipelineDef + .create(options) + .input<{ value: string }>() .stepWithRunnerType()(`a`, { slots: { append: vi.fn().mockImplementation((hookName) => { @@ -85,41 +87,46 @@ export const createPipeline = (options?: Options) => { }, run: stepBRunner, }) - .done() } -type TestPipeline = ReturnType -export type TestInterceptor = Interceptor.InferConstructor +type TestPipelineDef = ReturnType['type'] +type TestPipeline = Pipeline.InferFromDefinition +export let pipelineDef: TestPipelineDef export let pipeline: TestPipeline +export type TestInterceptor = Interceptor.InferFromPipeline + beforeEach(() => { - pipeline = createPipeline() + pipelineDef = createPipelineDef().type + pipeline = Pipeline.create(pipelineDef) }) export const pipelineWithOptions = (options?: Options) => { - const pipeline = createPipeline(options) + const pipeline = createPipelineDef(options).type + const pipelineE = Pipeline.create(pipeline) const run = async (...interceptors: TestInterceptor[]) => { - return await Pipeline.run(pipeline, { + return await PipelineDef.run(pipelineE, { initialInput: { value: `initial` }, interceptors, }) } return { pipeline, + pipelineE, run, } } export const run = async (...interceptors: NonRetryingInterceptorInput[]) => { - return await Pipeline.run(pipeline, { + return await PipelineDef.run(pipeline, { initialInput: initialInput2, interceptors, }) } export const runRetrying = async (interceptor: NonRetryingInterceptorInput) => { - return await Pipeline.run(pipeline, { + return await PipelineDef.run(pipeline, { initialInput: initialInput2, interceptors: [], retryingInterceptor: interceptor, diff --git a/src/lib/anyware/run/getEntrypoint.ts b/src/lib/anyware/run/getEntrypoint.ts index 40adaff01..a0aad6394 100644 --- a/src/lib/anyware/run/getEntrypoint.ts +++ b/src/lib/anyware/run/getEntrypoint.ts @@ -1,8 +1,8 @@ import { analyzeFunction } from '../../analyze-function.js' import { ContextualError } from '../../errors/ContextualError.js' -import type { ExecutableStep } from '../ExecutableStep.js' import type { NonRetryingInterceptorInput } from '../Interceptor/Interceptor.js' -import type { ExecutablePipeline } from '../Pipeline/ExecutablePipeline.js' +import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { Step } from '../Step.js' export class ErrorAnywareInterceptorEntrypoint extends ContextualError< 'ErrorGraffleInterceptorEntryHook', @@ -26,9 +26,9 @@ export const InterceptorEntryHookIssue = { export type InterceptorEntryHookIssue = typeof InterceptorEntryHookIssue[keyof typeof InterceptorEntryHookIssue] export const getEntryStep = ( - pipeline: ExecutablePipeline, + pipeline: Pipeline, interceptor: NonRetryingInterceptorInput, -): ErrorAnywareInterceptorEntrypoint | ExecutableStep => { +): ErrorAnywareInterceptorEntrypoint | Step => { const stepsIndex = pipeline.stepsIndex const x = analyzeFunction(interceptor) if (x.parameters.length > 1) { diff --git a/src/lib/anyware/run/run.test-d.ts b/src/lib/anyware/run/run.test-d.ts new file mode 100644 index 000000000..a2a470f29 --- /dev/null +++ b/src/lib/anyware/run/run.test-d.ts @@ -0,0 +1,28 @@ +import { expectTypeOf, test } from 'vitest' +import { Pipeline } from '../_.js' +import type { initialInput } from '../__.test-helpers.js' +import { PipelineDef } from '../PipelineDef/__.js' +import type { Result } from '../Result.js' + +const def = PipelineDef.create().input() + +test(`returns input if no steps`, async () => { + const d = def.type + const p = Pipeline.create(d) + const r = await PipelineDef.run(p) + expectTypeOf(r).toEqualTypeOf>() +}) + +test(`returns last step output if steps`, async () => { + const d = def.step({ name: `a`, run: () => 2 as const }).type + const p = Pipeline.create(d) + const r = await PipelineDef.run(p) + expectTypeOf(r).toEqualTypeOf>() +}) + +test(`can return a promise`, async () => { + const d = def.step({ name: `a`, run: () => Promise.resolve(2 as const) }).type + const p = Pipeline.create(d) + const r = await PipelineDef.run(p) + expectTypeOf(r).toEqualTypeOf>() +}) diff --git a/src/lib/anyware/run/run.test.ts b/src/lib/anyware/run/run.test.ts index d4ef1d8e7..b8ee9aafc 100644 --- a/src/lib/anyware/run/run.test.ts +++ b/src/lib/anyware/run/run.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test, vi } from 'vitest' import { Errors } from '../../errors/__.js' import type { ContextualError } from '../../errors/ContextualError.js' -import { Pipeline } from '../_.js' +import { PipelineDef } from '../_.js' import { initialInput2, oops, @@ -13,8 +13,9 @@ import { runRetrying, type TestInterceptor, } from '../__.test-helpers.js' -import { successfulResult } from '../Pipeline/Result.js' -import { Step } from '../Step.js' +import { Pipeline } from '../Pipeline/Pipeline.js' +import { successfulResult } from '../Result.js' +import { StepDef } from '../StepDef.js' describe(`no interceptors`, () => { test(`passthrough to implementation`, async () => { @@ -105,13 +106,13 @@ describe(`one extension`, () => { describe(`two interceptors`, () => { test(`first can short-circuit`, async () => { // Do not require selection mode so that we can use a mock interceptor. - const { run, pipeline } = pipelineWithOptions({ entrypointSelectionMode: 'optional' }) + const { run, pipelineE } = pipelineWithOptions({ entrypointSelectionMode: 'optional' }) const i1: TestInterceptor = async ({ a }) => ({ value: '1' }) const i2: TestInterceptor = vi.fn().mockImplementation(({ a }) => ({ value: '2' })) expect(await run(i1, i2)).toEqual(successfulResult({ value: '1' })) expect(i2).not.toHaveBeenCalled() - expect(pipeline.stepsIndex.get('a')?.run).not.toHaveBeenCalled() - expect(pipeline.stepsIndex.get('b')?.run).not.toHaveBeenCalled() + expect(pipelineE.stepsIndex.get('a')?.run).not.toHaveBeenCalled() + expect(pipelineE.stepsIndex.get('b')?.run).not.toHaveBeenCalled() }) test(`each can adjust first hook then passthrough`, async () => { @@ -256,7 +257,7 @@ describe(`errors`, () => { describe('certain errors can be configured to be re-thrown without wrapping error', () => { class SpecialError1 extends Error {} class SpecialError2 extends Error {} - const stepA = Step.createWithInput<{ throws: Error }>()({ + const stepA = StepDef.createWithInput<{ throws: Error }>()({ name: 'a', run: (input) => { if (input.throws) throw input.throws @@ -264,36 +265,39 @@ describe(`errors`, () => { }) test('via passthroughErrorInstanceOf (one)', async () => { - const builder = Pipeline.create<{ throws: Error }>({ + const pipelineE = PipelineDef.create({ passthroughErrorInstanceOf: [SpecialError1], - }).step(stepA).done() + }).input<{ throws: Error }>().step(stepA).type + const exPipeline = Pipeline.create(pipelineE) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) }) test('via passthroughErrorInstanceOf (multiple)', async () => { - const builder = Pipeline.create<{ throws: Error }>({ + const pipelineE = PipelineDef.create({ passthroughErrorInstanceOf: [SpecialError1, SpecialError2], - }).step(stepA).done() + }).input<{ throws: Error }>().step(stepA).type + const exPipeline = Pipeline.create(pipelineE) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new SpecialError2('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError2) }) test('via passthroughWith', async () => { - const builder = Pipeline.create<{ throws: Error }>({ + const pipelineE = PipelineDef.create({ // todo type-safe hook name according to values passed to constructor // todo type-tests on signal { hookName, source, error } passthroughErrorWith: (signal) => { return signal.error instanceof SpecialError1 }, - }).step(stepA).done() + }).input<{ throws: Error }>().step(stepA).type + const exPipeline = Pipeline.create(pipelineE) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new Error('oops') }, interceptors: [] })).resolves.toBeInstanceOf(Errors.ContextualError) // dprint-ignore - await expect(Pipeline.run(builder, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) + await expect(PipelineDef.run(exPipeline, { initialInput: { throws: new SpecialError1('oops') }, interceptors: [] })).resolves.toBeInstanceOf(SpecialError1) }) }) }) @@ -416,8 +420,9 @@ describe('step runner parameter - previous', () => { describe('overloads', () => { test('overloaded step runners are run', async () => { - const p = Pipeline - .create<{ value: string }>() + const p = PipelineDef + .create() + .input<{ value: string }>() .step('a') .step('b') .overload(o => @@ -431,13 +436,15 @@ describe('overloads', () => { run: (input) => ({ value: input.value + '+b' }), }) ) - .done() - const result = await Pipeline.run(p, { initialInput: { value: 'initial', x: 1 } }) + .type + const exP = Pipeline.create(p) + const result = await PipelineDef.run(exP, { initialInput: { value: 'initial', x: 1 } }) expect(result).toEqual(successfulResult({ value: 'initial+a+b' })) }) test('two overloads can be used; runtime discriminant decides which one is used', async () => { - const p = Pipeline - .create<{ value: string }>() + const p = PipelineDef + .create() + .input<{ value: string }>() .step('a') .step('b') .overload(o => @@ -451,10 +458,11 @@ describe('overloads', () => { .step('a', { run: (input) => ({ ...input, value: input.value + '+a2' }) }) .step('b', { run: (input) => ({ value: input.value + '+b2' }) }) ) - .done() - const result1 = await Pipeline.run(p, { initialInput: { value: 'initial', x: 1 } }) + .type + const exP = Pipeline.create(p) + const result1 = await PipelineDef.run(exP, { initialInput: { value: 'initial', x: 1 } }) expect(result1).toEqual(successfulResult({ value: 'initial+a+b' })) - const result2 = await Pipeline.run(p, { initialInput: { value: 'initial', x: 2 } }) + const result2 = await PipelineDef.run(exP, { initialInput: { value: 'initial', x: 2 } }) expect(result2).toEqual(successfulResult({ value: 'initial+a2+b2' })) }) }) diff --git a/src/lib/anyware/run/run.ts b/src/lib/anyware/run/run.ts index 41129e061..1c890d64e 100644 --- a/src/lib/anyware/run/run.ts +++ b/src/lib/anyware/run/run.ts @@ -1,14 +1,15 @@ import { createRunner } from '../_.js' -import type { Pipeline } from '../Pipeline/__.js' +import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { Result } from '../Result.js' import type { Params } from './runner.js' type Run = < - $Pipeline extends Pipeline.ExecutablePipeline, - $Params extends Params<$Pipeline['spec']>, + $Pipeline extends Pipeline, + $Params extends Params<$Pipeline>, >( pipeline: $Pipeline, params?: $Params, -) => Promise<$Pipeline['output']> +) => Promise> /** * todo diff --git a/src/lib/anyware/run/runPipeline.ts b/src/lib/anyware/run/runPipeline.ts index 563c02564..81275a8ba 100644 --- a/src/lib/anyware/run/runPipeline.ts +++ b/src/lib/anyware/run/runPipeline.ts @@ -1,9 +1,9 @@ import type { Errors } from '../../errors/__.js' import { ContextualError } from '../../errors/ContextualError.js' import { casesExhausted, createDeferred, debug } from '../../prelude.js' -import type { ExecutableStep } from '../ExecutableStep.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { ExecutablePipeline } from '../Pipeline/ExecutablePipeline.js' +import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { Step } from '../Step.js' import type { StepResult, StepResultErrorAsync } from '../StepResult.js' import { createResultEnvelope } from './resultEnvelope.js' import type { ResultEnvelop } from './resultEnvelope.js' @@ -20,8 +20,8 @@ export const runPipeline = async ( asyncErrorDeferred, previousStepsCompleted, }: { - pipeline: ExecutablePipeline - stepsToProcess: readonly ExecutableStep[] + pipeline: Pipeline + stepsToProcess: readonly Step[] originalInputOrResult: unknown interceptorsStack: readonly InterceptorGeneric[] asyncErrorDeferred: StepResultErrorAsync diff --git a/src/lib/anyware/run/runStep.ts b/src/lib/anyware/run/runStep.ts index 52220b2ed..d1f638a67 100644 --- a/src/lib/anyware/run/runStep.ts +++ b/src/lib/anyware/run/runStep.ts @@ -1,8 +1,8 @@ import { Errors } from '../../errors/__.js' import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../../prelude.js' import type { InterceptorGeneric } from '../Interceptor/Interceptor.js' -import type { ExecutablePipeline } from '../Pipeline/ExecutablePipeline.js' -import type { Step } from '../Step.js' +import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { StepDef } from '../StepDef.js' import type { StepResult, StepResultErrorAsync } from '../StepResult.js' import { StepTrigger } from '../StepTrigger.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' @@ -27,7 +27,7 @@ export const runStep = async ( asyncErrorDeferred, customSlots, }: { - pipeline: ExecutablePipeline + pipeline: Pipeline name: string done: HookDoneResolver inputOriginalOrFromExtension: object @@ -35,7 +35,7 @@ export const runStep = async ( * Information about previous hook executions, like what their input was. */ previousStepsCompleted: object - customSlots: Step.Slots + customSlots: StepDef.Slots /** * The extensions that are at this hook awaiting. */ diff --git a/src/lib/anyware/run/runner.ts b/src/lib/anyware/run/runner.ts index cfb500ffe..297210680 100644 --- a/src/lib/anyware/run/runner.ts +++ b/src/lib/anyware/run/runner.ts @@ -2,29 +2,31 @@ import { partitionAndAggregateErrors } from '../../errors/_.js' import { Errors } from '../../errors/__.js' import { createDeferred } from '../../prelude.js' import { casesExhausted } from '../../prelude.js' -import type { PipelineSpec } from '../_.js' import { createRetryingInterceptor, type InterceptorInput, type NonRetryingInterceptorInput, } from '../Interceptor/Interceptor.js' -import type { Pipeline } from '../Pipeline/__.js' -import { successfulResult } from '../Pipeline/Result.js' -import type { Step } from '../Step.js' +import type { Pipeline } from '../Pipeline/Pipeline.js' +import type { PipelineDef } from '../PipelineDef/__.js' +import { successfulResult } from '../Result.js' +import type { StepDef } from '../StepDef.js' import type { StepResultErrorExtension } from '../StepResult.js' import type { StepTriggerEnvelope } from '../StepTriggerEnvelope.js' import { getEntryStep } from './getEntrypoint.js' import { runPipeline } from './runPipeline.js' -export interface Params<$Pipeline extends PipelineSpec = PipelineSpec> { +export interface Params<$Pipeline extends Pipeline = Pipeline> { initialInput: $Pipeline['input'] interceptors?: NonRetryingInterceptorInput[] retryingInterceptor?: NonRetryingInterceptorInput } export const createRunner = - <$Pipeline extends Pipeline.ExecutablePipeline>(pipeline: $Pipeline) => - async (params?: Params<$Pipeline['spec']>): Promise<$Pipeline['output']> => { + <$Pipeline extends Pipeline>(pipeline: $Pipeline) => + async (params?: Params<$Pipeline>): Promise<$Pipeline['output']> => { + // const pipelineExecutable = pipeline as any as ExecutablePipeline.InferFromPipeline<$ExPipeline> + const { initialInput, interceptors = [], retryingInterceptor } = params ?? {} const interceptors_ = retryingInterceptor @@ -46,10 +48,10 @@ export const createRunner = }) if (result instanceof Error) return result as any - return successfulResult(result.result) + return successfulResult(result.result) as any } -const toInternalInterceptor = (pipeline: Pipeline.ExecutablePipeline, interceptor: InterceptorInput) => { +const toInternalInterceptor = (pipeline: PipelineDef.Pipeline, interceptor: InterceptorInput) => { const currentChunk = createDeferred() const body = createDeferred() const interceptorTrigger = typeof interceptor === `function` ? interceptor : interceptor.run @@ -92,7 +94,7 @@ const toInternalInterceptor = (pipeline: Pipeline.ExecutablePipeline, intercepto } } - const stepsBeforeEntrypoint: Step.Name[] = [] + const stepsBeforeEntrypoint: StepDef.Name[] = [] for (const step of pipeline.steps) { if (step === entryStep) break stepsBeforeEntrypoint.push(step.name) diff --git a/src/lib/config-manager/ConfigManager.test-d.ts b/src/lib/config-manager/ConfigManager.test-d.ts index b975f6f31..ad369a9af 100644 --- a/src/lib/config-manager/ConfigManager.test-d.ts +++ b/src/lib/config-manager/ConfigManager.test-d.ts @@ -24,17 +24,17 @@ assertEqual< // dprint-ignore { -assertEqual , { a: { b: 2 }; a2: 2 }>() -assertEqual , { a: { b: 3 } }>() -assertEqual , { a: { b: 3 } }>() +assertEqual , { a: { b: 2 }; a2: 2 }>() +assertEqual , { a: { b: 3 } }>() +assertEqual , { a: { b: 3 } }>() // never -assertEqual , never>() -assertEqual , never>() -assertEqual , { a: { b: never } }>() +assertEqual , never>() +assertEqual , never>() +assertEqual , { a: { b: never } }>() -assertEqual , { a: { b: 2 }; b: string }>() -assertEqual , { a: 1 }>() -assertEqual , { a: 1; b?: number }>() +assertEqual , { a: { b: 2 }; b: string }>() +assertEqual , { a: 1 }>() +assertEqual , { a: 1; b?: number }>() assertEqual , a1>() assertEqual , { a: { b: 2 }; b: string }>() @@ -44,8 +44,12 @@ assertEqual , { a: { b: { c: 9 assertEqual , { a: { b: number; b2: { c: 9 } }; b: string }>() assertEqual , { a: { b: number }; b: string; c: 9 }>() -assertEqual , { x: [1] }>() -assertEqual , { x: [1, 2] }>() +assertEqual , { x: [1] }>() +assertEqual , { x: [1, 2] }>() + +assertEqual , { x: {} }>() +assertEqual , { x: {a:1} }>() +assertEqual , { x: {a:1; b:2} }>() } assertEqual< diff --git a/src/lib/config-manager/ConfigManager.ts b/src/lib/config-manager/ConfigManager.ts index 3640077bd..33c05abae 100644 --- a/src/lib/config-manager/ConfigManager.ts +++ b/src/lib/config-manager/ConfigManager.ts @@ -51,7 +51,11 @@ export const createMerger = <$CustomScalars extends CustomScalarGuard[]>( } // dprint-ignore -export type SetAtPath<$Object extends object, $Path extends string[], $Value> = +export type SetAtPath< + $Object extends object, + $Path extends string[], + $Value, +> = $Path extends [] ? $Object : $Path extends [infer $Key extends string, ...infer $PathRest extends string[]] @@ -150,43 +154,74 @@ export type SetProperties<$Object1 extends object, $Object2 extends object> = export type SetMany<$Obj extends object, $Sets extends [Path, any][]> = $Sets extends [] ? $Obj : $Sets extends [infer $Set extends [Path, any], ...infer $SetRest extends [Path, any][]] ? SetMany< - SetAtKeyPath<$Obj, $Set[0], $Set[1]>, + SetKeyAtPath<$Obj, $Set[0], $Set[1]>, $SetRest > : never -export type AppendAtKey<$Obj extends object, $Prop extends keyof $Obj, $Type> = +// dprint-ignore +export type UpdateKeyWithAppend< + $Obj extends object, + $Prop extends keyof $Obj, + $Type, +> = + SetKey< + $Obj, + $Prop, // @ts-expect-error - UpdateAtKey<$Obj, $Prop, [...$Obj[$Prop], $Type]> + [...$Obj[$Prop], $Type] + > -export type AppendManyAtKey<$Obj extends object, $Prop extends keyof $Obj, $Type extends any[]> = - // @ts-expect-error - UpdateAtKey<$Obj, $Prop, [...$Obj[$Prop], ...$Type]> +// dprint-ignore +export type UpdateKeyWithAppendMany< + $Obj extends object, + $Prop extends keyof $Obj, + $Type extends any[], +> = + SetKey< + $Obj, + $Prop, + // @ts-expect-error + [...$Obj[$Prop], ...$Type] + > + +export type UpdateKeyWithIntersection< + $Obj extends object, + $PropertyName extends keyof $Obj, + $Type extends object, +> = + & $Obj + & { + [_ in $PropertyName]: $Type + } -export type UpdateAtKey<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> = +export type SetKey< + $Obj extends object, + $PropertyName extends keyof $Obj, + $Type extends $Obj[$PropertyName], +> = & { - [_ in keyof $Obj as _ extends $Prop ? never : _]: $Obj[_] + [_ in keyof $Obj as _ extends $PropertyName ? never : _]: $Obj[_] } & { - [_ in $Prop]: $Type + [_ in $PropertyName]: $Type } // dprint-ignore -export type SetAtKeyPath<$Obj extends object, $Path extends Path, $Value> = +export type SetKeyAtPath<$Obj extends object, $Path extends Path, $Value> = Simplify< $Path extends [] ? $Value extends object ? $Obj & $Value : never - : Set_<$Obj, $Path, $Value> + : SetKeyAtPath_<$Obj, $Path, $Value> > - // dprint-ignore -export type Set_<$ObjOrValue, $Path extends Path, $Value> = +type SetKeyAtPath_<$ObjOrValue, $Path extends Path, $Value> = Simplify< $Path extends [infer $P1 extends string, ...infer $PN extends string[]] ? $P1 extends keyof $ObjOrValue - ? Omit<$ObjOrValue, $P1> & { [_ in $P1]: Set_<$ObjOrValue[$P1], $PN, $Value> } + ? Omit<$ObjOrValue, $P1> & { [_ in $P1]: SetKeyAtPath_<$ObjOrValue[$P1], $PN, $Value> } // If we use a nice error display here (like the following comment) it will mess with the result type in variable cases. // `Error: Cannot set value at path in object. Path property "${$P1}" does not exist in object.` : never diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 16590d8d0..043be5328 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -305,6 +305,10 @@ export namespace Objekt { } export namespace Tuple { + export type IndexKey = number | `${number}` + + export type IsEmpty = T extends [] ? true : false + // dprint-ignore export type PreviousItem<$Items extends readonly any[], $OfItem> = $Items extends [infer $Next, ...infer $Rest] @@ -767,3 +771,5 @@ export type PropertyKeyToString<$Key extends PropertyKey> = $Key extends string : $Key extends number ? $Key : $Key extends symbol ? '' : never + +export type DiscriminantPropertyValue = string | number | symbol diff --git a/src/requestPipeline/RequestPipeline.ts b/src/requestPipeline/RequestPipeline.ts index e21f37bd9..d516b0cd3 100644 --- a/src/requestPipeline/RequestPipeline.ts +++ b/src/requestPipeline/RequestPipeline.ts @@ -9,11 +9,8 @@ import { encodeRequestVariables } from './CustomScalars/encode.js' import { httpTransport } from './extensions/httpTransport.js' import { memoryTransport } from './extensions/memoryTransport.js' -const requestPipelineBase = Anyware.Pipeline - .create<{ - request: Grafaid.RequestAnalyzedInput - state: Context - }>({ +const requestPipelineDefBuilderBase = Anyware.PipelineDef + .create({ // If core errors caused by an abort error then raise it as a direct error. // This is an expected possible error. Possible when user cancels a request. passthroughErrorWith: (signal) => { @@ -23,6 +20,10 @@ const requestPipelineBase = Anyware.Pipeline return signal.hookName === `exchange` && isAbortError(signal.error) }, }) + .input<{ + request: Grafaid.RequestAnalyzedInput + state: Context + }>() .step(`encode`, { run: (input) => { const sddm = input.state.schemaMap @@ -76,17 +77,19 @@ const requestPipelineBase = Anyware.Pipeline }, }) -export type RequestPipelineBaseContext = typeof requestPipelineBase['context'] +export type RequestPipelineSpecBase = typeof requestPipelineDefBuilderBase.type -export const requestPipeline = requestPipelineBase +const requestPipelineSpecBuilderFull = requestPipelineDefBuilderBase .use(httpTransport) .use(memoryTransport) - .done() +export type RequestPipelineSpec = typeof requestPipelineSpecBuilderFull.type + +export const requestPipeline = Anyware.Pipeline.create(requestPipelineSpecBuilderFull.type) export type RequestPipeline = typeof requestPipeline export namespace requestPipeline { - export type ResultFailure = Anyware.Pipeline.ResultFailure + export type ResultFailure = Anyware.PipelineDef.ResultFailure // | Errors.ContextualError // Possible from http transport fetch with abort controller. // | DOMException diff --git a/src/requestPipeline/extensions/httpTransport.ts b/src/requestPipeline/extensions/httpTransport.ts index 428cdded7..f70a2171f 100644 --- a/src/requestPipeline/extensions/httpTransport.ts +++ b/src/requestPipeline/extensions/httpTransport.ts @@ -9,10 +9,10 @@ import { mergeRequestInit, searchParamsAppendAll } from '../../lib/http.js' import type { httpMethodGet, httpMethodPost } from '../../lib/http.js' import { _, isString, type MaybePromise } from '../../lib/prelude.js' import { Transport } from '../../types/Transport.js' -import type { RequestPipelineBaseContext } from '../RequestPipeline.js' +import type { RequestPipelineSpecBase } from '../RequestPipeline.js' -export const httpTransport = Anyware.Extension - .create() +export const httpTransport = Anyware.Extension.Builder + .create() .overload(overload => overload .create({ diff --git a/src/requestPipeline/extensions/memoryTransport.ts b/src/requestPipeline/extensions/memoryTransport.ts index acbc2633e..842b3508c 100644 --- a/src/requestPipeline/extensions/memoryTransport.ts +++ b/src/requestPipeline/extensions/memoryTransport.ts @@ -2,10 +2,10 @@ import { Anyware } from '../../lib/anyware/__.js' import type { Grafaid } from '../../lib/grafaid/__.js' import { print } from '../../lib/grafaid/document.js' import { execute } from '../../lib/grafaid/execute.js' // todo -import type { RequestPipelineBaseContext } from '../RequestPipeline.js' +import type { RequestPipelineSpecBase } from '../RequestPipeline.js' -export const memoryTransport = Anyware.Extension - .create() +export const memoryTransport = Anyware.Extension.Builder + .create() .overload(overload => overload .create({ diff --git a/tests/_/SpyExtension.ts b/tests/_/SpyExtension.ts index 0a9dba865..a1ad75375 100644 --- a/tests/_/SpyExtension.ts +++ b/tests/_/SpyExtension.ts @@ -1,16 +1,16 @@ import { beforeEach } from 'vitest' import { createExtension } from '../../src/entrypoints/main.js' -import type { RequestPipeline } from '../../src/requestPipeline/__.js' +import type { RequestPipelineSpec } from '../../src/requestPipeline/__.js' interface SpyData { encode: { - input: RequestPipeline['spec']['steps']['0']['input'] | null + input: RequestPipelineSpec['steps']['0']['input'] | null } pack: { - input: RequestPipeline['spec']['steps']['1']['input'] | null + input: RequestPipelineSpec['steps']['1']['input'] | null } exchange: { - input: RequestPipeline['spec']['steps']['2']['input'] | null + input: RequestPipelineSpec['steps']['2']['input'] | null } }