@schmock/core 2.2.3 → 2.3.1
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +804 -203
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +52 -5
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +153 -37
- package/dist/index.d.ts +381 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +354 -156
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +117 -39
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +85 -18
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -701
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.ts +0 -113
- package/src/index.ts +0 -151
- package/src/interceptor.test.ts +0 -291
- package/src/interceptor.ts +0 -320
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -594
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -103
- package/src/plugin-system.test.ts +0 -602
- package/src/response-parser.ts +0 -75
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -165
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -400
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -99
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -345
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -331
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/lifecycle-events.steps.ts +0 -142
- package/src/steps/performance-reliability.steps.ts +0 -423
- package/src/steps/plugin-integration.steps.ts +0 -280
- package/src/steps/request-history.steps.ts +0 -276
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -233
- package/src/steps/state-concurrency.steps.ts +0 -739
- package/src/steps/stateful-workflows.steps.ts +0 -353
- package/src/types.ts +0 -36
package/src/parser.test.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { parseRouteKey } from "./parser";
|
|
3
|
-
|
|
4
|
-
describe("parseRouteKey", () => {
|
|
5
|
-
describe("valid route keys", () => {
|
|
6
|
-
it("parses simple GET route", () => {
|
|
7
|
-
const result = parseRouteKey("GET /users");
|
|
8
|
-
expect(result).toEqual({
|
|
9
|
-
method: "GET",
|
|
10
|
-
path: "/users",
|
|
11
|
-
pattern: expect.any(RegExp),
|
|
12
|
-
params: [],
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("parses route with single parameter", () => {
|
|
17
|
-
const result = parseRouteKey("GET /users/:id");
|
|
18
|
-
expect(result).toEqual({
|
|
19
|
-
method: "GET",
|
|
20
|
-
path: "/users/:id",
|
|
21
|
-
pattern: expect.any(RegExp),
|
|
22
|
-
params: ["id"],
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("parses route with multiple parameters", () => {
|
|
27
|
-
const result = parseRouteKey(
|
|
28
|
-
"DELETE /api/posts/:postId/comments/:commentId",
|
|
29
|
-
);
|
|
30
|
-
expect(result).toEqual({
|
|
31
|
-
method: "DELETE",
|
|
32
|
-
path: "/api/posts/:postId/comments/:commentId",
|
|
33
|
-
pattern: expect.any(RegExp),
|
|
34
|
-
params: ["postId", "commentId"],
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("supports all HTTP methods", () => {
|
|
39
|
-
const methods = [
|
|
40
|
-
"GET",
|
|
41
|
-
"POST",
|
|
42
|
-
"PUT",
|
|
43
|
-
"DELETE",
|
|
44
|
-
"PATCH",
|
|
45
|
-
"HEAD",
|
|
46
|
-
"OPTIONS",
|
|
47
|
-
] as const;
|
|
48
|
-
|
|
49
|
-
for (const method of methods) {
|
|
50
|
-
const result = parseRouteKey(`${method} /test`);
|
|
51
|
-
expect(result.method).toBe(method);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("handles paths with namespace", () => {
|
|
56
|
-
const result = parseRouteKey("POST /api/v2/users");
|
|
57
|
-
expect(result).toEqual({
|
|
58
|
-
method: "POST",
|
|
59
|
-
path: "/api/v2/users",
|
|
60
|
-
pattern: expect.any(RegExp),
|
|
61
|
-
params: [],
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("creates correct regex pattern for simple path", () => {
|
|
66
|
-
const result = parseRouteKey("GET /users");
|
|
67
|
-
expect("/users").toMatch(result.pattern);
|
|
68
|
-
expect("/users/123").not.toMatch(result.pattern);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("creates correct regex pattern with parameters", () => {
|
|
72
|
-
const result = parseRouteKey("GET /users/:id");
|
|
73
|
-
expect("/users/123").toMatch(result.pattern);
|
|
74
|
-
expect("/users/abc-def").toMatch(result.pattern);
|
|
75
|
-
expect("/users").not.toMatch(result.pattern);
|
|
76
|
-
expect("/users/").not.toMatch(result.pattern);
|
|
77
|
-
expect("/users/123/posts").not.toMatch(result.pattern);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("invalid route keys", () => {
|
|
82
|
-
it("throws on missing method", () => {
|
|
83
|
-
expect(() => parseRouteKey("/users")).toThrow("Invalid route key format");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("throws on missing path", () => {
|
|
87
|
-
expect(() => parseRouteKey("GET")).toThrow("Invalid route key format");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("throws on invalid method", () => {
|
|
91
|
-
expect(() => parseRouteKey("INVALID /users")).toThrow(
|
|
92
|
-
"Invalid route key format",
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("throws on lowercase method", () => {
|
|
97
|
-
expect(() => parseRouteKey("get /users")).toThrow(
|
|
98
|
-
"Invalid route key format",
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("throws on missing space", () => {
|
|
103
|
-
expect(() => parseRouteKey("GET/users")).toThrow(
|
|
104
|
-
"Invalid route key format",
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("throws on empty string", () => {
|
|
109
|
-
expect(() => parseRouteKey("")).toThrow("Invalid route key format");
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("parameter extraction", () => {
|
|
114
|
-
it("extracts matched parameters", () => {
|
|
115
|
-
const route = parseRouteKey("GET /users/:userId/posts/:postId");
|
|
116
|
-
const match = "/users/123/posts/456".match(route.pattern);
|
|
117
|
-
|
|
118
|
-
expect(match).toBeTruthy();
|
|
119
|
-
expect(match?.[1]).toBe("123");
|
|
120
|
-
expect(match?.[2]).toBe("456");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("handles special characters in parameters", () => {
|
|
124
|
-
const route = parseRouteKey("GET /files/:filename");
|
|
125
|
-
const match = "/files/report-2023.pdf".match(route.pattern);
|
|
126
|
-
|
|
127
|
-
expect(match).toBeTruthy();
|
|
128
|
-
expect(match?.[1]).toBe("report-2023.pdf");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("stops the param name at a literal '.' suffix and escapes the dot", () => {
|
|
132
|
-
const route = parseRouteKey("GET /files/:name.json");
|
|
133
|
-
|
|
134
|
-
expect(route.params).toEqual(["name"]);
|
|
135
|
-
expect("/files/report.json".match(route.pattern)?.[1]).toBe("report");
|
|
136
|
-
expect("/files/report.json2".match(route.pattern)).toBeNull();
|
|
137
|
-
expect("/files/reportXjson".match(route.pattern)).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("treats parens around params as literal characters", () => {
|
|
141
|
-
const route = parseRouteKey("GET /items/(:id)");
|
|
142
|
-
|
|
143
|
-
expect(route.params).toEqual(["id"]);
|
|
144
|
-
expect("/items/(42)".match(route.pattern)?.[1]).toBe("42");
|
|
145
|
-
expect("/items/42".match(route.pattern)).toBeNull();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
});
|
package/src/parser.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { toHttpMethod } from "./constants.js";
|
|
2
|
-
import { RouteParseError } from "./errors.js";
|
|
3
|
-
import type { HttpMethod } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export interface ParsedRoute {
|
|
6
|
-
method: HttpMethod;
|
|
7
|
-
path: string;
|
|
8
|
-
pattern: RegExp;
|
|
9
|
-
params: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Parse 'METHOD /path' route key format
|
|
14
|
-
*
|
|
15
|
-
* Design note: We validate the format strictly to catch typos early.
|
|
16
|
-
* The 'METHOD /path' format was chosen for its readability and
|
|
17
|
-
* similarity to API documentation formats.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* parseRouteKey('GET /users/:id')
|
|
21
|
-
* // => { method: 'GET', path: '/users/:id', pattern: /^\/users\/([^/]+)$/, params: ['id'] }
|
|
22
|
-
*/
|
|
23
|
-
export function parseRouteKey(routeKey: string): ParsedRoute {
|
|
24
|
-
const match = routeKey.match(
|
|
25
|
-
/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (.+)$/,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
if (!match) {
|
|
29
|
-
throw new RouteParseError(
|
|
30
|
-
routeKey,
|
|
31
|
-
'Expected format: "METHOD /path" (e.g., "GET /users")',
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const [, method, path] = match;
|
|
36
|
-
|
|
37
|
-
// Parameter names are restricted to [A-Za-z0-9_-] so that surrounding
|
|
38
|
-
// literals (".json", brackets, parens, etc.) terminate the name and can
|
|
39
|
-
// be escaped without bleeding into the parameter regex. Build the
|
|
40
|
-
// pattern by splitting the path on the param marker, escaping each
|
|
41
|
-
// literal segment, then substituting the capture group for each :name.
|
|
42
|
-
const params: string[] = [];
|
|
43
|
-
|
|
44
|
-
const regexPath = path
|
|
45
|
-
.split(/(:[a-zA-Z0-9_-]+)/g)
|
|
46
|
-
.map((segment) => {
|
|
47
|
-
const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
|
|
48
|
-
if (paramMatch) {
|
|
49
|
-
params.push(paramMatch[1]);
|
|
50
|
-
return "([^/]+)";
|
|
51
|
-
}
|
|
52
|
-
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
53
|
-
})
|
|
54
|
-
.join("");
|
|
55
|
-
|
|
56
|
-
const pattern = new RegExp(`^${regexPath}$`);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
method: toHttpMethod(method),
|
|
60
|
-
path,
|
|
61
|
-
pattern,
|
|
62
|
-
params,
|
|
63
|
-
};
|
|
64
|
-
}
|
package/src/plugin-pipeline.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { isStatusTuple } from "./constants.js";
|
|
2
|
-
import { errorMessage, PluginError } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
/** Structural typing — DebugLogger satisfies this without an import */
|
|
5
|
-
interface PipelineLogger {
|
|
6
|
-
log(category: string, message: string, data?: unknown): void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Run all registered plugins in sequence
|
|
11
|
-
* First plugin to set response becomes generator, subsequent plugins transform
|
|
12
|
-
* Handles plugin errors via onError hooks
|
|
13
|
-
*/
|
|
14
|
-
export async function runPluginPipeline(
|
|
15
|
-
plugins: Schmock.Plugin[],
|
|
16
|
-
context: Schmock.PluginContext,
|
|
17
|
-
initialResponse: unknown,
|
|
18
|
-
logger: PipelineLogger,
|
|
19
|
-
): Promise<{ context: Schmock.PluginContext; response?: unknown }> {
|
|
20
|
-
let currentContext = context;
|
|
21
|
-
let response: unknown = initialResponse;
|
|
22
|
-
|
|
23
|
-
logger.log(
|
|
24
|
-
"pipeline",
|
|
25
|
-
`Running plugin pipeline for ${plugins.length} plugins`,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
for (const plugin of plugins) {
|
|
29
|
-
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const result = await plugin.process(currentContext, response);
|
|
33
|
-
|
|
34
|
-
if (!result?.context) {
|
|
35
|
-
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
currentContext = result.context;
|
|
39
|
-
|
|
40
|
-
// First plugin to set response becomes the generator
|
|
41
|
-
if (
|
|
42
|
-
result.response !== undefined &&
|
|
43
|
-
(response === undefined || response === null)
|
|
44
|
-
) {
|
|
45
|
-
logger.log("pipeline", `Plugin ${plugin.name} generated response`);
|
|
46
|
-
response = result.response;
|
|
47
|
-
} else if (result.response !== undefined && response !== undefined) {
|
|
48
|
-
logger.log("pipeline", `Plugin ${plugin.name} transformed response`);
|
|
49
|
-
response = result.response;
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
logger.log(
|
|
53
|
-
"pipeline",
|
|
54
|
-
`Plugin ${plugin.name} failed: ${errorMessage(error)}`,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Try error handling if plugin has onError hook
|
|
58
|
-
if (plugin.onError) {
|
|
59
|
-
try {
|
|
60
|
-
const pluginError =
|
|
61
|
-
error instanceof Error ? error : new Error(errorMessage(error));
|
|
62
|
-
const errorResult = await plugin.onError(pluginError, currentContext);
|
|
63
|
-
if (errorResult) {
|
|
64
|
-
logger.log("pipeline", `Plugin ${plugin.name} handled error`);
|
|
65
|
-
|
|
66
|
-
// Error return → transform the thrown error
|
|
67
|
-
if (errorResult instanceof Error) {
|
|
68
|
-
throw new PluginError(plugin.name, errorResult);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ResponseResult return → recover, stop pipeline
|
|
72
|
-
// Accept both object-with-status and status tuple [code, body] / [code, body, headers]
|
|
73
|
-
if (
|
|
74
|
-
isStatusTuple(errorResult) ||
|
|
75
|
-
(typeof errorResult === "object" &&
|
|
76
|
-
errorResult !== null &&
|
|
77
|
-
"status" in errorResult)
|
|
78
|
-
) {
|
|
79
|
-
response = errorResult;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// void/falsy return → propagate original error below
|
|
84
|
-
} catch (hookError) {
|
|
85
|
-
// If the hook itself threw (including our PluginError above), re-throw it
|
|
86
|
-
if (hookError instanceof PluginError) {
|
|
87
|
-
throw hookError;
|
|
88
|
-
}
|
|
89
|
-
logger.log(
|
|
90
|
-
"pipeline",
|
|
91
|
-
`Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const cause =
|
|
97
|
-
error instanceof Error ? error : new Error(errorMessage(error));
|
|
98
|
-
throw new PluginError(plugin.name, cause);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { context: currentContext, response };
|
|
103
|
-
}
|