@schmock/core 1.0.0 → 1.0.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,3 +1,4 @@
1
+ import type { Generator, GlobalConfig, HttpMethod, Plugin, RequestOptions, Response, RouteConfig, RouteKey } from "./types";
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,SAAS,CAAC;AA4CjB;;;;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/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { CallableMockInstance, GlobalConfig } from "./types";
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 declare function schmock(config?: GlobalConfig): CallableMockInstance;
25
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors";
26
27
  export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, Response, ResponseResult, RouteConfig, RouteKey, } from "./types";
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,SAAS,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;;;;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,UAAU,CAAC;AAGlB,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"}
package/dist/index.js CHANGED
@@ -1 +1 @@
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
+ class Z 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 Z{constructor(A,B){super(`Route not found: ${A} ${B}`,"ROUTE_NOT_FOUND",{method:A,path:B});this.name="RouteNotFoundError"}}class L extends Z{constructor(A,B){super(`Invalid route key format: "${A}". ${B}`,"ROUTE_PARSE_ERROR",{routeKey:A,reason:B});this.name="RouteParseError"}}class v extends Z{constructor(A,B){super(`Failed to generate response for route ${A}: ${B.message}`,"RESPONSE_GENERATION_ERROR",{route:A,originalError:B});this.name="ResponseGenerationError"}}class O extends Z{constructor(A,B){super(`Plugin "${A}" failed: ${B.message}`,"PLUGIN_ERROR",{pluginName:A,originalError:B});this.name="PluginError"}}class H extends Z{constructor(A,B){super(`Invalid route definition for "${A}": ${B}`,"ROUTE_DEFINITION_ERROR",{routeKey:A,reason:B});this.name="RouteDefinitionError"}}class M extends Z{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 Z{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 Z{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"}}var x=["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"];function P(A){return x.includes(A)}function S(A){let B=A.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (.+)$/);if(!B)throw new L(A,'Expected format: "METHOD /path" (e.g., "GET /users")');let[,G,J]=B,Q=[],U=/:([^/]+)/g,V;V=U.exec(J);while(V!==null)Q.push(V[1]),V=U.exec(J);let W=J.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/:([^/]+)/g,"([^/]+)"),X=new RegExp(`^${W}$`);if(!P(G))throw new L(A,`Invalid HTTP method: ${G}`);return{method:G,path:J,pattern:X,params:Q}}class k{enabled;constructor(A=!1){this.enabled=A}log(A,B,G){if(!this.enabled)return;let Q=`[${new Date().toISOString()}] [SCHMOCK:${A.toUpperCase()}]`;if(G)console.log(`${Q} ${B}`,G);else console.log(`${Q} ${B}`)}time(A){if(!this.enabled)return;console.time(`[SCHMOCK] ${A}`)}timeEnd(A){if(!this.enabled)return;console.timeEnd(`[SCHMOCK] ${A}`)}}class w{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(U){throw new H(A,"Generator data is not valid JSON but contentType is application/json")}let J=S(A),Q={pattern:J.pattern,params:J.params,method:J.method,path:J.path,generator:B,config:G};return this.routes.push(Q),this.logger.log("route",`Route defined: ${A}`,{contentType:G.contentType,generatorType:typeof B,hasParams:J.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 J=Math.random().toString(36).substring(7);this.logger.log("request",`[${J}] ${A} ${B}`,{headers:G?.headers,query:G?.query,bodyType:G?.body?typeof G.body:"none"}),this.logger.time(`request-${J}`);try{let Q=B;if(this.globalConfig.namespace){let Y=this.globalConfig.namespace;if(Y==="/")Q=B;else{let $=Y.startsWith("/")?Y:`/${Y}`,T=B.startsWith("/")?B:`/${B}`,F=$.endsWith("/")&&$!=="/"?$.slice(0,-1):$;if(!T.startsWith(F)){this.logger.log("route",`[${J}] Path doesn't match namespace ${Y}`);let z=new _(A,B),I={status:404,body:{error:z.message,code:z.code},headers:{}};return this.logger.timeEnd(`request-${J}`),I}if(Q=T.substring(F.length),!Q.startsWith("/"))Q=`/${Q}`}}let U=this.findRoute(A,Q);if(!U){this.logger.log("route",`[${J}] No route found for ${A} ${Q}`);let Y=new _(A,B),$={status:404,body:{error:Y.message,code:Y.code},headers:{}};return this.logger.timeEnd(`request-${J}`),$}this.logger.log("route",`[${J}] Matched route: ${A} ${U.path}`);let V=this.extractParams(U,Q),W={method:A,path:Q,params:V,query:G?.query||{},headers:G?.headers||{},body:G?.body,state:this.globalConfig.state||{}},X;if(typeof U.generator==="function")X=await U.generator(W);else X=U.generator;let D={path:Q,route:U.config,method:A,params:V,query:G?.query||{},headers:G?.headers||{},body:G?.body,state:new Map,routeState:this.globalConfig.state||{}};try{let Y=await this.runPluginPipeline(D,X,U.config,J);D=Y.context,X=Y.response}catch(Y){throw this.logger.log("error",`[${J}] Plugin pipeline error: ${Y.message}`),Y}let j=this.parseResponse(X,U.config);return await this.applyDelay(),this.logger.log("response",`[${J}] Sending response ${j.status}`,{status:j.status,headers:j.headers,bodyType:typeof j.body}),this.logger.timeEnd(`request-${J}`),j}catch(Q){this.logger.log("error",`[${J}] Error processing request: ${Q.message}`,Q);let U={status:500,body:{error:Q.message,code:Q instanceof Z?Q.code:"INTERNAL_ERROR"},headers:{}};return await this.applyDelay(),this.logger.log("error",`[${J}] Returning error response 500`),this.logger.timeEnd(`request-${J}`),U}}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,J=A,Q={},U=!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,J,Q={}]=A,U=!0;if(J===null||J===void 0){if(!U)G=G===200?204:G;J=void 0}if(!Q["content-type"]&&B.contentType&&!U){if(Q["content-type"]=B.contentType,B.contentType==="text/plain"&&J!==void 0){if(typeof J==="object"&&!Buffer.isBuffer(J))J=JSON.stringify(J);else if(typeof J!=="string")J=String(J)}}return{status:G,body:J,headers:Q}}async runPluginPipeline(A,B,G,J){let Q=A,U=B;this.logger.log("pipeline",`Running plugin pipeline for ${this.plugins.length} plugins`);for(let V of this.plugins){this.logger.log("pipeline",`Processing plugin: ${V.name}`);try{let W=await V.process(Q,U);if(!W||!W.context)throw Error(`Plugin ${V.name} didn't return valid result`);if(Q=W.context,W.response!==void 0&&(U===void 0||U===null))this.logger.log("pipeline",`Plugin ${V.name} generated response`),U=W.response;else if(W.response!==void 0&&U!==void 0)this.logger.log("pipeline",`Plugin ${V.name} transformed response`),U=W.response}catch(W){if(this.logger.log("pipeline",`Plugin ${V.name} failed: ${W.message}`),V.onError)try{let X=await V.onError(W,Q);if(X){if(this.logger.log("pipeline",`Plugin ${V.name} handled error`),typeof X==="object"&&X.status){U=X;break}}}catch(X){this.logger.log("pipeline",`Plugin ${V.name} error handler failed: ${X.message}`)}throw new O(V.name,W)}}return{context:Q,response:U}}findRoute(A,B){for(let G=this.routes.length-1;G>=0;G--){let J=this.routes[G];if(J.method===A&&J.params.length===0&&J.pattern.test(B))return J}for(let G=this.routes.length-1;G>=0;G--){let J=this.routes[G];if(J.method===A&&J.params.length>0&&J.pattern.test(B))return J}return}extractParams(A,B){let G=B.match(A.pattern);if(!G)return{};let J={};return A.params.forEach((Q,U)=>{J[Q]=G[U+1]}),J}}function d(A){let B=new w(A||{}),G=(J,Q,U={})=>{return B.defineRoute(J,Q,U),G};return G.pipe=(J)=>{return B.pipe(J),G},G.handle=B.handle.bind(B),G}export{d as schmock,Z as SchmockError,M as SchemaValidationError,N as SchemaGenerationError,L as RouteParseError,_ as RouteNotFoundError,H as RouteDefinitionError,v as ResponseGenerationError,K as ResourceLimitError,O as PluginError};
@@ -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,SAAS,CAAC;AAgB1C,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
1
  import { RouteParseError } from "./errors";
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,199 @@
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
+ import type { JSONSchema7 } from "json-schema";
2
+ /**
3
+ * HTTP methods supported by Schmock
4
+ */
5
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
6
+ /**
7
+ * Route key format: 'METHOD /path'
8
+ *
9
+ * @example
10
+ * 'GET /users'
11
+ * 'POST /users/:id'
12
+ * 'DELETE /api/posts/:postId/comments/:commentId'
13
+ */
14
+ export type RouteKey = `${HttpMethod} ${string}`;
15
+ /**
16
+ * Plugin interface for extending Schmock functionality
17
+ */
18
+ export interface Plugin {
19
+ /** Unique plugin identifier */
20
+ name: string;
21
+ /** Plugin version (semver) */
22
+ version?: string;
23
+ /**
24
+ * Process the request through this plugin
25
+ * First plugin to set response becomes the generator, others transform
26
+ * @param context - Plugin context with request details
27
+ * @param response - Response from previous plugin (if any)
28
+ * @returns Updated context and response
29
+ */
30
+ process(context: PluginContext, response?: any): PluginResult | Promise<PluginResult>;
31
+ /**
32
+ * Called when an error occurs
33
+ * Can handle, transform, or suppress errors
34
+ * @param error - The error that occurred
35
+ * @param context - Plugin context
36
+ * @returns Modified error, response data, or void to continue error propagation
37
+ */
38
+ onError?(error: Error, context: PluginContext): Error | ResponseResult | undefined | Promise<Error | ResponseResult | undefined>;
39
+ }
40
+ /**
41
+ * Result returned by plugin process method
42
+ */
43
+ export interface PluginResult {
44
+ /** Updated context */
45
+ context: PluginContext;
46
+ /** Response data (if generated/modified) */
47
+ response?: any;
48
+ }
49
+ /**
50
+ * Context passed through plugin pipeline
51
+ */
52
+ export interface PluginContext {
53
+ /** Request path */
54
+ path: string;
55
+ /** Matched route configuration */
56
+ route: any;
57
+ /** HTTP method */
58
+ method: HttpMethod;
59
+ /** Route parameters */
60
+ params: Record<string, string>;
61
+ /** Query parameters */
62
+ query: Record<string, string>;
63
+ /** Request headers */
64
+ headers: Record<string, string>;
65
+ /** Request body */
66
+ body?: any;
67
+ /** Shared state between plugins for this request */
68
+ state: Map<string, any>;
69
+ /** Route-specific state */
70
+ routeState?: any;
71
+ }
72
+ /**
73
+ * Global configuration options for the mock instance
74
+ */
75
+ export interface GlobalConfig {
76
+ /** Base path prefix for all routes */
77
+ namespace?: string;
78
+ /** Response delay in ms, or [min, max] for random delay */
79
+ delay?: number | [number, number];
80
+ /** Enable debug mode for detailed logging */
81
+ debug?: boolean;
82
+ /** Initial shared state object */
83
+ state?: any;
84
+ }
85
+ /**
86
+ * Route-specific configuration options
87
+ */
88
+ export interface RouteConfig {
89
+ /** MIME type for content type validation (auto-detected if not provided) */
90
+ contentType?: string;
91
+ /** Additional route-specific options */
92
+ [key: string]: any;
93
+ }
94
+ /**
95
+ * Generator types that can be passed to route definitions
96
+ */
97
+ export type Generator = GeneratorFunction | StaticData | JSONSchema7;
98
+ /**
99
+ * Function that generates responses
100
+ */
101
+ export type GeneratorFunction = (context: RequestContext) => ResponseResult | Promise<ResponseResult>;
102
+ /**
103
+ * Static data (non-function) that gets returned as-is
104
+ */
105
+ export type StaticData = any;
106
+ /**
107
+ * Context passed to generator functions
108
+ */
109
+ export interface RequestContext {
110
+ /** HTTP method */
111
+ method: HttpMethod;
112
+ /** Request path */
113
+ path: string;
114
+ /** Route parameters (e.g., :id) */
115
+ params: Record<string, string>;
116
+ /** Query string parameters */
117
+ query: Record<string, string>;
118
+ /** Request headers */
119
+ headers: Record<string, string>;
120
+ /** Request body (for POST, PUT, PATCH) */
121
+ body?: any;
122
+ /** Shared mutable state */
123
+ state: any;
124
+ }
125
+ /**
126
+ * Response result types:
127
+ * - Any value: returns as 200 OK
128
+ * - [status, body]: custom status with body
129
+ * - [status, body, headers]: custom status, body, and headers
130
+ */
131
+ export type ResponseResult = any | [number, any] | [number, any, Record<string, string>];
132
+ /**
133
+ * Response object returned by handle method
134
+ */
135
+ export interface Response {
136
+ status: number;
137
+ body: any;
138
+ headers: Record<string, string>;
139
+ }
140
+ /**
141
+ * Options for handle method
142
+ */
143
+ export interface RequestOptions {
144
+ headers?: Record<string, string>;
145
+ body?: any;
146
+ query?: Record<string, string>;
147
+ }
148
+ /**
149
+ * Main callable mock instance interface
150
+ */
151
+ export interface CallableMockInstance {
152
+ /**
153
+ * Define a route by calling the instance directly
154
+ *
155
+ * @param route - Route pattern in format 'METHOD /path'
156
+ * @param generator - Response generator (function, static data, or schema)
157
+ * @param config - Route-specific configuration
158
+ * @returns The same instance for method chaining
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const mock = schmock()
163
+ * mock('GET /users', () => [...users], { contentType: 'application/json' })
164
+ * mock('POST /users', userData, { contentType: 'application/json' })
165
+ * ```
166
+ */
167
+ (route: RouteKey, generator: Generator, config?: RouteConfig): CallableMockInstance;
168
+ /**
169
+ * Add a plugin to the pipeline
170
+ *
171
+ * @param plugin - Plugin to add to the pipeline
172
+ * @returns The same instance for method chaining
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * mock('GET /users', generator, config)
177
+ * .pipe(authPlugin())
178
+ * .pipe(corsPlugin())
179
+ * ```
180
+ */
181
+ pipe(plugin: Plugin): CallableMockInstance;
182
+ /**
183
+ * Handle a request and return a response
184
+ *
185
+ * @param method - HTTP method
186
+ * @param path - Request path
187
+ * @param options - Request options (headers, body, query)
188
+ * @returns Promise resolving to response object
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * const response = await mock.handle('GET', '/users', {
193
+ * headers: { 'Authorization': 'Bearer token' }
194
+ * })
195
+ * ```
196
+ */
197
+ handle(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
198
+ }
15
199
  //# 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.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
package/src/builder.ts CHANGED
@@ -5,6 +5,19 @@ import {
5
5
  SchmockError,
6
6
  } from "./errors";
7
7
  import { parseRouteKey } from "./parser";
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";
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";
2
+ import type {
3
+ CallableMockInstance,
4
+ Generator,
5
+ GlobalConfig,
6
+ Plugin,
7
+ RouteConfig,
8
+ RouteKey,
9
+ } from "./types";
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
@@ -61,6 +67,7 @@ export {
61
67
  SchemaValidationError,
62
68
  SchmockError,
63
69
  } from "./errors";
70
+
64
71
  // Re-export types
65
72
  export type {
66
73
  CallableMockInstance,
package/src/parser.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import { RouteParseError } from "./errors";
2
2
  import type { HttpMethod } from "./types";
3
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
+ }
17
+
4
18
  export interface ParsedRoute {
5
19
  method: HttpMethod;
6
20
  path: string;
@@ -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,247 @@
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
+ import type { JSONSchema7 } from "json-schema";
2
+
3
+ /**
4
+ * HTTP methods supported by Schmock
5
+ */
6
+ export type HttpMethod =
7
+ | "GET"
8
+ | "POST"
9
+ | "PUT"
10
+ | "DELETE"
11
+ | "PATCH"
12
+ | "HEAD"
13
+ | "OPTIONS";
14
+
15
+ /**
16
+ * Route key format: 'METHOD /path'
17
+ *
18
+ * @example
19
+ * 'GET /users'
20
+ * 'POST /users/:id'
21
+ * 'DELETE /api/posts/:postId/comments/:commentId'
22
+ */
23
+ export type RouteKey = `${HttpMethod} ${string}`;
24
+
25
+ /**
26
+ * Plugin interface for extending Schmock functionality
27
+ */
28
+ export interface Plugin {
29
+ /** Unique plugin identifier */
30
+ name: string;
31
+ /** Plugin version (semver) */
32
+ version?: string;
33
+
34
+ /**
35
+ * Process the request through this plugin
36
+ * First plugin to set response becomes the generator, others transform
37
+ * @param context - Plugin context with request details
38
+ * @param response - Response from previous plugin (if any)
39
+ * @returns Updated context and response
40
+ */
41
+ process(
42
+ context: PluginContext,
43
+ response?: any,
44
+ ): PluginResult | Promise<PluginResult>;
45
+
46
+ /**
47
+ * Called when an error occurs
48
+ * Can handle, transform, or suppress errors
49
+ * @param error - The error that occurred
50
+ * @param context - Plugin context
51
+ * @returns Modified error, response data, or void to continue error propagation
52
+ */
53
+ onError?(
54
+ error: Error,
55
+ context: PluginContext,
56
+ ):
57
+ | Error
58
+ | ResponseResult
59
+ | undefined
60
+ | Promise<Error | ResponseResult | undefined>;
61
+ }
62
+
63
+ /**
64
+ * Result returned by plugin process method
65
+ */
66
+ export interface PluginResult {
67
+ /** Updated context */
68
+ context: PluginContext;
69
+ /** Response data (if generated/modified) */
70
+ response?: any;
71
+ }
72
+
73
+ /**
74
+ * Context passed through plugin pipeline
75
+ */
76
+ export interface PluginContext {
77
+ /** Request path */
78
+ path: string;
79
+ /** Matched route configuration */
80
+ route: any;
81
+ /** HTTP method */
82
+ method: HttpMethod;
83
+ /** Route parameters */
84
+ params: Record<string, string>;
85
+ /** Query parameters */
86
+ query: Record<string, string>;
87
+ /** Request headers */
88
+ headers: Record<string, string>;
89
+ /** Request body */
90
+ body?: any;
91
+ /** Shared state between plugins for this request */
92
+ state: Map<string, any>;
93
+ /** Route-specific state */
94
+ routeState?: any;
95
+ }
96
+
97
+ /**
98
+ * Global configuration options for the mock instance
99
+ */
100
+ export interface GlobalConfig {
101
+ /** Base path prefix for all routes */
102
+ namespace?: string;
103
+ /** Response delay in ms, or [min, max] for random delay */
104
+ delay?: number | [number, number];
105
+ /** Enable debug mode for detailed logging */
106
+ debug?: boolean;
107
+ /** Initial shared state object */
108
+ state?: any;
109
+ }
110
+
111
+ /**
112
+ * Route-specific configuration options
113
+ */
114
+ export interface RouteConfig {
115
+ /** MIME type for content type validation (auto-detected if not provided) */
116
+ contentType?: string;
117
+ /** Additional route-specific options */
118
+ [key: string]: any;
119
+ }
120
+
121
+ /**
122
+ * Generator types that can be passed to route definitions
123
+ */
124
+ export type Generator = GeneratorFunction | StaticData | JSONSchema7;
125
+
126
+ /**
127
+ * Function that generates responses
128
+ */
129
+ export type GeneratorFunction = (
130
+ context: RequestContext,
131
+ ) => ResponseResult | Promise<ResponseResult>;
132
+
133
+ /**
134
+ * Static data (non-function) that gets returned as-is
135
+ */
136
+ export type StaticData = any;
137
+
138
+ /**
139
+ * Context passed to generator functions
140
+ */
141
+ export interface RequestContext {
142
+ /** HTTP method */
143
+ method: HttpMethod;
144
+ /** Request path */
145
+ path: string;
146
+ /** Route parameters (e.g., :id) */
147
+ params: Record<string, string>;
148
+ /** Query string parameters */
149
+ query: Record<string, string>;
150
+ /** Request headers */
151
+ headers: Record<string, string>;
152
+ /** Request body (for POST, PUT, PATCH) */
153
+ body?: any;
154
+ /** Shared mutable state */
155
+ state: any;
156
+ }
157
+
158
+ /**
159
+ * Response result types:
160
+ * - Any value: returns as 200 OK
161
+ * - [status, body]: custom status with body
162
+ * - [status, body, headers]: custom status, body, and headers
163
+ */
164
+ export type ResponseResult =
165
+ | any
166
+ | [number, any]
167
+ | [number, any, Record<string, string>];
168
+
169
+ /**
170
+ * Response object returned by handle method
171
+ */
172
+ export interface Response {
173
+ status: number;
174
+ body: any;
175
+ headers: Record<string, string>;
176
+ }
177
+
178
+ /**
179
+ * Options for handle method
180
+ */
181
+ export interface RequestOptions {
182
+ headers?: Record<string, string>;
183
+ body?: any;
184
+ query?: Record<string, string>;
185
+ }
186
+
187
+ /**
188
+ * Main callable mock instance interface
189
+ */
190
+ export interface CallableMockInstance {
191
+ /**
192
+ * Define a route by calling the instance directly
193
+ *
194
+ * @param route - Route pattern in format 'METHOD /path'
195
+ * @param generator - Response generator (function, static data, or schema)
196
+ * @param config - Route-specific configuration
197
+ * @returns The same instance for method chaining
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const mock = schmock()
202
+ * mock('GET /users', () => [...users], { contentType: 'application/json' })
203
+ * mock('POST /users', userData, { contentType: 'application/json' })
204
+ * ```
205
+ */
206
+ (
207
+ route: RouteKey,
208
+ generator: Generator,
209
+ config?: RouteConfig,
210
+ ): CallableMockInstance;
211
+
212
+ /**
213
+ * Add a plugin to the pipeline
214
+ *
215
+ * @param plugin - Plugin to add to the pipeline
216
+ * @returns The same instance for method chaining
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * mock('GET /users', generator, config)
221
+ * .pipe(authPlugin())
222
+ * .pipe(corsPlugin())
223
+ * ```
224
+ */
225
+ pipe(plugin: Plugin): CallableMockInstance;
226
+
227
+ /**
228
+ * Handle a request and return a response
229
+ *
230
+ * @param method - HTTP method
231
+ * @param path - Request path
232
+ * @param options - Request options (headers, body, query)
233
+ * @returns Promise resolving to response object
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const response = await mock.handle('GET', '/users', {
238
+ * headers: { 'Authorization': 'Bearer token' }
239
+ * })
240
+ * ```
241
+ */
242
+ handle(
243
+ method: HttpMethod,
244
+ path: string,
245
+ options?: RequestOptions,
246
+ ): Promise<Response>;
247
+ }