-
Notifications
You must be signed in to change notification settings - Fork 0
/
labs.ts
157 lines (137 loc) · 3.85 KB
/
labs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* Variable is a named value in a lab.
*/
export type Variable<TName extends string, TValue> = Record<TName, TValue>;
/**
* Procedure is a callable variable in a lab.
*/
export interface Procedure<TRequest, TResponse> {
(props: TRequest): TResponse;
}
/**
* Lab is a collection of resources that can be used together to perform tasks.
*/
export class Lab<
T extends Record<PropertyKey, unknown> = Record<PropertyKey, unknown>,
> {
#variables = new Map<string, unknown>();
#procedures = new Set<string>();
public [Symbol.iterator](): IterableIterator<[string, unknown]> {
return this.#variables.entries();
}
/**
* variable sets a variable in the lab.
*/
public variable<TName extends string, TValue>(
name: TName,
value: TValue,
): Lab<T & Variable<TName, TValue>> {
this.#variables.set(name, value);
return this as Lab<T & Variable<TName, TValue>>;
}
/**
* get returns a variable from the lab.
*/
public get<TName extends keyof T>(
name: TName,
): T[TName] {
const value = this.#variables.get(name as string) as
| T[TName]
| undefined;
if (value === undefined) {
throw new Error(`No such resource: ${String(name)}`);
}
return value;
}
/**
* extends extends the lab with the variables and procedures from another lab.
*/
public extend<U extends Record<PropertyKey, unknown>>(
lab: Lab<U>,
): Lab<T & U> {
for (const [name, value] of lab) {
this.#variables.set(name, value);
}
for (const name of lab.#procedures) {
this.#procedures.add(name);
}
return this as Lab<T & U>;
}
/**
* satisfies fails type-check if the lab does implement the required variables.
*/
public satisfies<
U extends T extends U ? Record<PropertyKey, unknown> : never,
>(_: Lab<U>): Lab<T> {
return this;
}
/**
* deepCopy creates a new lab with the same variables.
*/
public deepCopy(): Lab<T> {
const lab = new Lab();
for (const [name, value] of this) {
if (this.#procedures.has(name)) {
lab.#procedures.add(name);
}
lab.variable(name, value);
}
return lab as Lab<T>;
}
/**
* procedure sets an executable variable in the lab.
*/
public procedure<
TName extends string,
TDependency extends string,
TRequest,
TResponse,
>(
name: TName,
execute: (
props: TRequest,
dependencies: { [K in TDependency]: T[K] },
) => TResponse,
dependencyNames?: TDependency[],
// TODO: Consider replacing arguments with a single object argument.
): Lab<T & Variable<TName, Procedure<TRequest, TResponse>>> {
const procedure = (props: TRequest) => {
const dependencies = (dependencyNames ?? [])
.reduce(
(acc, key) => {
acc[key] = this.get(key);
return acc;
},
{} as { [K in TDependency]: T[K] },
);
return execute(props, dependencies);
};
this.#procedures.add(name);
return this.variable(name, procedure);
}
/**
* execute executes a callable variable in the lab.
*/
public execute<
TName extends keyof T,
TValue extends T[TName],
TRequest extends // deno-lint-ignore no-explicit-any
(TValue extends (...args: any) => any ? Parameters<TValue>[0]
: never),
TResponse extends // deno-lint-ignore no-explicit-any
(TValue extends (...args: any) => any ? ReturnType<TValue>
: never),
>(name: TName, request: TRequest): TResponse {
const procedure = this.get(name);
if (!procedure) {
throw new Error(`No such resource: ${String(name)}`);
}
if (!this.#procedures.has(name as string)) {
throw new Error(`Resource is not a procedure: ${String(name)}`);
}
if (typeof procedure !== "function") {
throw new Error(`Unexpected resource type: ${String(name)}`);
}
return procedure(request);
}
}