@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/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
- function assertJson(value, owner, path, ancestors = /* @__PURE__ */ new Set()) {
14
- if (value === null || typeof value === "string" || typeof value === "boolean") return;
15
- if (typeof value === "number") {
16
- if (!Number.isFinite(value)) fail(owner, path, "expected a finite number");
17
- return;
18
- }
19
- if (typeof value !== "object") fail(owner, path, `expected JSON, received ${typeof value}`);
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)) for (let index = 0; index < value.length; index += 1) {
23
- if (!(index in value)) fail(owner, `${path}[${index}]`, "sparse arrays are not JSON");
24
- assertJson(value[index], owner, `${path}[${index}]`, ancestors);
25
- }
26
- else {
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
- for (const [key, child] of Object.entries(value)) assertJson(child, owner, `${path}.${key}`, ancestors);
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 === null || typeof value !== "object" || Array.isArray(value)) fail(owner, path, "expected a plain object");
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 (typeof value !== "string" || value.trim() === "") fail(owner, path, "expected a non-empty string");
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 !== null && typeof value === "object") {
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 [axis, values] of Object.entries(matrix)) {
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.forEach((value, index) => {
66
- assertJson(value, owner, `.matrix.${axis}[${index}]`);
67
- });
68
- cases = cases.flatMap((coordinates) => values.map((value) => ({
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
- const workflow = prepareWorkflow(definition);
89
- describe(workflow.name, { concurrent: false }, () => {
90
- for (const prepared of workflow.tasks) describe(prepared.task.name, { concurrent: false }, () => {
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
- assertJson(output, `Task ${JSON.stringify(prepared.task.name)}`, ".output");
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
- assertName(definition.name, "Workflow", ".name");
127
- const metadata = definition.metadata;
128
- if (metadata !== void 0) {
129
- assertJson(metadata, `Workflow ${JSON.stringify(definition.name)}`, ".metadata");
130
- assertPlainObject(metadata, `Workflow ${JSON.stringify(definition.name)}`, ".metadata");
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 = definition.tasks.map((value, index) => {
135
- const owner = `Workflow ${JSON.stringify(definition.name)} Task ${index + 1}`;
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
- assertName(task.name, owner, ".name");
147
- if (names.has(task.name)) fail(owner, ".name", `duplicate Task name ${JSON.stringify(task.name)}`);
148
- names.add(task.name);
149
- if (typeof task.run !== "function") fail(owner, ".run", "expected a function");
150
- if (task.setup !== void 0 && typeof task.setup !== "function") fail(owner, ".setup", "expected a function");
151
- if (task.teardown !== void 0 && typeof task.teardown !== "function") fail(owner, ".teardown", "expected a function");
152
- if (task.teardown && !task.setup) fail(owner, ".teardown", "requires setup");
153
- const cases = expandMatrix(task.matrix, `Task ${JSON.stringify(task.name)}`);
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 };
@@ -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 };