@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 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
- 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
+ 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
@@ -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":"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 = Math.random().toString(36).substring(2, 10) || "00000000";
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
- // 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
- }
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 (typeof matchedRoute.generator === "function") {
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.message}`);
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.message}`, 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.message,
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 (Array.isArray(result) && typeof result[0] === "number") {
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.message}`);
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 errorResult = await plugin.onError(error, currentContext);
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
- // If error handler returns response, use it and stop pipeline
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
- this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${hookError.message}`);
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
- throw new PluginError(plugin.name, error);
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
- // 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
- }
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
- // Second pass: Look for parameterized routes
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 &&
@@ -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
@@ -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
@@ -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,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 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,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 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);
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";
@@ -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,