@schmock/core 1.0.0 → 1.0.2

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,3 +1,4 @@
1
+ import type { Generator, GlobalConfig, HttpMethod, Plugin, RequestOptions, Response, RouteConfig, RouteKey } from "./types.js";
1
2
  /**
2
3
  * Callable mock instance that implements the new API.
3
4
  *
@@ -8,10 +9,10 @@ export declare class CallableMockInstance {
8
9
  private routes;
9
10
  private plugins;
10
11
  private logger;
11
- constructor(globalConfig?: Schmock.GlobalConfig);
12
- defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
13
- pipe(plugin: Schmock.Plugin): this;
14
- handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
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>;
15
16
  /**
16
17
  * Apply configured response delay
17
18
  * 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":"AAkDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,YAAY;IAJhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;gBAER,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;IA4DP,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAe5B,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;IAmL5B;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IA6DrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAmF/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA+BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
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;IA+BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
package/dist/builder.js CHANGED
@@ -1,5 +1,5 @@
1
- import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors";
2
- import { parseRouteKey } from "./parser";
1
+ import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
2
+ import { parseRouteKey } from "./parser.js";
3
3
  /**
4
4
  * Debug logger that respects debug mode configuration
5
5
  */
package/dist/errors.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  */
4
4
  export declare class SchmockError extends Error {
5
5
  readonly code: string;
6
- readonly context?: unknown;
7
- constructor(message: string, code: string, context?: unknown);
6
+ readonly context?: unknown | undefined;
7
+ constructor(message: string, code: string, context?: unknown | undefined);
8
8
  }
9
9
  /**
10
10
  * Error thrown when a route is not found
@@ -54,3 +54,4 @@ export declare class SchemaGenerationError extends SchmockError {
54
54
  export declare class ResourceLimitError extends SchmockError {
55
55
  constructor(resource: string, limit: number, actual?: number);
56
56
  }
57
+ //# sourceMappingURL=errors.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { CallableMockInstance, GlobalConfig } from "./types.js";
1
2
  /**
2
3
  * Create a new Schmock mock instance with callable API.
3
4
  *
@@ -21,7 +22,7 @@
21
22
  * @param config Optional global configuration
22
23
  * @returns A callable mock instance
23
24
  */
24
- export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
- export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors";
26
- export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, Response, ResponseResult, RouteConfig, RouteKey, } from "./types";
25
+ export declare function schmock(config?: GlobalConfig): CallableMockInstance;
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
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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,CAsB9B;AAGD,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,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,SAAS,CAAC"}
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"}
package/dist/index.js CHANGED
@@ -1 +1,42 @@
1
- class X extends Error{code;context;constructor(A,B,G){super(A);this.code=B;this.context=G;this.name="SchmockError",Error.captureStackTrace(this,this.constructor)}}class _ extends X{constructor(A,B){super(`Route not found: ${A} ${B}`,"ROUTE_NOT_FOUND",{method:A,path:B});this.name="RouteNotFoundError"}}class $ extends X{constructor(A,B){super(`Invalid route key format: "${A}". ${B}`,"ROUTE_PARSE_ERROR",{routeKey:A,reason:B});this.name="RouteParseError"}}class z extends X{constructor(A,B){super(`Failed to generate response for route ${A}: ${B.message}`,"RESPONSE_GENERATION_ERROR",{route:A,originalError:B});this.name="ResponseGenerationError"}}class j extends X{constructor(A,B){super(`Plugin "${A}" failed: ${B.message}`,"PLUGIN_ERROR",{pluginName:A,originalError:B});this.name="PluginError"}}class O extends X{constructor(A,B){super(`Invalid route definition for "${A}": ${B}`,"ROUTE_DEFINITION_ERROR",{routeKey:A,reason:B});this.name="RouteDefinitionError"}}class M extends X{constructor(A,B,G){super(`Schema validation failed at ${A}: ${B}${G?`. ${G}`:""}`,"SCHEMA_VALIDATION_ERROR",{schemaPath:A,issue:B,suggestion:G});this.name="SchemaValidationError"}}class N extends X{constructor(A,B,G){super(`Schema generation failed for route ${A}: ${B.message}`,"SCHEMA_GENERATION_ERROR",{route:A,originalError:B,schema:G});this.name="SchemaGenerationError"}}class K extends X{constructor(A,B,G){super(`Resource limit exceeded for ${A}: limit=${B}${G?`, actual=${G}`:""}`,"RESOURCE_LIMIT_ERROR",{resource:A,limit:B,actual:G});this.name="ResourceLimitError"}}function S(A){let B=A.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (.+)$/);if(!B)throw new $(A,'Expected format: "METHOD /path" (e.g., "GET /users")');let[,G,H]=B,J=[],Q=/:([^/]+)/g,T;T=Q.exec(H);while(T!==null)J.push(T[1]),T=Q.exec(H);let U=H.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/:([^/]+)/g,"([^/]+)"),V=new RegExp(`^${U}$`);return{method:G,path:H,pattern:V,params:J}}class k{enabled;constructor(A=!1){this.enabled=A}log(A,B,G){if(!this.enabled)return;let J=`[${new Date().toISOString()}] [SCHMOCK:${A.toUpperCase()}]`;if(G)console.log(`${J} ${B}`,G);else console.log(`${J} ${B}`)}time(A){if(!this.enabled)return;console.time(`[SCHMOCK] ${A}`)}timeEnd(A){if(!this.enabled)return;console.timeEnd(`[SCHMOCK] ${A}`)}}class L{globalConfig;routes=[];plugins=[];logger;constructor(A={}){this.globalConfig=A;if(this.logger=new k(A.debug||!1),A.debug)this.logger.log("config","Debug mode enabled");this.logger.log("config","Callable mock instance created",{debug:A.debug,namespace:A.namespace,delay:A.delay})}defineRoute(A,B,G){if(!G.contentType)if(typeof B==="function")G.contentType="application/json";else if(typeof B==="string"||typeof B==="number"||typeof B==="boolean")G.contentType="text/plain";else if(Buffer.isBuffer(B))G.contentType="application/octet-stream";else G.contentType="application/json";if(typeof B!=="function"&&G.contentType==="application/json")try{JSON.stringify(B)}catch(Q){throw new O(A,"Generator data is not valid JSON but contentType is application/json")}let H=S(A),J={pattern:H.pattern,params:H.params,method:H.method,path:H.path,generator:B,config:G};return this.routes.push(J),this.logger.log("route",`Route defined: ${A}`,{contentType:G.contentType,generatorType:typeof B,hasParams:H.params.length>0}),this}pipe(A){return this.plugins.push(A),this.logger.log("plugin",`Registered plugin: ${A.name}@${A.version||"unknown"}`,{name:A.name,version:A.version,hasProcess:typeof A.process==="function",hasOnError:typeof A.onError==="function"}),this}async handle(A,B,G){let H=Math.random().toString(36).substring(7);this.logger.log("request",`[${H}] ${A} ${B}`,{headers:G?.headers,query:G?.query,bodyType:G?.body?typeof G.body:"none"}),this.logger.time(`request-${H}`);try{let J=B;if(this.globalConfig.namespace){let W=this.globalConfig.namespace;if(W==="/")J=B;else{let Y=W.startsWith("/")?W:`/${W}`,w=B.startsWith("/")?B:`/${B}`,F=Y.endsWith("/")&&Y!=="/"?Y.slice(0,-1):Y;if(!w.startsWith(F)){this.logger.log("route",`[${H}] Path doesn't match namespace ${W}`);let v=new _(A,B),I={status:404,body:{error:v.message,code:v.code},headers:{}};return this.logger.timeEnd(`request-${H}`),I}if(J=w.substring(F.length),!J.startsWith("/"))J=`/${J}`}}let Q=this.findRoute(A,J);if(!Q){this.logger.log("route",`[${H}] No route found for ${A} ${J}`);let W=new _(A,B),Y={status:404,body:{error:W.message,code:W.code},headers:{}};return this.logger.timeEnd(`request-${H}`),Y}this.logger.log("route",`[${H}] Matched route: ${A} ${Q.path}`);let T=this.extractParams(Q,J),U={method:A,path:J,params:T,query:G?.query||{},headers:G?.headers||{},body:G?.body,state:this.globalConfig.state||{}},V;if(typeof Q.generator==="function")V=await Q.generator(U);else V=Q.generator;let D={path:J,route:Q.config,method:A,params:T,query:G?.query||{},headers:G?.headers||{},body:G?.body,state:new Map,routeState:this.globalConfig.state||{}};try{let W=await this.runPluginPipeline(D,V,Q.config,H);D=W.context,V=W.response}catch(W){throw this.logger.log("error",`[${H}] Plugin pipeline error: ${W.message}`),W}let Z=this.parseResponse(V,Q.config);return await this.applyDelay(),this.logger.log("response",`[${H}] Sending response ${Z.status}`,{status:Z.status,headers:Z.headers,bodyType:typeof Z.body}),this.logger.timeEnd(`request-${H}`),Z}catch(J){this.logger.log("error",`[${H}] Error processing request: ${J.message}`,J);let Q={status:500,body:{error:J.message,code:J instanceof X?J.code:"INTERNAL_ERROR"},headers:{}};return await this.applyDelay(),this.logger.log("error",`[${H}] Returning error response 500`),this.logger.timeEnd(`request-${H}`),Q}}async applyDelay(){if(!this.globalConfig.delay)return;let A=Array.isArray(this.globalConfig.delay)?Math.random()*(this.globalConfig.delay[1]-this.globalConfig.delay[0])+this.globalConfig.delay[0]:this.globalConfig.delay;await new Promise((B)=>setTimeout(B,A))}parseResponse(A,B){let G=200,H=A,J={},Q=!1;if(A&&typeof A==="object"&&"status"in A&&"body"in A)return{status:A.status,body:A.body,headers:A.headers||{}};if(Array.isArray(A)&&typeof A[0]==="number")[G,H,J={}]=A,Q=!0;if(H===null||H===void 0){if(!Q)G=G===200?204:G;H=void 0}if(!J["content-type"]&&B.contentType&&!Q){if(J["content-type"]=B.contentType,B.contentType==="text/plain"&&H!==void 0){if(typeof H==="object"&&!Buffer.isBuffer(H))H=JSON.stringify(H);else if(typeof H!=="string")H=String(H)}}return{status:G,body:H,headers:J}}async runPluginPipeline(A,B,G,H){let J=A,Q=B;this.logger.log("pipeline",`Running plugin pipeline for ${this.plugins.length} plugins`);for(let T of this.plugins){this.logger.log("pipeline",`Processing plugin: ${T.name}`);try{let U=await T.process(J,Q);if(!U||!U.context)throw Error(`Plugin ${T.name} didn't return valid result`);if(J=U.context,U.response!==void 0&&(Q===void 0||Q===null))this.logger.log("pipeline",`Plugin ${T.name} generated response`),Q=U.response;else if(U.response!==void 0&&Q!==void 0)this.logger.log("pipeline",`Plugin ${T.name} transformed response`),Q=U.response}catch(U){if(this.logger.log("pipeline",`Plugin ${T.name} failed: ${U.message}`),T.onError)try{let V=await T.onError(U,J);if(V){if(this.logger.log("pipeline",`Plugin ${T.name} handled error`),typeof V==="object"&&V.status){Q=V;break}}}catch(V){this.logger.log("pipeline",`Plugin ${T.name} error handler failed: ${V.message}`)}throw new j(T.name,U)}}return{context:J,response:Q}}findRoute(A,B){for(let G=this.routes.length-1;G>=0;G--){let H=this.routes[G];if(H.method===A&&H.params.length===0&&H.pattern.test(B))return H}for(let G=this.routes.length-1;G>=0;G--){let H=this.routes[G];if(H.method===A&&H.params.length>0&&H.pattern.test(B))return H}return}extractParams(A,B){let G=B.match(A.pattern);if(!G)return{};let H={};return A.params.forEach((J,Q)=>{H[J]=G[Q+1]}),H}}function b(A){let B=new L(A||{}),G=(H,J,Q={})=>{return B.defineRoute(H,J,Q),G};return G.pipe=(H)=>{return B.pipe(H),G},G.handle=B.handle.bind(B),G}export{b as schmock,X as SchmockError,M as SchemaValidationError,N as SchemaGenerationError,$ as RouteParseError,_ as RouteNotFoundError,O as RouteDefinitionError,z as ResponseGenerationError,K as ResourceLimitError,j as PluginError};
1
+ import { CallableMockInstance as CallableMockInstanceImpl } from "./builder.js";
2
+ /**
3
+ * Create a new Schmock mock instance with callable API.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // New callable API (default)
8
+ * const mock = schmock({ debug: true })
9
+ * mock('GET /users', () => [{ id: 1, name: 'John' }])
10
+ * .pipe(authPlugin())
11
+ *
12
+ * const response = await mock.handle('GET', '/users')
13
+ * ```
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Simple usage with defaults
18
+ * const mock = schmock()
19
+ * mock('GET /users', [{ id: 1, name: 'John' }])
20
+ * ```
21
+ *
22
+ * @param config Optional global configuration
23
+ * @returns A callable mock instance
24
+ */
25
+ export function schmock(config) {
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 = {}) => {
30
+ instance.defineRoute(route, generator, routeConfig);
31
+ return callableInstance; // Return the callable function for chaining
32
+ });
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
+ return callableInstance;
40
+ }
41
+ // Re-export errors
42
+ export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
package/dist/parser.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { HttpMethod } from "./types";
1
+ import type { HttpMethod } from "./types.js";
2
2
  export interface ParsedRoute {
3
3
  method: HttpMethod;
4
4
  path: string;
@@ -17,3 +17,4 @@ export interface ParsedRoute {
17
17
  * // => { method: 'GET', path: '/users/:id', pattern: /^\/users\/([^/]+)$/, params: ['id'] }
18
18
  */
19
19
  export declare function parseRouteKey(routeKey: string): ParsedRoute;
20
+ //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,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"}
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"}
package/dist/parser.js CHANGED
@@ -1,4 +1,16 @@
1
- import { RouteParseError } from "./errors";
1
+ 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
+ }
2
14
  /**
3
15
  * Parse 'METHOD /path' route key format
4
16
  *
@@ -31,8 +43,12 @@ export function parseRouteKey(routeKey) {
31
43
  .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except :
32
44
  .replace(/:([^/]+)/g, "([^/]+)"); // Replace :param with capture group
33
45
  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
+ }
34
50
  return {
35
- method: method,
51
+ method,
36
52
  path,
37
53
  pattern,
38
54
  params,
package/dist/types.d.ts CHANGED
@@ -1,15 +1,211 @@
1
- export type HttpMethod = Schmock.HttpMethod;
2
- export type RouteKey = Schmock.RouteKey;
3
- export type ResponseResult = Schmock.ResponseResult;
4
- export type RequestContext = Schmock.RequestContext;
5
- export type Response = Schmock.Response;
6
- export type RequestOptions = Schmock.RequestOptions;
7
- export type GlobalConfig = Schmock.GlobalConfig;
8
- export type RouteConfig = Schmock.RouteConfig;
9
- export type Generator = Schmock.Generator;
10
- export type GeneratorFunction = Schmock.GeneratorFunction;
11
- export type CallableMockInstance = Schmock.CallableMockInstance;
12
- export type Plugin = Schmock.Plugin;
13
- export type PluginContext = Schmock.PluginContext;
14
- export type PluginResult = Schmock.PluginResult;
1
+ /**
2
+ * JSON Schema type (simplified for core package)
3
+ * Full schema support available via @schmock/schema
4
+ */
5
+ export interface JSONSchema {
6
+ type?: string | string[];
7
+ properties?: Record<string, JSONSchema>;
8
+ items?: JSONSchema | JSONSchema[];
9
+ required?: string[];
10
+ enum?: any[];
11
+ const?: any;
12
+ [key: string]: any;
13
+ }
14
+ /**
15
+ * HTTP methods supported by Schmock
16
+ */
17
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
18
+ /**
19
+ * Route key format: 'METHOD /path'
20
+ *
21
+ * @example
22
+ * 'GET /users'
23
+ * 'POST /users/:id'
24
+ * 'DELETE /api/posts/:postId/comments/:commentId'
25
+ */
26
+ export type RouteKey = `${HttpMethod} ${string}`;
27
+ /**
28
+ * Plugin interface for extending Schmock functionality
29
+ */
30
+ export interface Plugin {
31
+ /** Unique plugin identifier */
32
+ name: string;
33
+ /** Plugin version (semver) */
34
+ version?: string;
35
+ /**
36
+ * Process the request through this plugin
37
+ * First plugin to set response becomes the generator, others transform
38
+ * @param context - Plugin context with request details
39
+ * @param response - Response from previous plugin (if any)
40
+ * @returns Updated context and response
41
+ */
42
+ process(context: PluginContext, response?: any): PluginResult | Promise<PluginResult>;
43
+ /**
44
+ * Called when an error occurs
45
+ * Can handle, transform, or suppress errors
46
+ * @param error - The error that occurred
47
+ * @param context - Plugin context
48
+ * @returns Modified error, response data, or void to continue error propagation
49
+ */
50
+ onError?(error: Error, context: PluginContext): Error | ResponseResult | undefined | Promise<Error | ResponseResult | undefined>;
51
+ }
52
+ /**
53
+ * Result returned by plugin process method
54
+ */
55
+ export interface PluginResult {
56
+ /** Updated context */
57
+ context: PluginContext;
58
+ /** Response data (if generated/modified) */
59
+ response?: any;
60
+ }
61
+ /**
62
+ * Context passed through plugin pipeline
63
+ */
64
+ export interface PluginContext {
65
+ /** Request path */
66
+ path: string;
67
+ /** Matched route configuration */
68
+ route: any;
69
+ /** HTTP method */
70
+ method: HttpMethod;
71
+ /** Route parameters */
72
+ params: Record<string, string>;
73
+ /** Query parameters */
74
+ query: Record<string, string>;
75
+ /** Request headers */
76
+ headers: Record<string, string>;
77
+ /** Request body */
78
+ body?: any;
79
+ /** Shared state between plugins for this request */
80
+ state: Map<string, any>;
81
+ /** Route-specific state */
82
+ routeState?: any;
83
+ }
84
+ /**
85
+ * Global configuration options for the mock instance
86
+ */
87
+ export interface GlobalConfig {
88
+ /** Base path prefix for all routes */
89
+ namespace?: string;
90
+ /** Response delay in ms, or [min, max] for random delay */
91
+ delay?: number | [number, number];
92
+ /** Enable debug mode for detailed logging */
93
+ debug?: boolean;
94
+ /** Initial shared state object */
95
+ state?: any;
96
+ }
97
+ /**
98
+ * Route-specific configuration options
99
+ */
100
+ export interface RouteConfig {
101
+ /** MIME type for content type validation (auto-detected if not provided) */
102
+ contentType?: string;
103
+ /** Additional route-specific options */
104
+ [key: string]: any;
105
+ }
106
+ /**
107
+ * Generator types that can be passed to route definitions
108
+ */
109
+ export type Generator = GeneratorFunction | StaticData | JSONSchema;
110
+ /**
111
+ * Function that generates responses
112
+ */
113
+ export type GeneratorFunction = (context: RequestContext) => ResponseResult | Promise<ResponseResult>;
114
+ /**
115
+ * Static data (non-function) that gets returned as-is
116
+ */
117
+ export type StaticData = any;
118
+ /**
119
+ * Context passed to generator functions
120
+ */
121
+ export interface RequestContext {
122
+ /** HTTP method */
123
+ method: HttpMethod;
124
+ /** Request path */
125
+ path: string;
126
+ /** Route parameters (e.g., :id) */
127
+ params: Record<string, string>;
128
+ /** Query string parameters */
129
+ query: Record<string, string>;
130
+ /** Request headers */
131
+ headers: Record<string, string>;
132
+ /** Request body (for POST, PUT, PATCH) */
133
+ body?: any;
134
+ /** Shared mutable state */
135
+ state: any;
136
+ }
137
+ /**
138
+ * Response result types:
139
+ * - Any value: returns as 200 OK
140
+ * - [status, body]: custom status with body
141
+ * - [status, body, headers]: custom status, body, and headers
142
+ */
143
+ export type ResponseResult = any | [number, any] | [number, any, Record<string, string>];
144
+ /**
145
+ * Response object returned by handle method
146
+ */
147
+ export interface Response {
148
+ status: number;
149
+ body: any;
150
+ headers: Record<string, string>;
151
+ }
152
+ /**
153
+ * Options for handle method
154
+ */
155
+ export interface RequestOptions {
156
+ headers?: Record<string, string>;
157
+ body?: any;
158
+ query?: Record<string, string>;
159
+ }
160
+ /**
161
+ * Main callable mock instance interface
162
+ */
163
+ export interface CallableMockInstance {
164
+ /**
165
+ * Define a route by calling the instance directly
166
+ *
167
+ * @param route - Route pattern in format 'METHOD /path'
168
+ * @param generator - Response generator (function, static data, or schema)
169
+ * @param config - Route-specific configuration
170
+ * @returns The same instance for method chaining
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const mock = schmock()
175
+ * mock('GET /users', () => [...users], { contentType: 'application/json' })
176
+ * mock('POST /users', userData, { contentType: 'application/json' })
177
+ * ```
178
+ */
179
+ (route: RouteKey, generator: Generator, config?: RouteConfig): CallableMockInstance;
180
+ /**
181
+ * Add a plugin to the pipeline
182
+ *
183
+ * @param plugin - Plugin to add to the pipeline
184
+ * @returns The same instance for method chaining
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * mock('GET /users', generator, config)
189
+ * .pipe(authPlugin())
190
+ * .pipe(corsPlugin())
191
+ * ```
192
+ */
193
+ pipe(plugin: Plugin): CallableMockInstance;
194
+ /**
195
+ * Handle a request and return a response
196
+ *
197
+ * @param method - HTTP method
198
+ * @param path - Request path
199
+ * @param options - Request options (headers, body, query)
200
+ * @returns Promise resolving to response object
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const response = await mock.handle('GET', '/users', {
205
+ * headers: { 'Authorization': 'Bearer token' }
206
+ * })
207
+ * ```
208
+ */
209
+ handle(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
210
+ }
15
211
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
2
1
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -18,16 +18,16 @@
18
18
  "./package.json": "./package.json"
19
19
  },
20
20
  "scripts": {
21
- "build": "bun build:lib && bun build:types",
22
- "build:lib": "bun build --minify --outdir=dist src/index.ts",
23
- "build:types": "tsc -p tsconfig.json",
21
+ "build": "rm -rf dist tsconfig.tsbuildinfo && tsc --build",
22
+ "build:types": "rm -f tsconfig.tsbuildinfo && tsc --build",
24
23
  "pretest": "rm -f src/*.js src/*.d.ts || true",
25
24
  "test": "vitest",
26
25
  "test:watch": "vitest --watch",
27
26
  "pretest:bdd": "rm -f src/*.js src/*.d.ts || true",
28
27
  "test:bdd": "vitest run --config vitest.config.bdd.ts",
29
28
  "lint": "biome check src/*.ts",
30
- "lint:fix": "biome check --write --unsafe src/*.ts"
29
+ "lint:fix": "biome check --write --unsafe src/*.ts",
30
+ "check:publish": "publint && attw --pack --ignore-rules cjs-resolves-to-esm"
31
31
  },
32
32
  "license": "MIT",
33
33
  "devDependencies": {
package/src/builder.ts CHANGED
@@ -3,8 +3,21 @@ import {
3
3
  RouteDefinitionError,
4
4
  RouteNotFoundError,
5
5
  SchmockError,
6
- } from "./errors";
7
- import { parseRouteKey } from "./parser";
6
+ } from "./errors.js";
7
+ import { parseRouteKey } from "./parser.js";
8
+ import type {
9
+ Generator,
10
+ GeneratorFunction,
11
+ GlobalConfig,
12
+ HttpMethod,
13
+ Plugin,
14
+ PluginContext,
15
+ RequestContext,
16
+ RequestOptions,
17
+ Response,
18
+ RouteConfig,
19
+ RouteKey,
20
+ } from "./types.js";
8
21
 
9
22
  /**
10
23
  * Debug logger that respects debug mode configuration
@@ -42,10 +55,10 @@ class DebugLogger {
42
55
  interface CompiledCallableRoute {
43
56
  pattern: RegExp;
44
57
  params: string[];
45
- method: Schmock.HttpMethod;
58
+ method: HttpMethod;
46
59
  path: string;
47
- generator: Schmock.Generator;
48
- config: Schmock.RouteConfig;
60
+ generator: Generator;
61
+ config: RouteConfig;
49
62
  }
50
63
 
51
64
  /**
@@ -55,10 +68,10 @@ interface CompiledCallableRoute {
55
68
  */
56
69
  export class CallableMockInstance {
57
70
  private routes: CompiledCallableRoute[] = [];
58
- private plugins: Schmock.Plugin[] = [];
71
+ private plugins: Plugin[] = [];
59
72
  private logger: DebugLogger;
60
73
 
61
- constructor(private globalConfig: Schmock.GlobalConfig = {}) {
74
+ constructor(private globalConfig: GlobalConfig = {}) {
62
75
  this.logger = new DebugLogger(globalConfig.debug || false);
63
76
  if (globalConfig.debug) {
64
77
  this.logger.log("config", "Debug mode enabled");
@@ -72,9 +85,9 @@ export class CallableMockInstance {
72
85
 
73
86
  // Method for defining routes (called when instance is invoked)
74
87
  defineRoute(
75
- route: Schmock.RouteKey,
76
- generator: Schmock.Generator,
77
- config: Schmock.RouteConfig,
88
+ route: RouteKey,
89
+ generator: Generator,
90
+ config: RouteConfig,
78
91
  ): this {
79
92
  // Auto-detect contentType if not provided
80
93
  if (!config.contentType) {
@@ -135,7 +148,7 @@ export class CallableMockInstance {
135
148
  return this;
136
149
  }
137
150
 
138
- pipe(plugin: Schmock.Plugin): this {
151
+ pipe(plugin: Plugin): this {
139
152
  this.plugins.push(plugin);
140
153
  this.logger.log(
141
154
  "plugin",
@@ -151,10 +164,10 @@ export class CallableMockInstance {
151
164
  }
152
165
 
153
166
  async handle(
154
- method: Schmock.HttpMethod,
167
+ method: HttpMethod,
155
168
  path: string,
156
- options?: Schmock.RequestOptions,
157
- ): Promise<Schmock.Response> {
169
+ options?: RequestOptions,
170
+ ): Promise<Response> {
158
171
  const requestId = Math.random().toString(36).substring(7);
159
172
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
160
173
  headers: options?.headers,
@@ -235,7 +248,7 @@ export class CallableMockInstance {
235
248
  const params = this.extractParams(matchedRoute, requestPath);
236
249
 
237
250
  // Generate initial response from route handler
238
- const context: Schmock.RequestContext = {
251
+ const context: RequestContext = {
239
252
  method,
240
253
  path: requestPath,
241
254
  params,
@@ -247,15 +260,13 @@ export class CallableMockInstance {
247
260
 
248
261
  let result: any;
249
262
  if (typeof matchedRoute.generator === "function") {
250
- result = await (matchedRoute.generator as Schmock.GeneratorFunction)(
251
- context,
252
- );
263
+ result = await (matchedRoute.generator as GeneratorFunction)(context);
253
264
  } else {
254
265
  result = matchedRoute.generator;
255
266
  }
256
267
 
257
268
  // Build plugin context
258
- let pluginContext: Schmock.PluginContext = {
269
+ let pluginContext: PluginContext = {
259
270
  path: requestPath,
260
271
  route: matchedRoute.config,
261
272
  method,
@@ -360,10 +371,7 @@ export class CallableMockInstance {
360
371
  * @returns Normalized Response object with status, body, and headers
361
372
  * @private
362
373
  */
363
- private parseResponse(
364
- result: any,
365
- routeConfig: Schmock.RouteConfig,
366
- ): Schmock.Response {
374
+ private parseResponse(result: any, routeConfig: RouteConfig): Response {
367
375
  let status = 200;
368
376
  let body = result;
369
377
  let headers: Record<string, string> = {};
@@ -433,11 +441,11 @@ export class CallableMockInstance {
433
441
  * @private
434
442
  */
435
443
  private async runPluginPipeline(
436
- context: Schmock.PluginContext,
444
+ context: PluginContext,
437
445
  initialResponse?: any,
438
- _routeConfig?: Schmock.RouteConfig,
446
+ _routeConfig?: RouteConfig,
439
447
  _requestId?: string,
440
- ): Promise<{ context: Schmock.PluginContext; response?: any }> {
448
+ ): Promise<{ context: PluginContext; response?: any }> {
441
449
  let currentContext = context;
442
450
  let response: any = initialResponse;
443
451
 
@@ -525,7 +533,7 @@ export class CallableMockInstance {
525
533
  * @private
526
534
  */
527
535
  private findRoute(
528
- method: Schmock.HttpMethod,
536
+ method: HttpMethod,
529
537
  path: string,
530
538
  ): CompiledCallableRoute | undefined {
531
539
  // First pass: Look for exact matches (routes without parameters)
package/src/index.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { CallableMockInstance } from "./builder";
1
+ import { CallableMockInstance as CallableMockInstanceImpl } from "./builder.js";
2
+ import type {
3
+ CallableMockInstance,
4
+ Generator,
5
+ GlobalConfig,
6
+ Plugin,
7
+ RouteConfig,
8
+ RouteKey,
9
+ } from "./types.js";
2
10
 
3
11
  /**
4
12
  * Create a new Schmock mock instance with callable API.
@@ -23,30 +31,28 @@ import { CallableMockInstance } from "./builder";
23
31
  * @param config Optional global configuration
24
32
  * @returns A callable mock instance
25
33
  */
26
- export function schmock(
27
- config?: Schmock.GlobalConfig,
28
- ): Schmock.CallableMockInstance {
34
+ export function schmock(config?: GlobalConfig): CallableMockInstance {
29
35
  // Always use new callable API
30
- const instance = new CallableMockInstance(config || {});
36
+ const instance = new CallableMockInstanceImpl(config || {});
31
37
 
32
38
  // Create a callable function that wraps the instance
33
39
  const callableInstance = ((
34
- route: Schmock.RouteKey,
35
- generator: Schmock.Generator,
36
- config: Schmock.RouteConfig = {},
40
+ route: RouteKey,
41
+ generator: Generator,
42
+ routeConfig: RouteConfig = {},
37
43
  ) => {
38
- instance.defineRoute(route, generator, config);
44
+ instance.defineRoute(route, generator, routeConfig);
39
45
  return callableInstance; // Return the callable function for chaining
40
46
  }) as any;
41
47
 
42
48
  // Manually bind all instance methods to the callable function with proper return values
43
- callableInstance.pipe = (plugin: Schmock.Plugin) => {
49
+ callableInstance.pipe = (plugin: Plugin) => {
44
50
  instance.pipe(plugin);
45
51
  return callableInstance; // Return callable function for chaining
46
52
  };
47
53
  callableInstance.handle = instance.handle.bind(instance);
48
54
 
49
- return callableInstance as Schmock.CallableMockInstance;
55
+ return callableInstance as CallableMockInstance;
50
56
  }
51
57
 
52
58
  // Re-export errors
@@ -60,7 +66,8 @@ export {
60
66
  SchemaGenerationError,
61
67
  SchemaValidationError,
62
68
  SchmockError,
63
- } from "./errors";
69
+ } from "./errors.js";
70
+
64
71
  // Re-export types
65
72
  export type {
66
73
  CallableMockInstance,
@@ -77,4 +84,4 @@ export type {
77
84
  ResponseResult,
78
85
  RouteConfig,
79
86
  RouteKey,
80
- } from "./types";
87
+ } from "./types.js";
package/src/parser.ts CHANGED
@@ -1,5 +1,19 @@
1
- import { RouteParseError } from "./errors";
2
- import type { HttpMethod } from "./types";
1
+ import { RouteParseError } from "./errors.js";
2
+ import type { HttpMethod } from "./types.js";
3
+
4
+ const HTTP_METHODS: readonly HttpMethod[] = [
5
+ "GET",
6
+ "POST",
7
+ "PUT",
8
+ "DELETE",
9
+ "PATCH",
10
+ "HEAD",
11
+ "OPTIONS",
12
+ ];
13
+
14
+ function isHttpMethod(method: string): method is HttpMethod {
15
+ return HTTP_METHODS.includes(method as HttpMethod);
16
+ }
3
17
 
4
18
  export interface ParsedRoute {
5
19
  method: HttpMethod;
@@ -52,8 +66,13 @@ export function parseRouteKey(routeKey: string): ParsedRoute {
52
66
 
53
67
  const pattern = new RegExp(`^${regexPath}$`);
54
68
 
69
+ // The regex guarantees method is valid, but we use the type guard for type safety
70
+ if (!isHttpMethod(method)) {
71
+ throw new RouteParseError(routeKey, `Invalid HTTP method: ${method}`);
72
+ }
73
+
55
74
  return {
56
- method: method as HttpMethod,
75
+ method,
57
76
  path,
58
77
  pattern,
59
78
  params,
package/src/types.ts CHANGED
@@ -1,17 +1,259 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
2
-
3
- // Re-export types for internal use
4
- export type HttpMethod = Schmock.HttpMethod;
5
- export type RouteKey = Schmock.RouteKey;
6
- export type ResponseResult = Schmock.ResponseResult;
7
- export type RequestContext = Schmock.RequestContext;
8
- export type Response = Schmock.Response;
9
- export type RequestOptions = Schmock.RequestOptions;
10
- export type GlobalConfig = Schmock.GlobalConfig;
11
- export type RouteConfig = Schmock.RouteConfig;
12
- export type Generator = Schmock.Generator;
13
- export type GeneratorFunction = Schmock.GeneratorFunction;
14
- export type CallableMockInstance = Schmock.CallableMockInstance;
15
- export type Plugin = Schmock.Plugin;
16
- export type PluginContext = Schmock.PluginContext;
17
- export type PluginResult = Schmock.PluginResult;
1
+ /**
2
+ * JSON Schema type (simplified for core package)
3
+ * Full schema support available via @schmock/schema
4
+ */
5
+ export interface JSONSchema {
6
+ type?: string | string[];
7
+ properties?: Record<string, JSONSchema>;
8
+ items?: JSONSchema | JSONSchema[];
9
+ required?: string[];
10
+ enum?: any[];
11
+ const?: any;
12
+ [key: string]: any;
13
+ }
14
+
15
+ /**
16
+ * HTTP methods supported by Schmock
17
+ */
18
+ export type HttpMethod =
19
+ | "GET"
20
+ | "POST"
21
+ | "PUT"
22
+ | "DELETE"
23
+ | "PATCH"
24
+ | "HEAD"
25
+ | "OPTIONS";
26
+
27
+ /**
28
+ * Route key format: 'METHOD /path'
29
+ *
30
+ * @example
31
+ * 'GET /users'
32
+ * 'POST /users/:id'
33
+ * 'DELETE /api/posts/:postId/comments/:commentId'
34
+ */
35
+ export type RouteKey = `${HttpMethod} ${string}`;
36
+
37
+ /**
38
+ * Plugin interface for extending Schmock functionality
39
+ */
40
+ export interface Plugin {
41
+ /** Unique plugin identifier */
42
+ name: string;
43
+ /** Plugin version (semver) */
44
+ version?: string;
45
+
46
+ /**
47
+ * Process the request through this plugin
48
+ * First plugin to set response becomes the generator, others transform
49
+ * @param context - Plugin context with request details
50
+ * @param response - Response from previous plugin (if any)
51
+ * @returns Updated context and response
52
+ */
53
+ process(
54
+ context: PluginContext,
55
+ response?: any,
56
+ ): PluginResult | Promise<PluginResult>;
57
+
58
+ /**
59
+ * Called when an error occurs
60
+ * Can handle, transform, or suppress errors
61
+ * @param error - The error that occurred
62
+ * @param context - Plugin context
63
+ * @returns Modified error, response data, or void to continue error propagation
64
+ */
65
+ onError?(
66
+ error: Error,
67
+ context: PluginContext,
68
+ ):
69
+ | Error
70
+ | ResponseResult
71
+ | undefined
72
+ | Promise<Error | ResponseResult | undefined>;
73
+ }
74
+
75
+ /**
76
+ * Result returned by plugin process method
77
+ */
78
+ export interface PluginResult {
79
+ /** Updated context */
80
+ context: PluginContext;
81
+ /** Response data (if generated/modified) */
82
+ response?: any;
83
+ }
84
+
85
+ /**
86
+ * Context passed through plugin pipeline
87
+ */
88
+ export interface PluginContext {
89
+ /** Request path */
90
+ path: string;
91
+ /** Matched route configuration */
92
+ route: any;
93
+ /** HTTP method */
94
+ method: HttpMethod;
95
+ /** Route parameters */
96
+ params: Record<string, string>;
97
+ /** Query parameters */
98
+ query: Record<string, string>;
99
+ /** Request headers */
100
+ headers: Record<string, string>;
101
+ /** Request body */
102
+ body?: any;
103
+ /** Shared state between plugins for this request */
104
+ state: Map<string, any>;
105
+ /** Route-specific state */
106
+ routeState?: any;
107
+ }
108
+
109
+ /**
110
+ * Global configuration options for the mock instance
111
+ */
112
+ export interface GlobalConfig {
113
+ /** Base path prefix for all routes */
114
+ namespace?: string;
115
+ /** Response delay in ms, or [min, max] for random delay */
116
+ delay?: number | [number, number];
117
+ /** Enable debug mode for detailed logging */
118
+ debug?: boolean;
119
+ /** Initial shared state object */
120
+ state?: any;
121
+ }
122
+
123
+ /**
124
+ * Route-specific configuration options
125
+ */
126
+ export interface RouteConfig {
127
+ /** MIME type for content type validation (auto-detected if not provided) */
128
+ contentType?: string;
129
+ /** Additional route-specific options */
130
+ [key: string]: any;
131
+ }
132
+
133
+ /**
134
+ * Generator types that can be passed to route definitions
135
+ */
136
+ export type Generator = GeneratorFunction | StaticData | JSONSchema;
137
+
138
+ /**
139
+ * Function that generates responses
140
+ */
141
+ export type GeneratorFunction = (
142
+ context: RequestContext,
143
+ ) => ResponseResult | Promise<ResponseResult>;
144
+
145
+ /**
146
+ * Static data (non-function) that gets returned as-is
147
+ */
148
+ export type StaticData = any;
149
+
150
+ /**
151
+ * Context passed to generator functions
152
+ */
153
+ export interface RequestContext {
154
+ /** HTTP method */
155
+ method: HttpMethod;
156
+ /** Request path */
157
+ path: string;
158
+ /** Route parameters (e.g., :id) */
159
+ params: Record<string, string>;
160
+ /** Query string parameters */
161
+ query: Record<string, string>;
162
+ /** Request headers */
163
+ headers: Record<string, string>;
164
+ /** Request body (for POST, PUT, PATCH) */
165
+ body?: any;
166
+ /** Shared mutable state */
167
+ state: any;
168
+ }
169
+
170
+ /**
171
+ * Response result types:
172
+ * - Any value: returns as 200 OK
173
+ * - [status, body]: custom status with body
174
+ * - [status, body, headers]: custom status, body, and headers
175
+ */
176
+ export type ResponseResult =
177
+ | any
178
+ | [number, any]
179
+ | [number, any, Record<string, string>];
180
+
181
+ /**
182
+ * Response object returned by handle method
183
+ */
184
+ export interface Response {
185
+ status: number;
186
+ body: any;
187
+ headers: Record<string, string>;
188
+ }
189
+
190
+ /**
191
+ * Options for handle method
192
+ */
193
+ export interface RequestOptions {
194
+ headers?: Record<string, string>;
195
+ body?: any;
196
+ query?: Record<string, string>;
197
+ }
198
+
199
+ /**
200
+ * Main callable mock instance interface
201
+ */
202
+ export interface CallableMockInstance {
203
+ /**
204
+ * Define a route by calling the instance directly
205
+ *
206
+ * @param route - Route pattern in format 'METHOD /path'
207
+ * @param generator - Response generator (function, static data, or schema)
208
+ * @param config - Route-specific configuration
209
+ * @returns The same instance for method chaining
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const mock = schmock()
214
+ * mock('GET /users', () => [...users], { contentType: 'application/json' })
215
+ * mock('POST /users', userData, { contentType: 'application/json' })
216
+ * ```
217
+ */
218
+ (
219
+ route: RouteKey,
220
+ generator: Generator,
221
+ config?: RouteConfig,
222
+ ): CallableMockInstance;
223
+
224
+ /**
225
+ * Add a plugin to the pipeline
226
+ *
227
+ * @param plugin - Plugin to add to the pipeline
228
+ * @returns The same instance for method chaining
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * mock('GET /users', generator, config)
233
+ * .pipe(authPlugin())
234
+ * .pipe(corsPlugin())
235
+ * ```
236
+ */
237
+ pipe(plugin: Plugin): CallableMockInstance;
238
+
239
+ /**
240
+ * Handle a request and return a response
241
+ *
242
+ * @param method - HTTP method
243
+ * @param path - Request path
244
+ * @param options - Request options (headers, body, query)
245
+ * @returns Promise resolving to response object
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const response = await mock.handle('GET', '/users', {
250
+ * headers: { 'Authorization': 'Bearer token' }
251
+ * })
252
+ * ```
253
+ */
254
+ handle(
255
+ method: HttpMethod,
256
+ path: string,
257
+ options?: RequestOptions,
258
+ ): Promise<Response>;
259
+ }