@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 +5 -4
- package/dist/builder.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +17 -1
- package/dist/types.d.ts +198 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/package.json +1 -1
- package/src/builder.ts +33 -25
- package/src/index.ts +18 -11
- package/src/parser.ts +20 -1
- package/src/types.ts +247 -17
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?:
|
|
12
|
-
defineRoute(route:
|
|
13
|
-
pipe(plugin:
|
|
14
|
-
handle(method:
|
|
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
|
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"
|
|
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?:
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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
|
|
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};
|
package/dist/parser.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
51
|
+
method,
|
|
36
52
|
path,
|
|
37
53
|
pattern,
|
|
38
54
|
params,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export type
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
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:
|
|
58
|
+
method: HttpMethod;
|
|
46
59
|
path: string;
|
|
47
|
-
generator:
|
|
48
|
-
config:
|
|
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:
|
|
71
|
+
private plugins: Plugin[] = [];
|
|
59
72
|
private logger: DebugLogger;
|
|
60
73
|
|
|
61
|
-
constructor(private 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:
|
|
76
|
-
generator:
|
|
77
|
-
config:
|
|
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:
|
|
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:
|
|
167
|
+
method: HttpMethod,
|
|
155
168
|
path: string,
|
|
156
|
-
options?:
|
|
157
|
-
): Promise<
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
444
|
+
context: PluginContext,
|
|
437
445
|
initialResponse?: any,
|
|
438
|
-
_routeConfig?:
|
|
446
|
+
_routeConfig?: RouteConfig,
|
|
439
447
|
_requestId?: string,
|
|
440
|
-
): Promise<{ context:
|
|
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:
|
|
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
|
|
36
|
+
const instance = new CallableMockInstanceImpl(config || {});
|
|
31
37
|
|
|
32
38
|
// Create a callable function that wraps the instance
|
|
33
39
|
const callableInstance = ((
|
|
34
|
-
route:
|
|
35
|
-
generator:
|
|
36
|
-
|
|
40
|
+
route: RouteKey,
|
|
41
|
+
generator: Generator,
|
|
42
|
+
routeConfig: RouteConfig = {},
|
|
37
43
|
) => {
|
|
38
|
-
instance.defineRoute(route, generator,
|
|
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:
|
|
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
|
|
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
|
|
75
|
+
method,
|
|
57
76
|
path,
|
|
58
77
|
pattern,
|
|
59
78
|
params,
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,247 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|