@schmock/core 2.3.0 → 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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/builder.d.ts +31 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +760 -190
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +123 -2
  12. package/dist/errors.d.ts +14 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +41 -6
  15. package/dist/helpers.d.ts +9 -0
  16. package/dist/helpers.d.ts.map +1 -1
  17. package/dist/helpers.js +18 -2
  18. package/dist/http-helpers.d.ts +39 -4
  19. package/dist/http-helpers.d.ts.map +1 -1
  20. package/dist/http-helpers.js +147 -49
  21. package/dist/index.d.ts +292 -34
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -3
  24. package/dist/interceptor.d.ts +11 -1
  25. package/dist/interceptor.d.ts.map +1 -1
  26. package/dist/interceptor.js +316 -169
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +12 -3
  29. package/dist/plugin-pipeline.d.ts +3 -3
  30. package/dist/plugin-pipeline.d.ts.map +1 -1
  31. package/dist/plugin-pipeline.js +29 -14
  32. package/dist/response-normalizer.d.ts +16 -0
  33. package/dist/response-normalizer.d.ts.map +1 -0
  34. package/dist/response-normalizer.js +316 -0
  35. package/dist/response-parser.d.ts.map +1 -1
  36. package/dist/response-parser.js +39 -2
  37. package/dist/route-matcher.d.ts +3 -0
  38. package/dist/route-matcher.d.ts.map +1 -1
  39. package/dist/route-matcher.js +12 -6
  40. package/dist/types.d.ts +7 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +14 -6
  43. package/src/audit-core-builder.test.ts +0 -218
  44. package/src/audit-response-guard.test.ts +0 -38
  45. package/src/binary.test.ts +0 -126
  46. package/src/binary.ts +0 -11
  47. package/src/builder.test.ts +0 -289
  48. package/src/builder.ts +0 -748
  49. package/src/constants.test.ts +0 -99
  50. package/src/constants.ts +0 -73
  51. package/src/debug.test.ts +0 -241
  52. package/src/delay.test.ts +0 -319
  53. package/src/dist-shape.test.ts +0 -35
  54. package/src/errors.test.ts +0 -223
  55. package/src/errors.ts +0 -130
  56. package/src/factory.test.ts +0 -133
  57. package/src/helpers.test.ts +0 -147
  58. package/src/helpers.ts +0 -58
  59. package/src/http-helpers.test.ts +0 -39
  60. package/src/http-helpers.ts +0 -149
  61. package/src/index.ts +0 -152
  62. package/src/interceptor.test.ts +0 -488
  63. package/src/interceptor.ts +0 -407
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -630
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -208
  69. package/src/plugin-system.test.ts +0 -697
  70. package/src/response-parser.ts +0 -114
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -234
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -421
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -83
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -555
  82. package/src/steps/fetch-interceptor.steps.ts +0 -457
  83. package/src/steps/fluent-api.steps.ts +0 -255
  84. package/src/steps/http-methods.steps.ts +0 -342
  85. package/src/steps/lifecycle-events.steps.ts +0 -149
  86. package/src/steps/performance-reliability.steps.ts +0 -90
  87. package/src/steps/plugin-integration.steps.ts +0 -334
  88. package/src/steps/request-history.steps.ts +0 -458
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -227
  92. package/src/steps/state-concurrency.steps.ts +0 -167
  93. package/src/types.ts +0 -36
@@ -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
- }
@@ -1,208 +0,0 @@
1
- import { errorMessage, PluginError } from "./errors.js";
2
-
3
- /** Structural typing — DebugLogger satisfies this without an import */
4
- interface PipelineLogger {
5
- log(category: string, message: string, data?: unknown): void;
6
- }
7
-
8
- interface PipelineResult {
9
- context: Schmock.PluginContext;
10
- response?: unknown;
11
- recoveredFromError?: boolean;
12
- requestShortCircuited?: boolean;
13
- }
14
-
15
- function isPluginResult(value: unknown): value is Schmock.PluginResult {
16
- return (
17
- typeof value === "object" &&
18
- value !== null &&
19
- "context" in value &&
20
- typeof value.context === "object" &&
21
- value.context !== null
22
- );
23
- }
24
-
25
- async function recoverPluginError(
26
- plugins: Schmock.Plugin[],
27
- startIndex: number,
28
- error: unknown,
29
- context: Schmock.PluginContext,
30
- logger: PipelineLogger,
31
- ): Promise<
32
- { handled: true; response: unknown } | { handled: false; error: Error }
33
- > {
34
- let currentError =
35
- error instanceof Error ? error : new Error(errorMessage(error));
36
-
37
- for (let index = startIndex; index < plugins.length; index += 1) {
38
- const plugin = plugins[index];
39
- if (!plugin.onError) continue;
40
-
41
- try {
42
- const errorResult = await plugin.onError(currentError, context);
43
- if (errorResult instanceof Error) {
44
- currentError = errorResult;
45
- continue;
46
- }
47
- if (errorResult !== undefined) {
48
- logger.log("pipeline", `Plugin ${plugin.name} handled error`);
49
- return { handled: true, response: errorResult };
50
- }
51
- } catch (hookError) {
52
- currentError =
53
- hookError instanceof Error
54
- ? hookError
55
- : new Error(errorMessage(hookError));
56
- logger.log(
57
- "pipeline",
58
- `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`,
59
- );
60
- }
61
- }
62
-
63
- return { handled: false, error: currentError };
64
- }
65
-
66
- /** Run request guards before route code is allowed to execute. */
67
- export async function runPluginBeforeRequest(
68
- plugins: Schmock.Plugin[],
69
- context: Schmock.PluginContext,
70
- logger: PipelineLogger,
71
- ): Promise<PipelineResult> {
72
- let currentContext = context;
73
-
74
- for (let index = 0; index < plugins.length; index += 1) {
75
- const plugin = plugins[index];
76
- if (!plugin.beforeRequest) continue;
77
-
78
- logger.log("pipeline", `Running beforeRequest: ${plugin.name}`);
79
- try {
80
- const result = await plugin.beforeRequest(currentContext);
81
- if (result === undefined) continue;
82
- if (!isPluginResult(result)) {
83
- throw new Error(`Plugin ${plugin.name} didn't return valid result`);
84
- }
85
-
86
- currentContext = result.context;
87
- if (result.response !== undefined) {
88
- logger.log("pipeline", `Plugin ${plugin.name} rejected request`);
89
- return {
90
- context: currentContext,
91
- response: result.response,
92
- requestShortCircuited: true,
93
- };
94
- }
95
- } catch (error) {
96
- logger.log(
97
- "pipeline",
98
- `Plugin ${plugin.name} beforeRequest failed: ${errorMessage(error)}`,
99
- );
100
- const recovery = await recoverPluginError(
101
- plugins,
102
- index,
103
- error,
104
- currentContext,
105
- logger,
106
- );
107
- if (recovery.handled) {
108
- return {
109
- context: currentContext,
110
- response: recovery.response,
111
- recoveredFromError: true,
112
- requestShortCircuited: true,
113
- };
114
- }
115
- throw new PluginError(plugin.name, recovery.error);
116
- }
117
- }
118
-
119
- return { context: currentContext };
120
- }
121
-
122
- /** Give pipeline error handlers a chance to recover a route-generator error. */
123
- export async function recoverGeneratorError(
124
- plugins: Schmock.Plugin[],
125
- context: Schmock.PluginContext,
126
- error: unknown,
127
- logger: PipelineLogger,
128
- ): Promise<PipelineResult> {
129
- const recovery = await recoverPluginError(plugins, 0, error, context, logger);
130
- if (recovery.handled) {
131
- return {
132
- context,
133
- response: recovery.response,
134
- recoveredFromError: true,
135
- };
136
- }
137
- throw recovery.error;
138
- }
139
-
140
- /**
141
- * Run all registered plugins in sequence
142
- * First plugin to set response becomes generator, subsequent plugins transform
143
- * Handles plugin errors via onError hooks
144
- */
145
- export async function runPluginPipeline(
146
- plugins: Schmock.Plugin[],
147
- context: Schmock.PluginContext,
148
- initialResponse: unknown,
149
- logger: PipelineLogger,
150
- ): Promise<PipelineResult> {
151
- let currentContext = context;
152
- let response: unknown = initialResponse;
153
-
154
- logger.log(
155
- "pipeline",
156
- `Running plugin pipeline for ${plugins.length} plugins`,
157
- );
158
-
159
- for (let index = 0; index < plugins.length; index += 1) {
160
- const plugin = plugins[index];
161
- logger.log("pipeline", `Processing plugin: ${plugin.name}`);
162
-
163
- try {
164
- const result = await plugin.process(currentContext, response);
165
-
166
- if (!isPluginResult(result)) {
167
- throw new Error(`Plugin ${plugin.name} didn't return valid result`);
168
- }
169
-
170
- currentContext = result.context;
171
-
172
- // First plugin to set response becomes the generator
173
- if (
174
- result.response !== undefined &&
175
- (response === undefined || response === null)
176
- ) {
177
- logger.log("pipeline", `Plugin ${plugin.name} generated response`);
178
- response = result.response;
179
- } else if (result.response !== undefined && response !== undefined) {
180
- logger.log("pipeline", `Plugin ${plugin.name} transformed response`);
181
- response = result.response;
182
- }
183
- } catch (error) {
184
- logger.log(
185
- "pipeline",
186
- `Plugin ${plugin.name} failed: ${errorMessage(error)}`,
187
- );
188
-
189
- const recovery = await recoverPluginError(
190
- plugins,
191
- index,
192
- error,
193
- currentContext,
194
- logger,
195
- );
196
- if (recovery.handled) {
197
- return {
198
- context: currentContext,
199
- response: recovery.response,
200
- recoveredFromError: true,
201
- };
202
- }
203
- throw new PluginError(plugin.name, recovery.error);
204
- }
205
- }
206
-
207
- return { context: currentContext, response };
208
- }