Skip to content

Instantly share code, notes, and snippets.

@dtinth
Created November 4, 2022 11:06
Show Gist options
  • Save dtinth/d4e3a9ae55f8ec2943b4cca6fc80d586 to your computer and use it in GitHub Desktop.
Save dtinth/d4e3a9ae55f8ec2943b4cca6fc80d586 to your computer and use it in GitHub Desktop.
stagesetter.ts
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