@schmock/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder.d.ts +62 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +432 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +92 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/parser.d.ts +19 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +40 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +39 -0
- package/src/builder.d.ts.map +1 -0
- package/src/builder.test.ts +289 -0
- package/src/builder.ts +580 -0
- package/src/debug.test.ts +241 -0
- package/src/delay.test.ts +319 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.test.ts +223 -0
- package/src/errors.ts +124 -0
- package/src/factory.test.ts +133 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +80 -0
- package/src/namespace.test.ts +273 -0
- package/src/parser.d.ts.map +1 -0
- package/src/parser.test.ts +131 -0
- package/src/parser.ts +61 -0
- package/src/plugin-system.test.ts +511 -0
- package/src/response-parsing.test.ts +255 -0
- package/src/route-matching.test.ts +351 -0
- package/src/smart-defaults.test.ts +361 -0
- package/src/steps/async-support.steps.ts +427 -0
- package/src/steps/basic-usage.steps.ts +316 -0
- package/src/steps/developer-experience.steps.ts +439 -0
- package/src/steps/error-handling.steps.ts +387 -0
- package/src/steps/fluent-api.steps.ts +252 -0
- package/src/steps/http-methods.steps.ts +397 -0
- package/src/steps/performance-reliability.steps.ts +459 -0
- package/src/steps/plugin-integration.steps.ts +279 -0
- package/src/steps/route-key-format.steps.ts +118 -0
- package/src/steps/state-concurrency.steps.ts +643 -0
- package/src/steps/stateful-workflows.steps.ts +351 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callable mock instance that implements the new API.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export declare class CallableMockInstance {
|
|
7
|
+
private globalConfig;
|
|
8
|
+
private routes;
|
|
9
|
+
private plugins;
|
|
10
|
+
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>;
|
|
15
|
+
/**
|
|
16
|
+
* Apply configured response delay
|
|
17
|
+
* Supports both fixed delays and random delays within a range
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
private applyDelay;
|
|
21
|
+
/**
|
|
22
|
+
* Parse and normalize response result into Response object
|
|
23
|
+
* Handles tuple format [status, body, headers], direct values, and response objects
|
|
24
|
+
* @param result - Raw result from generator or plugin
|
|
25
|
+
* @param routeConfig - Route configuration for content-type defaults
|
|
26
|
+
* @returns Normalized Response object with status, body, and headers
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
private parseResponse;
|
|
30
|
+
/**
|
|
31
|
+
* Run all registered plugins in sequence
|
|
32
|
+
* First plugin to set response becomes generator, subsequent plugins transform
|
|
33
|
+
* Handles plugin errors via onError hooks
|
|
34
|
+
* @param context - Plugin context with request details
|
|
35
|
+
* @param initialResponse - Initial response from route generator
|
|
36
|
+
* @param _routeConfig - Route config (unused but kept for signature)
|
|
37
|
+
* @param _requestId - Request ID (unused but kept for signature)
|
|
38
|
+
* @returns Updated context and final response after all plugins
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
private runPluginPipeline;
|
|
42
|
+
/**
|
|
43
|
+
* Find a route that matches the given method and path
|
|
44
|
+
* Uses two-pass matching: exact routes first, then parameterized routes
|
|
45
|
+
* Searches in reverse order to prefer most recently defined routes
|
|
46
|
+
* @param method - HTTP method to match
|
|
47
|
+
* @param path - Request path to match
|
|
48
|
+
* @returns Matched compiled route or undefined if no match
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
private findRoute;
|
|
52
|
+
/**
|
|
53
|
+
* Extract parameter values from path based on route pattern
|
|
54
|
+
* Maps capture groups from regex match to parameter names
|
|
55
|
+
* @param route - Compiled route with pattern and param names
|
|
56
|
+
* @param path - Request path to extract values from
|
|
57
|
+
* @returns Object mapping parameter names to extracted values
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
private extractParams;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors";
|
|
2
|
+
import { parseRouteKey } from "./parser";
|
|
3
|
+
/**
|
|
4
|
+
* Debug logger that respects debug mode configuration
|
|
5
|
+
*/
|
|
6
|
+
class DebugLogger {
|
|
7
|
+
enabled;
|
|
8
|
+
constructor(enabled = false) {
|
|
9
|
+
this.enabled = enabled;
|
|
10
|
+
}
|
|
11
|
+
log(category, message, data) {
|
|
12
|
+
if (!this.enabled)
|
|
13
|
+
return;
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
const prefix = `[${timestamp}] [SCHMOCK:${category.toUpperCase()}]`;
|
|
16
|
+
if (data) {
|
|
17
|
+
console.log(`${prefix} ${message}`, data);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`${prefix} ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
time(label) {
|
|
24
|
+
if (!this.enabled)
|
|
25
|
+
return;
|
|
26
|
+
console.time(`[SCHMOCK] ${label}`);
|
|
27
|
+
}
|
|
28
|
+
timeEnd(label) {
|
|
29
|
+
if (!this.enabled)
|
|
30
|
+
return;
|
|
31
|
+
console.timeEnd(`[SCHMOCK] ${label}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Callable mock instance that implements the new API.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export class CallableMockInstance {
|
|
40
|
+
globalConfig;
|
|
41
|
+
routes = [];
|
|
42
|
+
plugins = [];
|
|
43
|
+
logger;
|
|
44
|
+
constructor(globalConfig = {}) {
|
|
45
|
+
this.globalConfig = globalConfig;
|
|
46
|
+
this.logger = new DebugLogger(globalConfig.debug || false);
|
|
47
|
+
if (globalConfig.debug) {
|
|
48
|
+
this.logger.log("config", "Debug mode enabled");
|
|
49
|
+
}
|
|
50
|
+
this.logger.log("config", "Callable mock instance created", {
|
|
51
|
+
debug: globalConfig.debug,
|
|
52
|
+
namespace: globalConfig.namespace,
|
|
53
|
+
delay: globalConfig.delay,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Method for defining routes (called when instance is invoked)
|
|
57
|
+
defineRoute(route, generator, config) {
|
|
58
|
+
// Auto-detect contentType if not provided
|
|
59
|
+
if (!config.contentType) {
|
|
60
|
+
if (typeof generator === "function") {
|
|
61
|
+
// Default to JSON for function generators
|
|
62
|
+
config.contentType = "application/json";
|
|
63
|
+
}
|
|
64
|
+
else if (typeof generator === "string" ||
|
|
65
|
+
typeof generator === "number" ||
|
|
66
|
+
typeof generator === "boolean") {
|
|
67
|
+
// Default to plain text for primitives
|
|
68
|
+
config.contentType = "text/plain";
|
|
69
|
+
}
|
|
70
|
+
else if (Buffer.isBuffer(generator)) {
|
|
71
|
+
// Default to octet-stream for buffers
|
|
72
|
+
config.contentType = "application/octet-stream";
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Default to JSON for objects/arrays
|
|
76
|
+
config.contentType = "application/json";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Validate generator matches contentType if it's static data
|
|
80
|
+
if (typeof generator !== "function" &&
|
|
81
|
+
config.contentType === "application/json") {
|
|
82
|
+
try {
|
|
83
|
+
JSON.stringify(generator);
|
|
84
|
+
}
|
|
85
|
+
catch (_error) {
|
|
86
|
+
throw new RouteDefinitionError(route, "Generator data is not valid JSON but contentType is application/json");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Parse the route key to create pattern and extract parameters
|
|
90
|
+
const parsed = parseRouteKey(route);
|
|
91
|
+
// Compile the route
|
|
92
|
+
const compiledRoute = {
|
|
93
|
+
pattern: parsed.pattern,
|
|
94
|
+
params: parsed.params,
|
|
95
|
+
method: parsed.method,
|
|
96
|
+
path: parsed.path,
|
|
97
|
+
generator,
|
|
98
|
+
config,
|
|
99
|
+
};
|
|
100
|
+
this.routes.push(compiledRoute);
|
|
101
|
+
this.logger.log("route", `Route defined: ${route}`, {
|
|
102
|
+
contentType: config.contentType,
|
|
103
|
+
generatorType: typeof generator,
|
|
104
|
+
hasParams: parsed.params.length > 0,
|
|
105
|
+
});
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
pipe(plugin) {
|
|
109
|
+
this.plugins.push(plugin);
|
|
110
|
+
this.logger.log("plugin", `Registered plugin: ${plugin.name}@${plugin.version || "unknown"}`, {
|
|
111
|
+
name: plugin.name,
|
|
112
|
+
version: plugin.version,
|
|
113
|
+
hasProcess: typeof plugin.process === "function",
|
|
114
|
+
hasOnError: typeof plugin.onError === "function",
|
|
115
|
+
});
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
async handle(method, path, options) {
|
|
119
|
+
const requestId = Math.random().toString(36).substring(7);
|
|
120
|
+
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
121
|
+
headers: options?.headers,
|
|
122
|
+
query: options?.query,
|
|
123
|
+
bodyType: options?.body ? typeof options.body : "none",
|
|
124
|
+
});
|
|
125
|
+
this.logger.time(`request-${requestId}`);
|
|
126
|
+
try {
|
|
127
|
+
// Apply namespace if configured
|
|
128
|
+
let requestPath = path;
|
|
129
|
+
if (this.globalConfig.namespace) {
|
|
130
|
+
// Normalize namespace to handle edge cases
|
|
131
|
+
const namespace = this.globalConfig.namespace;
|
|
132
|
+
if (namespace === "/") {
|
|
133
|
+
// Root namespace means no transformation needed
|
|
134
|
+
requestPath = path;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Handle namespace without leading slash by normalizing both namespace and path
|
|
138
|
+
const normalizedNamespace = namespace.startsWith("/")
|
|
139
|
+
? namespace
|
|
140
|
+
: `/${namespace}`;
|
|
141
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
142
|
+
// Remove trailing slash from namespace unless it's root
|
|
143
|
+
const finalNamespace = normalizedNamespace.endsWith("/") && normalizedNamespace !== "/"
|
|
144
|
+
? normalizedNamespace.slice(0, -1)
|
|
145
|
+
: normalizedNamespace;
|
|
146
|
+
if (!normalizedPath.startsWith(finalNamespace)) {
|
|
147
|
+
this.logger.log("route", `[${requestId}] Path doesn't match namespace ${namespace}`);
|
|
148
|
+
const error = new RouteNotFoundError(method, path);
|
|
149
|
+
const response = {
|
|
150
|
+
status: 404,
|
|
151
|
+
body: { error: error.message, code: error.code },
|
|
152
|
+
headers: {},
|
|
153
|
+
};
|
|
154
|
+
this.logger.timeEnd(`request-${requestId}`);
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
// Remove namespace prefix, ensuring we always start with /
|
|
158
|
+
requestPath = normalizedPath.substring(finalNamespace.length);
|
|
159
|
+
if (!requestPath.startsWith("/")) {
|
|
160
|
+
requestPath = `/${requestPath}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Find matching route
|
|
165
|
+
const matchedRoute = this.findRoute(method, requestPath);
|
|
166
|
+
if (!matchedRoute) {
|
|
167
|
+
this.logger.log("route", `[${requestId}] No route found for ${method} ${requestPath}`);
|
|
168
|
+
const error = new RouteNotFoundError(method, path);
|
|
169
|
+
const response = {
|
|
170
|
+
status: 404,
|
|
171
|
+
body: { error: error.message, code: error.code },
|
|
172
|
+
headers: {},
|
|
173
|
+
};
|
|
174
|
+
this.logger.timeEnd(`request-${requestId}`);
|
|
175
|
+
return response;
|
|
176
|
+
}
|
|
177
|
+
this.logger.log("route", `[${requestId}] Matched route: ${method} ${matchedRoute.path}`);
|
|
178
|
+
// Extract parameters from the matched route
|
|
179
|
+
const params = this.extractParams(matchedRoute, requestPath);
|
|
180
|
+
// Generate initial response from route handler
|
|
181
|
+
const context = {
|
|
182
|
+
method,
|
|
183
|
+
path: requestPath,
|
|
184
|
+
params,
|
|
185
|
+
query: options?.query || {},
|
|
186
|
+
headers: options?.headers || {},
|
|
187
|
+
body: options?.body,
|
|
188
|
+
state: this.globalConfig.state || {},
|
|
189
|
+
};
|
|
190
|
+
let result;
|
|
191
|
+
if (typeof matchedRoute.generator === "function") {
|
|
192
|
+
result = await matchedRoute.generator(context);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
result = matchedRoute.generator;
|
|
196
|
+
}
|
|
197
|
+
// Build plugin context
|
|
198
|
+
let pluginContext = {
|
|
199
|
+
path: requestPath,
|
|
200
|
+
route: matchedRoute.config,
|
|
201
|
+
method,
|
|
202
|
+
params,
|
|
203
|
+
query: options?.query || {},
|
|
204
|
+
headers: options?.headers || {},
|
|
205
|
+
body: options?.body,
|
|
206
|
+
state: new Map(),
|
|
207
|
+
routeState: this.globalConfig.state || {},
|
|
208
|
+
};
|
|
209
|
+
// Run plugin pipeline to transform the response
|
|
210
|
+
try {
|
|
211
|
+
const pipelineResult = await this.runPluginPipeline(pluginContext, result, matchedRoute.config, requestId);
|
|
212
|
+
pluginContext = pipelineResult.context;
|
|
213
|
+
result = pipelineResult.response;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
this.logger.log("error", `[${requestId}] Plugin pipeline error: ${error.message}`);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
// Parse and prepare response
|
|
220
|
+
const response = this.parseResponse(result, matchedRoute.config);
|
|
221
|
+
// Apply global delay if configured
|
|
222
|
+
await this.applyDelay();
|
|
223
|
+
// Log successful response
|
|
224
|
+
this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
|
|
225
|
+
status: response.status,
|
|
226
|
+
headers: response.headers,
|
|
227
|
+
bodyType: typeof response.body,
|
|
228
|
+
});
|
|
229
|
+
this.logger.timeEnd(`request-${requestId}`);
|
|
230
|
+
return response;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
this.logger.log("error", `[${requestId}] Error processing request: ${error.message}`, error);
|
|
234
|
+
// Return error response
|
|
235
|
+
const errorResponse = {
|
|
236
|
+
status: 500,
|
|
237
|
+
body: {
|
|
238
|
+
error: error.message,
|
|
239
|
+
code: error instanceof SchmockError
|
|
240
|
+
? error.code
|
|
241
|
+
: "INTERNAL_ERROR",
|
|
242
|
+
},
|
|
243
|
+
headers: {},
|
|
244
|
+
};
|
|
245
|
+
// Apply global delay if configured (even for error responses)
|
|
246
|
+
await this.applyDelay();
|
|
247
|
+
this.logger.log("error", `[${requestId}] Returning error response 500`);
|
|
248
|
+
this.logger.timeEnd(`request-${requestId}`);
|
|
249
|
+
return errorResponse;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Apply configured response delay
|
|
254
|
+
* Supports both fixed delays and random delays within a range
|
|
255
|
+
* @private
|
|
256
|
+
*/
|
|
257
|
+
async applyDelay() {
|
|
258
|
+
if (!this.globalConfig.delay) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const delay = Array.isArray(this.globalConfig.delay)
|
|
262
|
+
? Math.random() *
|
|
263
|
+
(this.globalConfig.delay[1] - this.globalConfig.delay[0]) +
|
|
264
|
+
this.globalConfig.delay[0]
|
|
265
|
+
: this.globalConfig.delay;
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Parse and normalize response result into Response object
|
|
270
|
+
* Handles tuple format [status, body, headers], direct values, and response objects
|
|
271
|
+
* @param result - Raw result from generator or plugin
|
|
272
|
+
* @param routeConfig - Route configuration for content-type defaults
|
|
273
|
+
* @returns Normalized Response object with status, body, and headers
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
parseResponse(result, routeConfig) {
|
|
277
|
+
let status = 200;
|
|
278
|
+
let body = result;
|
|
279
|
+
let headers = {};
|
|
280
|
+
let tupleFormat = false;
|
|
281
|
+
// Handle already-formed response objects (from plugin error recovery)
|
|
282
|
+
if (result &&
|
|
283
|
+
typeof result === "object" &&
|
|
284
|
+
"status" in result &&
|
|
285
|
+
"body" in result) {
|
|
286
|
+
return {
|
|
287
|
+
status: result.status,
|
|
288
|
+
body: result.body,
|
|
289
|
+
headers: result.headers || {},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Handle tuple response format [status, body, headers?]
|
|
293
|
+
if (Array.isArray(result) && typeof result[0] === "number") {
|
|
294
|
+
[status, body, headers = {}] = result;
|
|
295
|
+
tupleFormat = true;
|
|
296
|
+
}
|
|
297
|
+
// Handle null/undefined responses with 204 No Content
|
|
298
|
+
// But don't auto-convert if tuple format was used (status was explicitly provided)
|
|
299
|
+
if (body === null || body === undefined) {
|
|
300
|
+
if (!tupleFormat) {
|
|
301
|
+
status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
|
|
302
|
+
}
|
|
303
|
+
body = undefined; // Ensure body is undefined for null responses
|
|
304
|
+
}
|
|
305
|
+
// Add content-type header from route config if it exists and headers don't already have it
|
|
306
|
+
// But only if this isn't a tuple response (where headers are explicitly controlled)
|
|
307
|
+
if (!headers["content-type"] && routeConfig.contentType && !tupleFormat) {
|
|
308
|
+
headers["content-type"] = routeConfig.contentType;
|
|
309
|
+
// Handle special conversion cases when contentType is explicitly set
|
|
310
|
+
if (routeConfig.contentType === "text/plain" && body !== undefined) {
|
|
311
|
+
if (typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
312
|
+
body = JSON.stringify(body);
|
|
313
|
+
}
|
|
314
|
+
else if (typeof body !== "string") {
|
|
315
|
+
body = String(body);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
status,
|
|
321
|
+
body,
|
|
322
|
+
headers,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Run all registered plugins in sequence
|
|
327
|
+
* First plugin to set response becomes generator, subsequent plugins transform
|
|
328
|
+
* Handles plugin errors via onError hooks
|
|
329
|
+
* @param context - Plugin context with request details
|
|
330
|
+
* @param initialResponse - Initial response from route generator
|
|
331
|
+
* @param _routeConfig - Route config (unused but kept for signature)
|
|
332
|
+
* @param _requestId - Request ID (unused but kept for signature)
|
|
333
|
+
* @returns Updated context and final response after all plugins
|
|
334
|
+
* @private
|
|
335
|
+
*/
|
|
336
|
+
async runPluginPipeline(context, initialResponse, _routeConfig, _requestId) {
|
|
337
|
+
let currentContext = context;
|
|
338
|
+
let response = initialResponse;
|
|
339
|
+
this.logger.log("pipeline", `Running plugin pipeline for ${this.plugins.length} plugins`);
|
|
340
|
+
for (const plugin of this.plugins) {
|
|
341
|
+
this.logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
342
|
+
try {
|
|
343
|
+
const result = await plugin.process(currentContext, response);
|
|
344
|
+
if (!result || !result.context) {
|
|
345
|
+
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
346
|
+
}
|
|
347
|
+
currentContext = result.context;
|
|
348
|
+
// First plugin to set response becomes the generator
|
|
349
|
+
if (result.response !== undefined &&
|
|
350
|
+
(response === undefined || response === null)) {
|
|
351
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} generated response`);
|
|
352
|
+
response = result.response;
|
|
353
|
+
}
|
|
354
|
+
else if (result.response !== undefined && response !== undefined) {
|
|
355
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} transformed response`);
|
|
356
|
+
response = result.response;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} failed: ${error.message}`);
|
|
361
|
+
// Try error handling if plugin has onError hook
|
|
362
|
+
if (plugin.onError) {
|
|
363
|
+
try {
|
|
364
|
+
const errorResult = await plugin.onError(error, currentContext);
|
|
365
|
+
if (errorResult) {
|
|
366
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} handled error`);
|
|
367
|
+
// If error handler returns response, use it and stop pipeline
|
|
368
|
+
if (typeof errorResult === "object" && errorResult.status) {
|
|
369
|
+
// Return the error response as the current response, stop pipeline
|
|
370
|
+
response = errorResult;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (hookError) {
|
|
376
|
+
this.logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${hookError.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
throw new PluginError(plugin.name, error);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { context: currentContext, response };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Find a route that matches the given method and path
|
|
386
|
+
* Uses two-pass matching: exact routes first, then parameterized routes
|
|
387
|
+
* Searches in reverse order to prefer most recently defined routes
|
|
388
|
+
* @param method - HTTP method to match
|
|
389
|
+
* @param path - Request path to match
|
|
390
|
+
* @returns Matched compiled route or undefined if no match
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
findRoute(method, path) {
|
|
394
|
+
// First pass: Look for exact matches (routes without parameters)
|
|
395
|
+
for (let i = this.routes.length - 1; i >= 0; i--) {
|
|
396
|
+
const route = this.routes[i];
|
|
397
|
+
if (route.method === method &&
|
|
398
|
+
route.params.length === 0 &&
|
|
399
|
+
route.pattern.test(path)) {
|
|
400
|
+
return route;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Second pass: Look for parameterized routes
|
|
404
|
+
for (let i = this.routes.length - 1; i >= 0; i--) {
|
|
405
|
+
const route = this.routes[i];
|
|
406
|
+
if (route.method === method &&
|
|
407
|
+
route.params.length > 0 &&
|
|
408
|
+
route.pattern.test(path)) {
|
|
409
|
+
return route;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Extract parameter values from path based on route pattern
|
|
416
|
+
* Maps capture groups from regex match to parameter names
|
|
417
|
+
* @param route - Compiled route with pattern and param names
|
|
418
|
+
* @param path - Request path to extract values from
|
|
419
|
+
* @returns Object mapping parameter names to extracted values
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
extractParams(route, path) {
|
|
423
|
+
const match = path.match(route.pattern);
|
|
424
|
+
if (!match)
|
|
425
|
+
return {};
|
|
426
|
+
const params = {};
|
|
427
|
+
route.params.forEach((param, index) => {
|
|
428
|
+
params[param] = match[index + 1];
|
|
429
|
+
});
|
|
430
|
+
return params;
|
|
431
|
+
}
|
|
432
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Schmock errors
|
|
3
|
+
*/
|
|
4
|
+
export declare class SchmockError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly context?: unknown;
|
|
7
|
+
constructor(message: string, code: string, context?: unknown);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when a route is not found
|
|
11
|
+
*/
|
|
12
|
+
export declare class RouteNotFoundError extends SchmockError {
|
|
13
|
+
constructor(method: string, path: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when route parsing fails
|
|
17
|
+
*/
|
|
18
|
+
export declare class RouteParseError extends SchmockError {
|
|
19
|
+
constructor(routeKey: string, reason: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when response generation fails
|
|
23
|
+
*/
|
|
24
|
+
export declare class ResponseGenerationError extends SchmockError {
|
|
25
|
+
constructor(route: string, error: Error);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when a plugin fails
|
|
29
|
+
*/
|
|
30
|
+
export declare class PluginError extends SchmockError {
|
|
31
|
+
constructor(pluginName: string, error: Error);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when route definition is invalid
|
|
35
|
+
*/
|
|
36
|
+
export declare class RouteDefinitionError extends SchmockError {
|
|
37
|
+
constructor(routeKey: string, reason: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when schema validation fails
|
|
41
|
+
*/
|
|
42
|
+
export declare class SchemaValidationError extends SchmockError {
|
|
43
|
+
constructor(schemaPath: string, issue: string, suggestion?: string);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when schema generation fails
|
|
47
|
+
*/
|
|
48
|
+
export declare class SchemaGenerationError extends SchmockError {
|
|
49
|
+
constructor(route: string, error: Error, schema?: unknown);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Error thrown when resource limits are exceeded
|
|
53
|
+
*/
|
|
54
|
+
export declare class ResourceLimitError extends SchmockError {
|
|
55
|
+
constructor(resource: string, limit: number, actual?: number);
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAMpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Schmock errors
|
|
3
|
+
*/
|
|
4
|
+
export class SchmockError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
context;
|
|
7
|
+
constructor(message, code, context) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.name = "SchmockError";
|
|
12
|
+
Error.captureStackTrace(this, this.constructor);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when a route is not found
|
|
17
|
+
*/
|
|
18
|
+
export class RouteNotFoundError extends SchmockError {
|
|
19
|
+
constructor(method, path) {
|
|
20
|
+
super(`Route not found: ${method} ${path}`, "ROUTE_NOT_FOUND", {
|
|
21
|
+
method,
|
|
22
|
+
path,
|
|
23
|
+
});
|
|
24
|
+
this.name = "RouteNotFoundError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when route parsing fails
|
|
29
|
+
*/
|
|
30
|
+
export class RouteParseError extends SchmockError {
|
|
31
|
+
constructor(routeKey, reason) {
|
|
32
|
+
super(`Invalid route key format: "${routeKey}". ${reason}`, "ROUTE_PARSE_ERROR", { routeKey, reason });
|
|
33
|
+
this.name = "RouteParseError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Error thrown when response generation fails
|
|
38
|
+
*/
|
|
39
|
+
export class ResponseGenerationError extends SchmockError {
|
|
40
|
+
constructor(route, error) {
|
|
41
|
+
super(`Failed to generate response for route ${route}: ${error.message}`, "RESPONSE_GENERATION_ERROR", { route, originalError: error });
|
|
42
|
+
this.name = "ResponseGenerationError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Error thrown when a plugin fails
|
|
47
|
+
*/
|
|
48
|
+
export class PluginError extends SchmockError {
|
|
49
|
+
constructor(pluginName, error) {
|
|
50
|
+
super(`Plugin "${pluginName}" failed: ${error.message}`, "PLUGIN_ERROR", {
|
|
51
|
+
pluginName,
|
|
52
|
+
originalError: error,
|
|
53
|
+
});
|
|
54
|
+
this.name = "PluginError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Error thrown when route definition is invalid
|
|
59
|
+
*/
|
|
60
|
+
export class RouteDefinitionError extends SchmockError {
|
|
61
|
+
constructor(routeKey, reason) {
|
|
62
|
+
super(`Invalid route definition for "${routeKey}": ${reason}`, "ROUTE_DEFINITION_ERROR", { routeKey, reason });
|
|
63
|
+
this.name = "RouteDefinitionError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Error thrown when schema validation fails
|
|
68
|
+
*/
|
|
69
|
+
export class SchemaValidationError extends SchmockError {
|
|
70
|
+
constructor(schemaPath, issue, suggestion) {
|
|
71
|
+
super(`Schema validation failed at ${schemaPath}: ${issue}${suggestion ? `. ${suggestion}` : ""}`, "SCHEMA_VALIDATION_ERROR", { schemaPath, issue, suggestion });
|
|
72
|
+
this.name = "SchemaValidationError";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Error thrown when schema generation fails
|
|
77
|
+
*/
|
|
78
|
+
export class SchemaGenerationError extends SchmockError {
|
|
79
|
+
constructor(route, error, schema) {
|
|
80
|
+
super(`Schema generation failed for route ${route}: ${error.message}`, "SCHEMA_GENERATION_ERROR", { route, originalError: error, schema });
|
|
81
|
+
this.name = "SchemaGenerationError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Error thrown when resource limits are exceeded
|
|
86
|
+
*/
|
|
87
|
+
export class ResourceLimitError extends SchmockError {
|
|
88
|
+
constructor(resource, limit, actual) {
|
|
89
|
+
super(`Resource limit exceeded for ${resource}: limit=${limit}${actual ? `, actual=${actual}` : ""}`, "RESOURCE_LIMIT_ERROR", { resource, limit, actual });
|
|
90
|
+
this.name = "ResourceLimitError";
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a new Schmock mock instance with callable API.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // New callable API (default)
|
|
7
|
+
* const mock = schmock({ debug: true })
|
|
8
|
+
* mock('GET /users', () => [{ id: 1, name: 'John' }])
|
|
9
|
+
* .pipe(authPlugin())
|
|
10
|
+
*
|
|
11
|
+
* const response = await mock.handle('GET', '/users')
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Simple usage with defaults
|
|
17
|
+
* const mock = schmock()
|
|
18
|
+
* mock('GET /users', [{ id: 1, name: 'John' }])
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param config Optional global configuration
|
|
22
|
+
* @returns A callable mock instance
|
|
23
|
+
*/
|
|
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";
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|