Created
November 4, 2022 11:06
-
-
Save dtinth/d4e3a9ae55f8ec2943b4cca6fc80d586 to your computer and use it in GitHub Desktop.
stagesetter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') | |
export type Input<T> = T | Output<T> | |
export interface Output<T> { | |
resolveWithContext(ctx: Context): Promise<T> | |
} | |
class OutputImpl<T> implements Output<T> { | |
private promise?: Promise<T> | |
constructor( | |
private readonly resolver: (ctx: Context) => Promise<T>, | |
private readonly getDescription: () => string, | |
) {} | |
resolveWithContext(ctx: Context): Promise<T> { | |
this.promise ??= this.resolver(ctx) | |
return this.promise | |
} | |
toString() { | |
return `[Output ${this.getDescription()}]` | |
} | |
toJSON() { | |
return { $output: this.toString() } | |
} | |
[customInspectSymbol](depth: any, options: any, inspect: (a: any) => string) { | |
return this.toString() | |
} | |
} | |
export function defineResourceType<N extends string>(name: N) { | |
return { | |
onCreate: <I, R>(handler: ResourceCreateHandler<I, R>) => { | |
return { | |
build: <X extends OutputExtractors<R>>(options: { | |
description?: string | ((input: I) => string) | |
outputs: X | |
}): ResourceType<N, I, R, X> => { | |
const impl = new ResourceTypeImpl<N, I, R, X>( | |
name, | |
handler, | |
options.outputs, | |
options.description || '', | |
) | |
return Object.assign((input: I) => impl.create(input), { | |
type: impl.name, | |
}) | |
}, | |
} | |
}, | |
} | |
} | |
export interface ResourceType< | |
N extends string, | |
I, | |
R, | |
X extends OutputExtractors<R>, | |
> { | |
type: N | |
(input: I): Resource<N, R> & Outputs<X> | |
} | |
class ResourceTypeImpl<N extends string, I, R, X extends OutputExtractors<R>> { | |
constructor( | |
public name: N, | |
private handler: ResourceCreateHandler<I, R>, | |
private extractors: X, | |
private descriptionGenerator: string | ((input: I) => string), | |
) {} | |
create(input: I) { | |
const createResult: OutputImpl<R> = new OutputImpl( | |
(ctx) => this.handler(ctx, input), | |
() => resource.toString(), | |
) | |
const resource: ResourceImpl<N, I, R> = new ResourceImpl( | |
this.name, | |
() => | |
typeof this.descriptionGenerator === 'string' | |
? this.descriptionGenerator | |
: this.descriptionGenerator(input), | |
input, | |
createResult, | |
) | |
const outputs = {} as any | |
for (const [key, extractor] of Object.entries(this.extractors)) { | |
outputs[key] = new OutputImpl<any>( | |
(ctx) => ctx.read(createResult).then((x) => extractor(x)), | |
() => `(${createResult.toString()}).${key}`, | |
) | |
} | |
return Object.assign(resource, outputs as Outputs<X>) | |
} | |
} | |
export interface Resource<N extends string, R> { | |
type: N | |
createResult: OutputImpl<R> | |
} | |
export type ResourceTypeOf<T extends ResourceType<any, any, any, any>> = | |
T extends ResourceType<infer N, any, infer R, infer X> | |
? Resource<N, R> & Outputs<X> | |
: never | |
class ResourceImpl<N extends string, I, R> implements Resource<N, R> { | |
constructor( | |
public type: N, | |
private _getDescription: () => string, | |
private _input: I, | |
public createResult: OutputImpl<R>, | |
) {} | |
toString() { | |
const description = this._getDescription() | |
if (!description) return this.type | |
return '[Resource ' + this.type + ': ' + description + ']' | |
} | |
toJSON() { | |
return { $resource: this.toString() } | |
} | |
[customInspectSymbol](depth: any, options: any, inspect: (a: any) => string) { | |
return `[Resource ${this.type}: ${inspect(this._input)}]` | |
} | |
} | |
type ResourceCreateHandler<I, R> = (ctx: Context, input: I) => Promise<R> | |
type OutputExtractors<R> = Record<string, (value: R) => any> | |
type Outputs<X extends OutputExtractors<any>> = { | |
[K in keyof X]: Output<Awaited<ReturnType<X[K]>>> | |
} | |
interface Context { | |
read<T>(input: Input<T>): Promise<T> | |
} | |
export class StageSetter implements Context { | |
async read<T>(input: Input<T>): Promise<T> { | |
if ( | |
typeof input !== 'object' || | |
!input || | |
!('resolveWithContext' in input) | |
) { | |
return Promise.resolve(input) | |
} | |
return input.resolveWithContext(this) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment