@schmock/core 1.0.3 → 1.1.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.
Files changed (39) hide show
  1. package/dist/builder.d.ts +13 -5
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +147 -60
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +20 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +3 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +20 -11
  12. package/dist/parser.d.ts.map +1 -1
  13. package/dist/parser.js +2 -17
  14. package/dist/types.d.ts +17 -210
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/types.js +1 -0
  17. package/package.json +4 -4
  18. package/src/builder.test.ts +2 -2
  19. package/src/builder.ts +232 -108
  20. package/src/constants.test.ts +59 -0
  21. package/src/constants.ts +25 -0
  22. package/src/errors.ts +3 -1
  23. package/src/index.ts +41 -29
  24. package/src/namespace.test.ts +3 -2
  25. package/src/parser.property.test.ts +495 -0
  26. package/src/parser.ts +2 -20
  27. package/src/route-matching.test.ts +1 -1
  28. package/src/steps/async-support.steps.ts +101 -91
  29. package/src/steps/basic-usage.steps.ts +49 -36
  30. package/src/steps/developer-experience.steps.ts +110 -94
  31. package/src/steps/error-handling.steps.ts +90 -66
  32. package/src/steps/fluent-api.steps.ts +75 -72
  33. package/src/steps/http-methods.steps.ts +33 -33
  34. package/src/steps/performance-reliability.steps.ts +52 -88
  35. package/src/steps/plugin-integration.steps.ts +176 -176
  36. package/src/steps/request-history.steps.ts +333 -0
  37. package/src/steps/state-concurrency.steps.ts +418 -316
  38. package/src/steps/stateful-workflows.steps.ts +138 -136
  39. package/src/types.ts +20 -259
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,21 @@ 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
- constructor(globalConfig?: GlobalConfig);
13
- defineRoute(route: RouteKey, generator: Generator, config: RouteConfig): this;
14
- pipe(plugin: Plugin): this;
15
- handle(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
12
+ private requestHistory;
13
+ constructor(globalConfig?: Schmock.GlobalConfig);
14
+ defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
15
+ pipe(plugin: Schmock.Plugin): this;
16
+ history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[];
17
+ called(method?: Schmock.HttpMethod, path?: string): boolean;
18
+ callCount(method?: Schmock.HttpMethod, path?: string): number;
19
+ lastRequest(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord | undefined;
20
+ reset(): void;
21
+ resetHistory(): void;
22
+ resetState(): void;
23
+ handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
16
24
  /**
17
25
  * Apply configured response delay
18
26
  * Supports both fixed delays and random delays within a range
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,SAAS,EAET,YAAY,EACZ,UAAU,EACV,MAAM,EAGN,cAAc,EACd,QAAQ,EACR,WAAW,EACX,QAAQ,EACT,MAAM,YAAY,CAAC;AA4CpB;;;;GAIG;AACH,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,YAAY;IAJhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAc;gBAER,YAAY,GAAE,YAAiB;IAanD,WAAW,CACT,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,WAAW,GAClB,IAAI;IA4DP,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAepB,MAAM,CACV,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC;IAiLpB;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IA0DrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAmF/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA6BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAyEA;;;;GAIG;AACH,qBAAa,oBAAoB;IAOnB,OAAO,CAAC,YAAY;IANhC,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;gBAEjC,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,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAiBlC,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,8 @@
1
1
  import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
2
2
  import { parseRouteKey } from "./parser.js";
3
+ function errorMessage(error) {
4
+ return error instanceof Error ? error.message : "Unknown error";
5
+ }
3
6
  /**
4
7
  * Debug logger that respects debug mode configuration
5
8
  */
@@ -31,6 +34,15 @@ class DebugLogger {
31
34
  console.timeEnd(`[SCHMOCK] ${label}`);
32
35
  }
33
36
  }
37
+ function isGeneratorFunction(gen) {
38
+ return typeof gen === "function";
39
+ }
40
+ function isResponseObject(value) {
41
+ return (typeof value === "object" &&
42
+ value !== null &&
43
+ "status" in value &&
44
+ "body" in value);
45
+ }
34
46
  /**
35
47
  * Callable mock instance that implements the new API.
36
48
  *
@@ -39,8 +51,10 @@ class DebugLogger {
39
51
  export class CallableMockInstance {
40
52
  globalConfig;
41
53
  routes = [];
54
+ staticRoutes = new Map();
42
55
  plugins = [];
43
56
  logger;
57
+ requestHistory = [];
44
58
  constructor(globalConfig = {}) {
45
59
  this.globalConfig = globalConfig;
46
60
  this.logger = new DebugLogger(globalConfig.debug || false);
@@ -88,6 +102,11 @@ export class CallableMockInstance {
88
102
  }
89
103
  // Parse the route key to create pattern and extract parameters
90
104
  const parsed = parseRouteKey(route);
105
+ // Check for duplicate routes
106
+ const existing = this.routes.find((r) => r.method === parsed.method && r.path === parsed.path);
107
+ if (existing) {
108
+ this.logger.log("warning", `Duplicate route: ${route} — first registration wins`);
109
+ }
91
110
  // Compile the route
92
111
  const compiledRoute = {
93
112
  pattern: parsed.pattern,
@@ -98,6 +117,17 @@ export class CallableMockInstance {
98
117
  config,
99
118
  };
100
119
  this.routes.push(compiledRoute);
120
+ // Store static routes (no params) in Map for O(1) lookup
121
+ // Only store the first registration — "first registration wins" semantics
122
+ if (parsed.params.length === 0) {
123
+ const normalizedPath = parsed.path.endsWith("/") && parsed.path !== "/"
124
+ ? parsed.path.slice(0, -1)
125
+ : parsed.path;
126
+ const key = `${parsed.method} ${normalizedPath}`;
127
+ if (!this.staticRoutes.has(key)) {
128
+ this.staticRoutes.set(key, compiledRoute);
129
+ }
130
+ }
101
131
  this.logger.log("route", `Route defined: ${route}`, {
102
132
  contentType: config.contentType,
103
133
  generatorType: typeof generator,
@@ -115,8 +145,59 @@ export class CallableMockInstance {
115
145
  });
116
146
  return this;
117
147
  }
148
+ // ===== Request Spy / History API =====
149
+ history(method, path) {
150
+ if (method && path) {
151
+ return this.requestHistory.filter((r) => r.method === method && r.path === path);
152
+ }
153
+ return [...this.requestHistory];
154
+ }
155
+ called(method, path) {
156
+ if (method && path) {
157
+ return this.requestHistory.some((r) => r.method === method && r.path === path);
158
+ }
159
+ return this.requestHistory.length > 0;
160
+ }
161
+ callCount(method, path) {
162
+ if (method && path) {
163
+ return this.requestHistory.filter((r) => r.method === method && r.path === path).length;
164
+ }
165
+ return this.requestHistory.length;
166
+ }
167
+ lastRequest(method, path) {
168
+ if (method && path) {
169
+ const filtered = this.requestHistory.filter((r) => r.method === method && r.path === path);
170
+ return filtered[filtered.length - 1];
171
+ }
172
+ return this.requestHistory[this.requestHistory.length - 1];
173
+ }
174
+ // ===== Reset / Lifecycle =====
175
+ reset() {
176
+ this.routes = [];
177
+ this.staticRoutes.clear();
178
+ this.plugins = [];
179
+ this.requestHistory = [];
180
+ if (this.globalConfig.state) {
181
+ for (const key of Object.keys(this.globalConfig.state)) {
182
+ delete this.globalConfig.state[key];
183
+ }
184
+ }
185
+ this.logger.log("lifecycle", "Mock fully reset");
186
+ }
187
+ resetHistory() {
188
+ this.requestHistory = [];
189
+ this.logger.log("lifecycle", "Request history cleared");
190
+ }
191
+ resetState() {
192
+ if (this.globalConfig.state) {
193
+ for (const key of Object.keys(this.globalConfig.state)) {
194
+ delete this.globalConfig.state[key];
195
+ }
196
+ }
197
+ this.logger.log("lifecycle", "State cleared");
198
+ }
118
199
  async handle(method, path, options) {
119
- const requestId = Math.random().toString(36).substring(7);
200
+ const requestId = crypto.randomUUID();
120
201
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
121
202
  headers: options?.headers,
122
203
  query: options?.query,
@@ -126,40 +207,30 @@ export class CallableMockInstance {
126
207
  try {
127
208
  // Apply namespace if configured
128
209
  let requestPath = path;
129
- if (this.globalConfig.namespace) {
130
- // Normalize namespace to handle edge cases
131
- const namespace = this.globalConfig.namespace;
132
- if (namespace === "/") {
133
- // Root namespace means no transformation needed
134
- requestPath = path;
135
- }
136
- else {
137
- // Handle namespace without leading slash by normalizing both namespace and path
138
- const normalizedNamespace = namespace.startsWith("/")
139
- ? namespace
140
- : `/${namespace}`;
141
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
142
- // Remove trailing slash from namespace unless it's root
143
- const finalNamespace = normalizedNamespace.endsWith("/") && normalizedNamespace !== "/"
144
- ? normalizedNamespace.slice(0, -1)
145
- : normalizedNamespace;
146
- if (!normalizedPath.startsWith(finalNamespace)) {
147
- this.logger.log("route", `[${requestId}] Path doesn't match namespace ${namespace}`);
148
- const error = new RouteNotFoundError(method, path);
149
- const response = {
150
- status: 404,
151
- body: { error: error.message, code: error.code },
152
- headers: {},
153
- };
154
- this.logger.timeEnd(`request-${requestId}`);
155
- return response;
156
- }
157
- // Remove namespace prefix, ensuring we always start with /
158
- requestPath = normalizedPath.substring(finalNamespace.length);
159
- if (!requestPath.startsWith("/")) {
160
- requestPath = `/${requestPath}`;
161
- }
210
+ if (this.globalConfig.namespace && this.globalConfig.namespace !== "/") {
211
+ const namespace = this.globalConfig.namespace.startsWith("/")
212
+ ? this.globalConfig.namespace
213
+ : `/${this.globalConfig.namespace}`;
214
+ const pathToCheck = path.startsWith("/") ? path : `/${path}`;
215
+ // Check if path starts with namespace
216
+ // handle both "/api/users" (starts with /api) and "/api" (exact match)
217
+ // but NOT "/apiv2" (prefix match but wrong segment)
218
+ const isMatch = pathToCheck === namespace ||
219
+ pathToCheck.startsWith(namespace.endsWith("/") ? namespace : `${namespace}/`);
220
+ if (!isMatch) {
221
+ this.logger.log("route", `[${requestId}] Path doesn't match namespace ${namespace}`);
222
+ const error = new RouteNotFoundError(method, path);
223
+ const response = {
224
+ status: 404,
225
+ body: { error: error.message, code: error.code },
226
+ headers: {},
227
+ };
228
+ this.logger.timeEnd(`request-${requestId}`);
229
+ return response;
162
230
  }
231
+ // Remove namespace prefix, ensuring we always start with /
232
+ const stripped = pathToCheck.slice(namespace.length);
233
+ requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
163
234
  }
164
235
  // Find matching route
165
236
  const matchedRoute = this.findRoute(method, requestPath);
@@ -188,7 +259,7 @@ export class CallableMockInstance {
188
259
  state: this.globalConfig.state || {},
189
260
  };
190
261
  let result;
191
- if (typeof matchedRoute.generator === "function") {
262
+ if (isGeneratorFunction(matchedRoute.generator)) {
192
263
  result = await matchedRoute.generator(context);
193
264
  }
194
265
  else {
@@ -213,13 +284,24 @@ export class CallableMockInstance {
213
284
  result = pipelineResult.response;
214
285
  }
215
286
  catch (error) {
216
- this.logger.log("error", `[${requestId}] Plugin pipeline error: ${error.message}`);
287
+ this.logger.log("error", `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`);
217
288
  throw error;
218
289
  }
219
290
  // Parse and prepare response
220
291
  const response = this.parseResponse(result, matchedRoute.config);
221
292
  // Apply global delay if configured
222
293
  await this.applyDelay();
294
+ // Record request in history
295
+ this.requestHistory.push({
296
+ method,
297
+ path: requestPath,
298
+ params,
299
+ query: options?.query || {},
300
+ headers: options?.headers || {},
301
+ body: options?.body,
302
+ timestamp: Date.now(),
303
+ response: { status: response.status, body: response.body },
304
+ });
223
305
  // Log successful response
224
306
  this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
225
307
  status: response.status,
@@ -230,15 +312,13 @@ export class CallableMockInstance {
230
312
  return response;
231
313
  }
232
314
  catch (error) {
233
- this.logger.log("error", `[${requestId}] Error processing request: ${error.message}`, error);
315
+ this.logger.log("error", `[${requestId}] Error processing request: ${errorMessage(error)}`, error);
234
316
  // Return error response
235
317
  const errorResponse = {
236
318
  status: 500,
237
319
  body: {
238
- error: error.message,
239
- code: error instanceof SchmockError
240
- ? error.code
241
- : "INTERNAL_ERROR",
320
+ error: errorMessage(error),
321
+ code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
242
322
  },
243
323
  headers: {},
244
324
  };
@@ -279,10 +359,7 @@ export class CallableMockInstance {
279
359
  let headers = {};
280
360
  let tupleFormat = false;
281
361
  // Handle already-formed response objects (from plugin error recovery)
282
- if (result &&
283
- typeof result === "object" &&
284
- "status" in result &&
285
- "body" in result) {
362
+ if (isResponseObject(result)) {
286
363
  return {
287
364
  status: result.status,
288
365
  body: result.body,
@@ -357,26 +434,38 @@ export class CallableMockInstance {
357
434
  }
358
435
  }
359
436
  catch (error) {
360
- this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${error.message}`);
437
+ this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
361
438
  // Try error handling if plugin has onError hook
362
439
  if (plugin.onError) {
363
440
  try {
364
- const errorResult = await plugin.onError(error, currentContext);
441
+ const pluginError = error instanceof Error ? error : new Error(errorMessage(error));
442
+ const errorResult = await plugin.onError(pluginError, currentContext);
365
443
  if (errorResult) {
366
444
  this.logger.log("pipeline", `Plugin ${plugin.name} handled error`);
367
- // If error handler returns response, use it and stop pipeline
368
- if (typeof errorResult === "object" && errorResult.status) {
369
- // Return the error response as the current response, stop pipeline
445
+ // Error return transform the thrown error
446
+ if (errorResult instanceof Error) {
447
+ throw new PluginError(plugin.name, errorResult);
448
+ }
449
+ // ResponseResult return → recover, stop pipeline
450
+ if (typeof errorResult === "object" &&
451
+ errorResult !== null &&
452
+ "status" in errorResult) {
370
453
  response = errorResult;
371
454
  break;
372
455
  }
373
456
  }
457
+ // void/falsy return → propagate original error below
374
458
  }
375
459
  catch (hookError) {
376
- this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${hookError.message}`);
460
+ // If the hook itself threw (including our PluginError above), re-throw it
461
+ if (hookError instanceof PluginError) {
462
+ throw hookError;
463
+ }
464
+ this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
377
465
  }
378
466
  }
379
- throw new PluginError(plugin.name, error);
467
+ const cause = error instanceof Error ? error : new Error(errorMessage(error));
468
+ throw new PluginError(plugin.name, cause);
380
469
  }
381
470
  }
382
471
  return { context: currentContext, response };
@@ -391,15 +480,13 @@ export class CallableMockInstance {
391
480
  * @private
392
481
  */
393
482
  findRoute(method, path) {
394
- // First pass: Look for static routes (routes without parameters)
395
- for (const route of this.routes) {
396
- if (route.method === method &&
397
- route.params.length === 0 &&
398
- route.pattern.test(path)) {
399
- return route;
400
- }
483
+ // O(1) lookup for static routes
484
+ const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
485
+ const staticMatch = this.staticRoutes.get(`${method} ${normalizedPath}`);
486
+ if (staticMatch) {
487
+ return staticMatch;
401
488
  }
402
- // Second pass: Look for parameterized routes
489
+ // Fall through to parameterized route scan
403
490
  for (const route of this.routes) {
404
491
  if (route.method === method &&
405
492
  route.params.length > 0 &&
@@ -0,0 +1,6 @@
1
+ import type { HttpMethod } from "./types.js";
2
+ export declare const ROUTE_NOT_FOUND_CODE: "ROUTE_NOT_FOUND";
3
+ export declare const HTTP_METHODS: readonly HttpMethod[];
4
+ export declare function isHttpMethod(method: string): method is HttpMethod;
5
+ export declare function toHttpMethod(method: string): HttpMethod;
6
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,20 @@
1
+ export const ROUTE_NOT_FOUND_CODE = "ROUTE_NOT_FOUND";
2
+ export const HTTP_METHODS = [
3
+ "GET",
4
+ "POST",
5
+ "PUT",
6
+ "DELETE",
7
+ "PATCH",
8
+ "HEAD",
9
+ "OPTIONS",
10
+ ];
11
+ export function isHttpMethod(method) {
12
+ return HTTP_METHODS.includes(method);
13
+ }
14
+ export function toHttpMethod(method) {
15
+ const upper = method.toUpperCase();
16
+ if (!isHttpMethod(upper)) {
17
+ throw new Error(`Invalid HTTP method: "${method}"`);
18
+ }
19
+ return upper;
20
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAMpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAQpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
package/dist/errors.js CHANGED
@@ -9,7 +9,9 @@ export class SchmockError extends Error {
9
9
  this.code = code;
10
10
  this.context = context;
11
11
  this.name = "SchmockError";
12
- Error.captureStackTrace(this, this.constructor);
12
+ if (typeof Error.captureStackTrace === "function") {
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
13
15
  }
14
16
  }
15
17
  /**
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,7 +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;
24
+ export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
+ export { HTTP_METHODS, isHttpMethod, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
26
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
27
- export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, Response, ResponseResult, RouteConfig, RouteKey, } 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";
28
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EAEpB,YAAY,EAIb,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,oBAAoB,CAsBnE;AAGD,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,GACT,MAAM,YAAY,CAAC"}
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,CA+B9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,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 as CallableMockInstanceImpl } from "./builder.js";
1
+ import { CallableMockInstance } from "./builder.js";
2
2
  /**
3
3
  * Create a new Schmock mock instance with callable API.
4
4
  *
@@ -24,19 +24,28 @@ 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 CallableMockInstanceImpl(config || {});
28
- // Create a callable function that wraps the instance
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; // Return the callable function for chaining
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
- // Manually bind all instance methods to the callable function with proper return values
34
- callableInstance.pipe = (plugin) => {
35
- instance.pipe(plugin);
36
- return callableInstance; // Return callable function for chaining
37
- };
38
- callableInstance.handle = instance.handle.bind(instance);
39
46
  return callableInstance;
40
47
  }
48
+ // Re-export constants and utilities
49
+ export { HTTP_METHODS, isHttpMethod, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
41
50
  // Re-export errors
42
51
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgB7C,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,CA4C3D"}
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,