@schmock/core 1.0.4 → 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.
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;IAuEP,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;IAuF/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);
@@ -103,6 +117,17 @@ export class CallableMockInstance {
103
117
  config,
104
118
  };
105
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
+ }
106
131
  this.logger.log("route", `Route defined: ${route}`, {
107
132
  contentType: config.contentType,
108
133
  generatorType: typeof generator,
@@ -120,8 +145,59 @@ export class CallableMockInstance {
120
145
  });
121
146
  return this;
122
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
+ }
123
199
  async handle(method, path, options) {
124
- const requestId = Math.random().toString(36).substring(2, 10) || "00000000";
200
+ const requestId = crypto.randomUUID();
125
201
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
126
202
  headers: options?.headers,
127
203
  query: options?.query,
@@ -131,40 +207,30 @@ export class CallableMockInstance {
131
207
  try {
132
208
  // Apply namespace if configured
133
209
  let requestPath = path;
134
- if (this.globalConfig.namespace) {
135
- // Normalize namespace to handle edge cases
136
- const namespace = this.globalConfig.namespace;
137
- if (namespace === "/") {
138
- // Root namespace means no transformation needed
139
- requestPath = path;
140
- }
141
- else {
142
- // Handle namespace without leading slash by normalizing both namespace and path
143
- const normalizedNamespace = namespace.startsWith("/")
144
- ? namespace
145
- : `/${namespace}`;
146
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
147
- // Remove trailing slash from namespace unless it's root
148
- const finalNamespace = normalizedNamespace.endsWith("/") && normalizedNamespace !== "/"
149
- ? normalizedNamespace.slice(0, -1)
150
- : normalizedNamespace;
151
- if (!normalizedPath.startsWith(finalNamespace)) {
152
- this.logger.log("route", `[${requestId}] Path doesn't match namespace ${namespace}`);
153
- const error = new RouteNotFoundError(method, path);
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
- }
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;
167
230
  }
231
+ // Remove namespace prefix, ensuring we always start with /
232
+ const stripped = pathToCheck.slice(namespace.length);
233
+ requestPath = stripped.startsWith("/") ? stripped : `/${stripped}`;
168
234
  }
169
235
  // Find matching route
170
236
  const matchedRoute = this.findRoute(method, requestPath);
@@ -193,7 +259,7 @@ export class CallableMockInstance {
193
259
  state: this.globalConfig.state || {},
194
260
  };
195
261
  let result;
196
- if (typeof matchedRoute.generator === "function") {
262
+ if (isGeneratorFunction(matchedRoute.generator)) {
197
263
  result = await matchedRoute.generator(context);
198
264
  }
199
265
  else {
@@ -218,13 +284,24 @@ export class CallableMockInstance {
218
284
  result = pipelineResult.response;
219
285
  }
220
286
  catch (error) {
221
- this.logger.log("error", `[${requestId}] Plugin pipeline error: ${error.message}`);
287
+ this.logger.log("error", `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`);
222
288
  throw error;
223
289
  }
224
290
  // Parse and prepare response
225
291
  const response = this.parseResponse(result, matchedRoute.config);
226
292
  // Apply global delay if configured
227
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
+ });
228
305
  // Log successful response
229
306
  this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
230
307
  status: response.status,
@@ -235,15 +312,13 @@ export class CallableMockInstance {
235
312
  return response;
236
313
  }
237
314
  catch (error) {
238
- this.logger.log("error", `[${requestId}] Error processing request: ${error.message}`, error);
315
+ this.logger.log("error", `[${requestId}] Error processing request: ${errorMessage(error)}`, error);
239
316
  // Return error response
240
317
  const errorResponse = {
241
318
  status: 500,
242
319
  body: {
243
- error: error.message,
244
- code: error instanceof SchmockError
245
- ? error.code
246
- : "INTERNAL_ERROR",
320
+ error: errorMessage(error),
321
+ code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
247
322
  },
248
323
  headers: {},
249
324
  };
@@ -284,10 +359,7 @@ export class CallableMockInstance {
284
359
  let headers = {};
285
360
  let tupleFormat = false;
286
361
  // Handle already-formed response objects (from plugin error recovery)
287
- if (result &&
288
- typeof result === "object" &&
289
- "status" in result &&
290
- "body" in result) {
362
+ if (isResponseObject(result)) {
291
363
  return {
292
364
  status: result.status,
293
365
  body: result.body,
@@ -362,28 +434,38 @@ export class CallableMockInstance {
362
434
  }
363
435
  }
364
436
  catch (error) {
365
- this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${error.message}`);
437
+ this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
366
438
  // Try error handling if plugin has onError hook
367
439
  if (plugin.onError) {
368
440
  try {
369
- 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);
370
443
  if (errorResult) {
371
444
  this.logger.log("pipeline", `Plugin ${plugin.name} handled error`);
372
- // If error handler returns response, use it and 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
373
450
  if (typeof errorResult === "object" &&
374
451
  errorResult !== null &&
375
452
  "status" in errorResult) {
376
- // Return the error response as the current response, stop pipeline
377
453
  response = errorResult;
378
454
  break;
379
455
  }
380
456
  }
457
+ // void/falsy return → propagate original error below
381
458
  }
382
459
  catch (hookError) {
383
- 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)}`);
384
465
  }
385
466
  }
386
- throw new PluginError(plugin.name, error);
467
+ const cause = error instanceof Error ? error : new Error(errorMessage(error));
468
+ throw new PluginError(plugin.name, cause);
387
469
  }
388
470
  }
389
471
  return { context: currentContext, response };
@@ -398,15 +480,13 @@ export class CallableMockInstance {
398
480
  * @private
399
481
  */
400
482
  findRoute(method, path) {
401
- // First pass: Look for static routes (routes without parameters)
402
- for (const route of this.routes) {
403
- if (route.method === method &&
404
- route.params.length === 0 &&
405
- route.pattern.test(path)) {
406
- return route;
407
- }
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;
408
488
  }
409
- // Second pass: Look for parameterized routes
489
+ // Fall through to parameterized route scan
410
490
  for (const route of this.routes) {
411
491
  if (route.method === method &&
412
492
  route.params.length > 0 &&
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;
24
+ export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
26
25
  export { HTTP_METHODS, isHttpMethod, 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
@@ -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,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;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,YAAY,EACZ,cAAc,EACd,WAAW,EACX,QAAQ,EACR,UAAU,GACX,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,18 +24,25 @@ 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
  }
41
48
  // Re-export constants and utilities
@@ -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,