@schmock/core 1.0.4 → 1.2.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/dist/builder.d.ts +15 -5
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +148 -60
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -12
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +2 -17
- package/dist/types.d.ts +17 -214
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +4 -4
- package/src/builder.test.ts +2 -2
- package/src/builder.ts +226 -108
- package/src/constants.ts +17 -1
- package/src/index.ts +34 -28
- package/src/namespace.test.ts +3 -2
- package/src/parser.property.test.ts +493 -0
- package/src/parser.ts +2 -20
- package/src/plugin-system.test.ts +91 -0
- package/src/response-parsing.test.ts +11 -7
- package/src/steps/async-support.steps.ts +101 -91
- package/src/steps/basic-usage.steps.ts +49 -36
- package/src/steps/developer-experience.steps.ts +95 -97
- package/src/steps/error-handling.steps.ts +71 -72
- package/src/steps/fluent-api.steps.ts +75 -72
- package/src/steps/http-methods.steps.ts +33 -33
- package/src/steps/performance-reliability.steps.ts +52 -88
- package/src/steps/plugin-integration.steps.ts +176 -176
- package/src/steps/request-history.steps.ts +333 -0
- package/src/steps/state-concurrency.steps.ts +418 -316
- package/src/steps/stateful-workflows.steps.ts +138 -136
- package/src/types.ts +20 -271
package/dist/builder.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Generator, GlobalConfig, HttpMethod, Plugin, RequestOptions, Response, RouteConfig, RouteKey } from "./types.js";
|
|
2
1
|
/**
|
|
3
2
|
* Callable mock instance that implements the new API.
|
|
4
3
|
*
|
|
@@ -7,12 +6,23 @@ import type { Generator, GlobalConfig, HttpMethod, Plugin, RequestOptions, Respo
|
|
|
7
6
|
export declare class CallableMockInstance {
|
|
8
7
|
private globalConfig;
|
|
9
8
|
private routes;
|
|
9
|
+
private staticRoutes;
|
|
10
10
|
private plugins;
|
|
11
11
|
private logger;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
private requestHistory;
|
|
13
|
+
private callableRef;
|
|
14
|
+
constructor(globalConfig?: Schmock.GlobalConfig);
|
|
15
|
+
defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
|
|
16
|
+
setCallableRef(ref: Schmock.CallableMockInstance): void;
|
|
17
|
+
pipe(plugin: Schmock.Plugin): this;
|
|
18
|
+
history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[];
|
|
19
|
+
called(method?: Schmock.HttpMethod, path?: string): boolean;
|
|
20
|
+
callCount(method?: Schmock.HttpMethod, path?: string): number;
|
|
21
|
+
lastRequest(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord | undefined;
|
|
22
|
+
reset(): void;
|
|
23
|
+
resetHistory(): void;
|
|
24
|
+
resetState(): void;
|
|
25
|
+
handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
|
|
16
26
|
/**
|
|
17
27
|
* Apply configured response delay
|
|
18
28
|
* Supports both fixed delays and random delays within a range
|
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA0EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAQnB,OAAO,CAAC,YAAY;IAPhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;gBAE1C,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAqFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,KAAK,IAAI,IAAI;IAab,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IASZ,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoL5B;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
|
package/dist/builder.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { isStatusTuple } from "./constants.js";
|
|
1
2
|
import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
|
|
2
3
|
import { parseRouteKey } from "./parser.js";
|
|
4
|
+
function errorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : "Unknown error";
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Debug logger that respects debug mode configuration
|
|
5
9
|
*/
|
|
@@ -31,6 +35,15 @@ class DebugLogger {
|
|
|
31
35
|
console.timeEnd(`[SCHMOCK] ${label}`);
|
|
32
36
|
}
|
|
33
37
|
}
|
|
38
|
+
function isGeneratorFunction(gen) {
|
|
39
|
+
return typeof gen === "function";
|
|
40
|
+
}
|
|
41
|
+
function isResponseObject(value) {
|
|
42
|
+
return (typeof value === "object" &&
|
|
43
|
+
value !== null &&
|
|
44
|
+
"status" in value &&
|
|
45
|
+
"body" in value);
|
|
46
|
+
}
|
|
34
47
|
/**
|
|
35
48
|
* Callable mock instance that implements the new API.
|
|
36
49
|
*
|
|
@@ -39,8 +52,11 @@ class DebugLogger {
|
|
|
39
52
|
export class CallableMockInstance {
|
|
40
53
|
globalConfig;
|
|
41
54
|
routes = [];
|
|
55
|
+
staticRoutes = new Map();
|
|
42
56
|
plugins = [];
|
|
43
57
|
logger;
|
|
58
|
+
requestHistory = [];
|
|
59
|
+
callableRef;
|
|
44
60
|
constructor(globalConfig = {}) {
|
|
45
61
|
this.globalConfig = globalConfig;
|
|
46
62
|
this.logger = new DebugLogger(globalConfig.debug || false);
|
|
@@ -103,6 +119,17 @@ export class CallableMockInstance {
|
|
|
103
119
|
config,
|
|
104
120
|
};
|
|
105
121
|
this.routes.push(compiledRoute);
|
|
122
|
+
// Store static routes (no params) in Map for O(1) lookup
|
|
123
|
+
// Only store the first registration — "first registration wins" semantics
|
|
124
|
+
if (parsed.params.length === 0) {
|
|
125
|
+
const normalizedPath = parsed.path.endsWith("/") && parsed.path !== "/"
|
|
126
|
+
? parsed.path.slice(0, -1)
|
|
127
|
+
: parsed.path;
|
|
128
|
+
const key = `${parsed.method} ${normalizedPath}`;
|
|
129
|
+
if (!this.staticRoutes.has(key)) {
|
|
130
|
+
this.staticRoutes.set(key, compiledRoute);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
106
133
|
this.logger.log("route", `Route defined: ${route}`, {
|
|
107
134
|
contentType: config.contentType,
|
|
108
135
|
generatorType: typeof generator,
|
|
@@ -110,6 +137,9 @@ export class CallableMockInstance {
|
|
|
110
137
|
});
|
|
111
138
|
return this;
|
|
112
139
|
}
|
|
140
|
+
setCallableRef(ref) {
|
|
141
|
+
this.callableRef = ref;
|
|
142
|
+
}
|
|
113
143
|
pipe(plugin) {
|
|
114
144
|
this.plugins.push(plugin);
|
|
115
145
|
this.logger.log("plugin", `Registered plugin: ${plugin.name}@${plugin.version || "unknown"}`, {
|
|
@@ -118,10 +148,64 @@ export class CallableMockInstance {
|
|
|
118
148
|
hasProcess: typeof plugin.process === "function",
|
|
119
149
|
hasOnError: typeof plugin.onError === "function",
|
|
120
150
|
});
|
|
151
|
+
if (plugin.install && this.callableRef) {
|
|
152
|
+
plugin.install(this.callableRef);
|
|
153
|
+
}
|
|
121
154
|
return this;
|
|
122
155
|
}
|
|
156
|
+
// ===== Request Spy / History API =====
|
|
157
|
+
history(method, path) {
|
|
158
|
+
if (method && path) {
|
|
159
|
+
return this.requestHistory.filter((r) => r.method === method && r.path === path);
|
|
160
|
+
}
|
|
161
|
+
return [...this.requestHistory];
|
|
162
|
+
}
|
|
163
|
+
called(method, path) {
|
|
164
|
+
if (method && path) {
|
|
165
|
+
return this.requestHistory.some((r) => r.method === method && r.path === path);
|
|
166
|
+
}
|
|
167
|
+
return this.requestHistory.length > 0;
|
|
168
|
+
}
|
|
169
|
+
callCount(method, path) {
|
|
170
|
+
if (method && path) {
|
|
171
|
+
return this.requestHistory.filter((r) => r.method === method && r.path === path).length;
|
|
172
|
+
}
|
|
173
|
+
return this.requestHistory.length;
|
|
174
|
+
}
|
|
175
|
+
lastRequest(method, path) {
|
|
176
|
+
if (method && path) {
|
|
177
|
+
const filtered = this.requestHistory.filter((r) => r.method === method && r.path === path);
|
|
178
|
+
return filtered[filtered.length - 1];
|
|
179
|
+
}
|
|
180
|
+
return this.requestHistory[this.requestHistory.length - 1];
|
|
181
|
+
}
|
|
182
|
+
// ===== Reset / Lifecycle =====
|
|
183
|
+
reset() {
|
|
184
|
+
this.routes = [];
|
|
185
|
+
this.staticRoutes.clear();
|
|
186
|
+
this.plugins = [];
|
|
187
|
+
this.requestHistory = [];
|
|
188
|
+
if (this.globalConfig.state) {
|
|
189
|
+
for (const key of Object.keys(this.globalConfig.state)) {
|
|
190
|
+
delete this.globalConfig.state[key];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this.logger.log("lifecycle", "Mock fully reset");
|
|
194
|
+
}
|
|
195
|
+
resetHistory() {
|
|
196
|
+
this.requestHistory = [];
|
|
197
|
+
this.logger.log("lifecycle", "Request history cleared");
|
|
198
|
+
}
|
|
199
|
+
resetState() {
|
|
200
|
+
if (this.globalConfig.state) {
|
|
201
|
+
for (const key of Object.keys(this.globalConfig.state)) {
|
|
202
|
+
delete this.globalConfig.state[key];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.logger.log("lifecycle", "State cleared");
|
|
206
|
+
}
|
|
123
207
|
async handle(method, path, options) {
|
|
124
|
-
const requestId =
|
|
208
|
+
const requestId = crypto.randomUUID();
|
|
125
209
|
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
126
210
|
headers: options?.headers,
|
|
127
211
|
query: options?.query,
|
|
@@ -131,40 +215,30 @@ export class CallableMockInstance {
|
|
|
131
215
|
try {
|
|
132
216
|
// Apply namespace if configured
|
|
133
217
|
let requestPath = path;
|
|
134
|
-
if (this.globalConfig.namespace) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const response = {
|
|
155
|
-
status: 404,
|
|
156
|
-
body: { error: error.message, code: error.code },
|
|
157
|
-
headers: {},
|
|
158
|
-
};
|
|
159
|
-
this.logger.timeEnd(`request-${requestId}`);
|
|
160
|
-
return response;
|
|
161
|
-
}
|
|
162
|
-
// Remove namespace prefix, ensuring we always start with /
|
|
163
|
-
requestPath = normalizedPath.substring(finalNamespace.length);
|
|
164
|
-
if (!requestPath.startsWith("/")) {
|
|
165
|
-
requestPath = `/${requestPath}`;
|
|
166
|
-
}
|
|
218
|
+
if (this.globalConfig.namespace && this.globalConfig.namespace !== "/") {
|
|
219
|
+
const namespace = this.globalConfig.namespace.startsWith("/")
|
|
220
|
+
? this.globalConfig.namespace
|
|
221
|
+
: `/${this.globalConfig.namespace}`;
|
|
222
|
+
const pathToCheck = path.startsWith("/") ? path : `/${path}`;
|
|
223
|
+
// Check if path starts with namespace
|
|
224
|
+
// handle both "/api/users" (starts with /api) and "/api" (exact match)
|
|
225
|
+
// but NOT "/apiv2" (prefix match but wrong segment)
|
|
226
|
+
const isMatch = pathToCheck === namespace ||
|
|
227
|
+
pathToCheck.startsWith(namespace.endsWith("/") ? namespace : `${namespace}/`);
|
|
228
|
+
if (!isMatch) {
|
|
229
|
+
this.logger.log("route", `[${requestId}] Path doesn't match namespace ${namespace}`);
|
|
230
|
+
const error = new RouteNotFoundError(method, path);
|
|
231
|
+
const response = {
|
|
232
|
+
status: 404,
|
|
233
|
+
body: { error: error.message, code: error.code },
|
|
234
|
+
headers: {},
|
|
235
|
+
};
|
|
236
|
+
this.logger.timeEnd(`request-${requestId}`);
|
|
237
|
+
return response;
|
|
167
238
|
}
|
|
239
|
+
// Remove namespace prefix, ensuring we always start with /
|
|
240
|
+
const stripped = pathToCheck.slice(namespace.length);
|
|
241
|
+
requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
|
|
168
242
|
}
|
|
169
243
|
// Find matching route
|
|
170
244
|
const matchedRoute = this.findRoute(method, requestPath);
|
|
@@ -193,7 +267,7 @@ export class CallableMockInstance {
|
|
|
193
267
|
state: this.globalConfig.state || {},
|
|
194
268
|
};
|
|
195
269
|
let result;
|
|
196
|
-
if (
|
|
270
|
+
if (isGeneratorFunction(matchedRoute.generator)) {
|
|
197
271
|
result = await matchedRoute.generator(context);
|
|
198
272
|
}
|
|
199
273
|
else {
|
|
@@ -218,13 +292,24 @@ export class CallableMockInstance {
|
|
|
218
292
|
result = pipelineResult.response;
|
|
219
293
|
}
|
|
220
294
|
catch (error) {
|
|
221
|
-
this.logger.log("error", `[${requestId}] Plugin pipeline error: ${error
|
|
295
|
+
this.logger.log("error", `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`);
|
|
222
296
|
throw error;
|
|
223
297
|
}
|
|
224
298
|
// Parse and prepare response
|
|
225
299
|
const response = this.parseResponse(result, matchedRoute.config);
|
|
226
300
|
// Apply global delay if configured
|
|
227
301
|
await this.applyDelay();
|
|
302
|
+
// Record request in history
|
|
303
|
+
this.requestHistory.push({
|
|
304
|
+
method,
|
|
305
|
+
path: requestPath,
|
|
306
|
+
params,
|
|
307
|
+
query: options?.query || {},
|
|
308
|
+
headers: options?.headers || {},
|
|
309
|
+
body: options?.body,
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
response: { status: response.status, body: response.body },
|
|
312
|
+
});
|
|
228
313
|
// Log successful response
|
|
229
314
|
this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
|
|
230
315
|
status: response.status,
|
|
@@ -235,15 +320,13 @@ export class CallableMockInstance {
|
|
|
235
320
|
return response;
|
|
236
321
|
}
|
|
237
322
|
catch (error) {
|
|
238
|
-
this.logger.log("error", `[${requestId}] Error processing request: ${error
|
|
323
|
+
this.logger.log("error", `[${requestId}] Error processing request: ${errorMessage(error)}`, error);
|
|
239
324
|
// Return error response
|
|
240
325
|
const errorResponse = {
|
|
241
326
|
status: 500,
|
|
242
327
|
body: {
|
|
243
|
-
error: error
|
|
244
|
-
code: error instanceof SchmockError
|
|
245
|
-
? error.code
|
|
246
|
-
: "INTERNAL_ERROR",
|
|
328
|
+
error: errorMessage(error),
|
|
329
|
+
code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
|
|
247
330
|
},
|
|
248
331
|
headers: {},
|
|
249
332
|
};
|
|
@@ -284,10 +367,7 @@ export class CallableMockInstance {
|
|
|
284
367
|
let headers = {};
|
|
285
368
|
let tupleFormat = false;
|
|
286
369
|
// Handle already-formed response objects (from plugin error recovery)
|
|
287
|
-
if (result
|
|
288
|
-
typeof result === "object" &&
|
|
289
|
-
"status" in result &&
|
|
290
|
-
"body" in result) {
|
|
370
|
+
if (isResponseObject(result)) {
|
|
291
371
|
return {
|
|
292
372
|
status: result.status,
|
|
293
373
|
body: result.body,
|
|
@@ -295,7 +375,7 @@ export class CallableMockInstance {
|
|
|
295
375
|
};
|
|
296
376
|
}
|
|
297
377
|
// Handle tuple response format [status, body, headers?]
|
|
298
|
-
if (
|
|
378
|
+
if (isStatusTuple(result)) {
|
|
299
379
|
[status, body, headers = {}] = result;
|
|
300
380
|
tupleFormat = true;
|
|
301
381
|
}
|
|
@@ -362,28 +442,38 @@ export class CallableMockInstance {
|
|
|
362
442
|
}
|
|
363
443
|
}
|
|
364
444
|
catch (error) {
|
|
365
|
-
this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${error
|
|
445
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
|
|
366
446
|
// Try error handling if plugin has onError hook
|
|
367
447
|
if (plugin.onError) {
|
|
368
448
|
try {
|
|
369
|
-
const
|
|
449
|
+
const pluginError = error instanceof Error ? error : new Error(errorMessage(error));
|
|
450
|
+
const errorResult = await plugin.onError(pluginError, currentContext);
|
|
370
451
|
if (errorResult) {
|
|
371
452
|
this.logger.log("pipeline", `Plugin ${plugin.name} handled error`);
|
|
372
|
-
//
|
|
453
|
+
// Error return → transform the thrown error
|
|
454
|
+
if (errorResult instanceof Error) {
|
|
455
|
+
throw new PluginError(plugin.name, errorResult);
|
|
456
|
+
}
|
|
457
|
+
// ResponseResult return → recover, stop pipeline
|
|
373
458
|
if (typeof errorResult === "object" &&
|
|
374
459
|
errorResult !== null &&
|
|
375
460
|
"status" in errorResult) {
|
|
376
|
-
// Return the error response as the current response, stop pipeline
|
|
377
461
|
response = errorResult;
|
|
378
462
|
break;
|
|
379
463
|
}
|
|
380
464
|
}
|
|
465
|
+
// void/falsy return → propagate original error below
|
|
381
466
|
}
|
|
382
467
|
catch (hookError) {
|
|
383
|
-
|
|
468
|
+
// If the hook itself threw (including our PluginError above), re-throw it
|
|
469
|
+
if (hookError instanceof PluginError) {
|
|
470
|
+
throw hookError;
|
|
471
|
+
}
|
|
472
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
|
|
384
473
|
}
|
|
385
474
|
}
|
|
386
|
-
|
|
475
|
+
const cause = error instanceof Error ? error : new Error(errorMessage(error));
|
|
476
|
+
throw new PluginError(plugin.name, cause);
|
|
387
477
|
}
|
|
388
478
|
}
|
|
389
479
|
return { context: currentContext, response };
|
|
@@ -398,15 +488,13 @@ export class CallableMockInstance {
|
|
|
398
488
|
* @private
|
|
399
489
|
*/
|
|
400
490
|
findRoute(method, path) {
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return route;
|
|
407
|
-
}
|
|
491
|
+
// O(1) lookup for static routes
|
|
492
|
+
const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
493
|
+
const staticMatch = this.staticRoutes.get(`${method} ${normalizedPath}`);
|
|
494
|
+
if (staticMatch) {
|
|
495
|
+
return staticMatch;
|
|
408
496
|
}
|
|
409
|
-
//
|
|
497
|
+
// Fall through to parameterized route scan
|
|
410
498
|
for (const route of this.routes) {
|
|
411
499
|
if (route.method === method &&
|
|
412
500
|
route.params.length > 0 &&
|
package/dist/constants.d.ts
CHANGED
|
@@ -3,4 +3,9 @@ export declare const ROUTE_NOT_FOUND_CODE: "ROUTE_NOT_FOUND";
|
|
|
3
3
|
export declare const HTTP_METHODS: readonly HttpMethod[];
|
|
4
4
|
export declare function isHttpMethod(method: string): method is HttpMethod;
|
|
5
5
|
export declare function toHttpMethod(method: string): HttpMethod;
|
|
6
|
+
/**
|
|
7
|
+
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
8
|
+
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isStatusTuple(value: unknown): value is [number, unknown] | [number, unknown, Record<string, string>];
|
|
6
11
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
|
package/dist/constants.js
CHANGED
|
@@ -18,3 +18,14 @@ export function toHttpMethod(method) {
|
|
|
18
18
|
}
|
|
19
19
|
return upper;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
23
|
+
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
24
|
+
*/
|
|
25
|
+
export function isStatusTuple(value) {
|
|
26
|
+
return (Array.isArray(value) &&
|
|
27
|
+
(value.length === 2 || value.length === 3) &&
|
|
28
|
+
typeof value[0] === "number" &&
|
|
29
|
+
value[0] >= 100 &&
|
|
30
|
+
value[0] <= 599);
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CallableMockInstance, GlobalConfig } from "./types.js";
|
|
2
1
|
/**
|
|
3
2
|
* Create a new Schmock mock instance with callable API.
|
|
4
3
|
*
|
|
@@ -22,8 +21,8 @@ import type { CallableMockInstance, GlobalConfig } from "./types.js";
|
|
|
22
21
|
* @param config Optional global configuration
|
|
23
22
|
* @returns A callable mock instance
|
|
24
23
|
*/
|
|
25
|
-
export declare function schmock(config?: GlobalConfig): CallableMockInstance;
|
|
26
|
-
export { HTTP_METHODS, isHttpMethod, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
24
|
+
export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
|
|
25
|
+
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
27
26
|
export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
|
|
28
|
-
export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, Response, ResponseBody, ResponseResult, RouteConfig, RouteKey, StaticData, } from "./types.js";
|
|
27
|
+
export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteKey, StaticData, } from "./types.js";
|
|
29
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAiC9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,QAAQ,EACR,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CallableMockInstance
|
|
1
|
+
import { CallableMockInstance } from "./builder.js";
|
|
2
2
|
/**
|
|
3
3
|
* Create a new Schmock mock instance with callable API.
|
|
4
4
|
*
|
|
@@ -24,21 +24,29 @@ import { CallableMockInstance as CallableMockInstanceImpl } from "./builder.js";
|
|
|
24
24
|
*/
|
|
25
25
|
export function schmock(config) {
|
|
26
26
|
// Always use new callable API
|
|
27
|
-
const instance = new
|
|
28
|
-
//
|
|
29
|
-
const callableInstance = ((route, generator, routeConfig = {}) => {
|
|
27
|
+
const instance = new CallableMockInstance(config || {});
|
|
28
|
+
// Callable proxy: a function with attached methods
|
|
29
|
+
const callableInstance = Object.assign((route, generator, routeConfig = {}) => {
|
|
30
30
|
instance.defineRoute(route, generator, routeConfig);
|
|
31
|
-
return callableInstance;
|
|
31
|
+
return callableInstance;
|
|
32
|
+
}, {
|
|
33
|
+
pipe: (plugin) => {
|
|
34
|
+
instance.pipe(plugin);
|
|
35
|
+
return callableInstance;
|
|
36
|
+
},
|
|
37
|
+
handle: instance.handle.bind(instance),
|
|
38
|
+
history: instance.history.bind(instance),
|
|
39
|
+
called: instance.called.bind(instance),
|
|
40
|
+
callCount: instance.callCount.bind(instance),
|
|
41
|
+
lastRequest: instance.lastRequest.bind(instance),
|
|
42
|
+
reset: instance.reset.bind(instance),
|
|
43
|
+
resetHistory: instance.resetHistory.bind(instance),
|
|
44
|
+
resetState: instance.resetState.bind(instance),
|
|
32
45
|
});
|
|
33
|
-
|
|
34
|
-
callableInstance.pipe = (plugin) => {
|
|
35
|
-
instance.pipe(plugin);
|
|
36
|
-
return callableInstance; // Return callable function for chaining
|
|
37
|
-
};
|
|
38
|
-
callableInstance.handle = instance.handle.bind(instance);
|
|
46
|
+
instance.setCallableRef(callableInstance);
|
|
39
47
|
return callableInstance;
|
|
40
48
|
}
|
|
41
49
|
// Re-export constants and utilities
|
|
42
|
-
export { HTTP_METHODS, isHttpMethod, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
50
|
+
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
43
51
|
// Re-export errors
|
|
44
52
|
export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
|
package/dist/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAuC3D"}
|
package/dist/parser.js
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
|
+
import { toHttpMethod } from "./constants.js";
|
|
1
2
|
import { RouteParseError } from "./errors.js";
|
|
2
|
-
const HTTP_METHODS = [
|
|
3
|
-
"GET",
|
|
4
|
-
"POST",
|
|
5
|
-
"PUT",
|
|
6
|
-
"DELETE",
|
|
7
|
-
"PATCH",
|
|
8
|
-
"HEAD",
|
|
9
|
-
"OPTIONS",
|
|
10
|
-
];
|
|
11
|
-
function isHttpMethod(method) {
|
|
12
|
-
return HTTP_METHODS.includes(method);
|
|
13
|
-
}
|
|
14
3
|
/**
|
|
15
4
|
* Parse 'METHOD /path' route key format
|
|
16
5
|
*
|
|
@@ -43,12 +32,8 @@ export function parseRouteKey(routeKey) {
|
|
|
43
32
|
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except :
|
|
44
33
|
.replace(/:([^/]+)/g, "([^/]+)"); // Replace :param with capture group
|
|
45
34
|
const pattern = new RegExp(`^${regexPath}$`);
|
|
46
|
-
// The regex guarantees method is valid, but we use the type guard for type safety
|
|
47
|
-
if (!isHttpMethod(method)) {
|
|
48
|
-
throw new RouteParseError(routeKey, `Invalid HTTP method: ${method}`);
|
|
49
|
-
}
|
|
50
35
|
return {
|
|
51
|
-
method,
|
|
36
|
+
method: toHttpMethod(method),
|
|
52
37
|
path,
|
|
53
38
|
pattern,
|
|
54
39
|
params,
|