@akshatmittal/invoker 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -13
- package/dist/github.d.mts +22 -0
- package/dist/github.mjs +353 -0
- package/dist/index.d.mts +6 -12
- package/dist/index.mjs +79 -62
- package/dist/slack.d.mts +10 -0
- package/dist/slack.mjs +482 -0
- package/package.json +22 -1
package/dist/index.mjs
CHANGED
|
@@ -1,81 +1,100 @@
|
|
|
1
1
|
import { beforeAll, describe, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
//#region src/task.ts
|
|
3
4
|
const taskDefinitionBrand = Symbol("invoker.task");
|
|
4
5
|
function defineTask(definition) {
|
|
5
6
|
return {
|
|
6
7
|
...definition,
|
|
7
|
-
matrix: definition.matrix ?? {},
|
|
8
|
+
matrix: definition.matrix ?? (async () => ({})),
|
|
8
9
|
[taskDefinitionBrand]: true
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
//#endregion
|
|
12
13
|
//#region src/json.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const scalarSchema = z.union([
|
|
15
|
+
z.null(),
|
|
16
|
+
z.string(),
|
|
17
|
+
z.boolean(),
|
|
18
|
+
z.number()
|
|
19
|
+
]);
|
|
20
|
+
const nameSchema = z.string().trim().min(1);
|
|
21
|
+
function snapshotJson(value, owner, path, ancestors = /* @__PURE__ */ new Set()) {
|
|
22
|
+
return cloneJson(value, owner, path, ancestors);
|
|
23
|
+
}
|
|
24
|
+
function cloneJson(value, owner, path, ancestors) {
|
|
25
|
+
if (scalarSchema.safeParse(value).success) return value;
|
|
26
|
+
if (!isJsonContainer(value)) fail(owner, path, "expected JSON");
|
|
20
27
|
if (ancestors.has(value)) fail(owner, path, "cyclic values are not JSON");
|
|
21
28
|
ancestors.add(value);
|
|
22
|
-
if (Array.isArray(value))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
const snapshot = [];
|
|
31
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
32
|
+
if (!(index in value)) fail(owner, `${path}[${index}]`, "sparse arrays are not JSON");
|
|
33
|
+
snapshot.push(cloneJson(value[index], owner, `${path}[${index}]`, ancestors));
|
|
34
|
+
}
|
|
35
|
+
ancestors.delete(value);
|
|
36
|
+
return snapshot;
|
|
37
|
+
} else {
|
|
27
38
|
assertPlainObject(value, owner, path);
|
|
28
39
|
if (Object.getOwnPropertySymbols(value).length > 0) fail(owner, path, "JSON objects cannot have symbol keys");
|
|
29
|
-
|
|
40
|
+
const snapshot = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneJson(child, owner, `${path}.${key}`, ancestors)]));
|
|
41
|
+
ancestors.delete(value);
|
|
42
|
+
return snapshot;
|
|
30
43
|
}
|
|
31
|
-
ancestors.delete(value);
|
|
32
44
|
}
|
|
33
45
|
function assertPlainObject(value, owner, path) {
|
|
34
|
-
if (value
|
|
46
|
+
if (Object(value) !== value || Array.isArray(value)) fail(owner, path, "expected a plain object");
|
|
35
47
|
const prototype = Object.getPrototypeOf(value);
|
|
36
48
|
if (prototype !== Object.prototype && prototype !== null) fail(owner, path, "expected a plain object");
|
|
37
49
|
}
|
|
38
50
|
function assertName(value, owner, path) {
|
|
39
|
-
if (
|
|
51
|
+
if (!nameSchema.safeParse(value).success) fail(owner, path, "expected a non-empty string");
|
|
40
52
|
}
|
|
41
53
|
function assertOnlyKeys(value, allowed, owner) {
|
|
42
54
|
for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(owner, `.${key}`, "unknown property");
|
|
43
55
|
}
|
|
44
56
|
function canonicalJson(value) {
|
|
45
57
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
46
|
-
if (value
|
|
47
|
-
const object = value;
|
|
48
|
-
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
49
|
-
}
|
|
58
|
+
if (isJsonObject(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
50
59
|
return JSON.stringify(value);
|
|
51
60
|
}
|
|
52
61
|
function fail(owner, path, message) {
|
|
53
62
|
throw new TypeError(`${owner}${path}: ${message}`);
|
|
54
63
|
}
|
|
64
|
+
function isJsonContainer(value) {
|
|
65
|
+
return value !== null && Object(value) === value;
|
|
66
|
+
}
|
|
67
|
+
function isJsonObject(value) {
|
|
68
|
+
return isJsonContainer(value) && !Array.isArray(value);
|
|
69
|
+
}
|
|
55
70
|
//#endregion
|
|
56
71
|
//#region src/matrix.ts
|
|
72
|
+
const axisSchema = z.string();
|
|
57
73
|
function expandMatrix(matrix, owner) {
|
|
58
|
-
if (matrix === void 0) return [{}];
|
|
59
74
|
assertPlainObject(matrix, owner, ".matrix");
|
|
60
75
|
let cases = [{}];
|
|
61
|
-
for (const
|
|
76
|
+
for (const candidate of Reflect.ownKeys(matrix)) {
|
|
77
|
+
const parsed = axisSchema.safeParse(candidate);
|
|
78
|
+
if (!parsed.success || !Object.prototype.propertyIsEnumerable.call(matrix, candidate)) fail(owner, ".matrix", "axis names must be enumerable strings");
|
|
79
|
+
const axis = parsed.data;
|
|
62
80
|
if (axis.trim() === "") fail(owner, ".matrix", "axis names must not be empty");
|
|
81
|
+
const index = Number(axis);
|
|
82
|
+
if (Number.isInteger(index) && index >= 0 && index < 2 ** 32 - 1 && String(index) === axis) fail(owner, `.matrix.${axis}`, "array-index axis names cannot preserve insertion order");
|
|
83
|
+
const values = matrix[axis];
|
|
63
84
|
if (!Array.isArray(values)) fail(owner, `.matrix.${axis}`, "expected an array");
|
|
64
85
|
if (values.length === 0) fail(owner, `.matrix.${axis}`, "expected at least one value");
|
|
65
|
-
values.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
const snapshots = values.map((value, index) => snapshotJson(value, owner, `.matrix.${axis}[${index}]`));
|
|
87
|
+
const axisValues = /* @__PURE__ */ new Set();
|
|
88
|
+
for (const [index, value] of snapshots.entries()) {
|
|
89
|
+
const key = canonicalJson(value);
|
|
90
|
+
if (axisValues.has(key)) fail(owner, `.matrix.${axis}[${index}]`, `duplicate axis value ${JSON.stringify(value)}`);
|
|
91
|
+
axisValues.add(key);
|
|
92
|
+
}
|
|
93
|
+
cases = cases.flatMap((coordinates) => snapshots.map((value) => ({
|
|
69
94
|
...coordinates,
|
|
70
95
|
[axis]: value
|
|
71
96
|
})));
|
|
72
97
|
}
|
|
73
|
-
const coordinates = /* @__PURE__ */ new Set();
|
|
74
|
-
for (const value of cases) {
|
|
75
|
-
const key = canonicalJson(value);
|
|
76
|
-
if (coordinates.has(key)) fail(owner, ".matrix", `duplicate coordinate ${JSON.stringify(value)}`);
|
|
77
|
-
coordinates.add(key);
|
|
78
|
-
}
|
|
79
98
|
return cases;
|
|
80
99
|
}
|
|
81
100
|
function caseName(matrix, index) {
|
|
@@ -85,9 +104,9 @@ function caseName(matrix, index) {
|
|
|
85
104
|
//#endregion
|
|
86
105
|
//#region src/workflow.ts
|
|
87
106
|
function defineWorkflow(definition) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const prepared of
|
|
107
|
+
describe(definition.name, { concurrent: false }, async () => {
|
|
108
|
+
const tasks = await prepareWorkflow(definition);
|
|
109
|
+
for (const prepared of tasks) describe(prepared.task.name, { concurrent: false }, () => {
|
|
91
110
|
let setup;
|
|
92
111
|
const setupTask = prepared.task.setup;
|
|
93
112
|
if (setupTask) beforeAll(async () => {
|
|
@@ -109,33 +128,30 @@ function defineWorkflow(definition) {
|
|
|
109
128
|
setup,
|
|
110
129
|
vitest
|
|
111
130
|
});
|
|
112
|
-
|
|
113
|
-
meta.invoker.output = output;
|
|
131
|
+
meta.invoker.output = snapshotJson(output, `Task ${JSON.stringify(prepared.task.name)}`, ".output");
|
|
114
132
|
});
|
|
115
133
|
}
|
|
116
134
|
});
|
|
117
135
|
});
|
|
118
136
|
}
|
|
119
|
-
function prepareWorkflow(definition) {
|
|
137
|
+
async function prepareWorkflow(definition) {
|
|
120
138
|
assertPlainObject(definition, "Workflow", "");
|
|
121
139
|
assertOnlyKeys(definition, [
|
|
122
140
|
"name",
|
|
123
141
|
"metadata",
|
|
124
142
|
"tasks"
|
|
125
143
|
], "Workflow");
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (!Array.isArray(definition.tasks) || definition.tasks.length === 0) fail("Workflow", ".tasks", "expected a non-empty Task tuple");
|
|
144
|
+
const { name, metadata: workflowMetadata, tasks: taskDefinitions } = definition;
|
|
145
|
+
assertName(name, "Workflow", ".name");
|
|
146
|
+
const metadata = workflowMetadata === void 0 ? void 0 : snapshotJson(workflowMetadata, `Workflow ${JSON.stringify(name)}`, ".metadata");
|
|
147
|
+
if (metadata !== void 0) assertPlainObject(metadata, `Workflow ${JSON.stringify(name)}`, ".metadata");
|
|
148
|
+
if (!Array.isArray(taskDefinitions) || taskDefinitions.length === 0) fail("Workflow", ".tasks", "expected a non-empty Task tuple");
|
|
133
149
|
const names = /* @__PURE__ */ new Set();
|
|
134
|
-
const tasks =
|
|
135
|
-
const owner = `Workflow ${JSON.stringify(
|
|
150
|
+
const tasks = taskDefinitions.map((value, index) => {
|
|
151
|
+
const owner = `Workflow ${JSON.stringify(name)} Task ${index + 1}`;
|
|
136
152
|
assertPlainObject(value, owner, "");
|
|
153
|
+
if (value[taskDefinitionBrand] !== true) fail(owner, "", "expected a Task created by defineTask");
|
|
137
154
|
const task = value;
|
|
138
|
-
if (task[taskDefinitionBrand] !== true) fail(owner, "", "expected a Task created by defineTask");
|
|
139
155
|
assertOnlyKeys(task, [
|
|
140
156
|
"name",
|
|
141
157
|
"matrix",
|
|
@@ -143,14 +159,19 @@ function prepareWorkflow(definition) {
|
|
|
143
159
|
"run",
|
|
144
160
|
"teardown"
|
|
145
161
|
], owner);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
names.
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
153
|
-
|
|
162
|
+
const taskSnapshot = { ...task };
|
|
163
|
+
assertName(taskSnapshot.name, owner, ".name");
|
|
164
|
+
if (names.has(taskSnapshot.name)) fail(owner, ".name", `duplicate Task name ${JSON.stringify(taskSnapshot.name)}`);
|
|
165
|
+
names.add(taskSnapshot.name);
|
|
166
|
+
if (!z.function().safeParse(taskSnapshot.run).success) fail(owner, ".run", "expected a function");
|
|
167
|
+
if (!z.function().safeParse(taskSnapshot.matrix).success) fail(owner, ".matrix", "expected a function");
|
|
168
|
+
if (taskSnapshot.setup !== void 0 && !z.function().safeParse(taskSnapshot.setup).success) fail(owner, ".setup", "expected a function");
|
|
169
|
+
if (taskSnapshot.teardown !== void 0 && !z.function().safeParse(taskSnapshot.teardown).success) fail(owner, ".teardown", "expected a function");
|
|
170
|
+
if (taskSnapshot.teardown && !taskSnapshot.setup) fail(owner, ".teardown", "requires setup");
|
|
171
|
+
return taskSnapshot;
|
|
172
|
+
});
|
|
173
|
+
return await Promise.all(tasks.map(async (task) => {
|
|
174
|
+
const cases = expandMatrix(await task.matrix(), `Task ${JSON.stringify(task.name)}`);
|
|
154
175
|
return {
|
|
155
176
|
task,
|
|
156
177
|
cases,
|
|
@@ -164,11 +185,7 @@ function prepareWorkflow(definition) {
|
|
|
164
185
|
metadata
|
|
165
186
|
})
|
|
166
187
|
};
|
|
167
|
-
});
|
|
168
|
-
return {
|
|
169
|
-
name: definition.name,
|
|
170
|
-
tasks
|
|
171
|
-
};
|
|
188
|
+
}));
|
|
172
189
|
}
|
|
173
190
|
//#endregion
|
|
174
191
|
export { defineTask, defineWorkflow };
|
package/dist/slack.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Reporter } from "vitest/reporters";
|
|
2
|
+
//#region src/slack/reporter.d.ts
|
|
3
|
+
type SlackReporterOptions = {
|
|
4
|
+
readonly token: string;
|
|
5
|
+
readonly channel: string;
|
|
6
|
+
readonly runUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
declare function slackReporter(options: SlackReporterOptions): Reporter;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { type SlackReporterOptions, slackReporter };
|