@schmock/core 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder.d.ts +0 -40
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +65 -217
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/http-helpers.d.ts +4 -1
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +18 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin-pipeline.d.ts +15 -0
- package/dist/plugin-pipeline.d.ts.map +1 -0
- package/dist/plugin-pipeline.js +66 -0
- package/dist/response-parser.d.ts +6 -0
- package/dist/response-parser.d.ts.map +1 -0
- package/dist/response-parser.js +57 -0
- package/dist/route-matcher.d.ts +24 -0
- package/dist/route-matcher.d.ts.map +1 -0
- package/dist/route-matcher.js +39 -0
- package/package.json +3 -2
- package/src/builder.ts +83 -314
- package/src/constants.ts +9 -0
- package/src/errors.ts +4 -0
- package/src/http-helpers.ts +24 -2
- package/src/index.ts +1 -0
- package/src/plugin-pipeline.ts +100 -0
- package/src/response-parser.ts +74 -0
- package/src/route-matcher.ts +69 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { errorMessage, PluginError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run all registered plugins in sequence
|
|
4
|
+
* First plugin to set response becomes generator, subsequent plugins transform
|
|
5
|
+
* Handles plugin errors via onError hooks
|
|
6
|
+
*/
|
|
7
|
+
export async function runPluginPipeline(plugins, context, initialResponse, logger) {
|
|
8
|
+
let currentContext = context;
|
|
9
|
+
let response = initialResponse;
|
|
10
|
+
logger.log("pipeline", `Running plugin pipeline for ${plugins.length} plugins`);
|
|
11
|
+
for (const plugin of plugins) {
|
|
12
|
+
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
13
|
+
try {
|
|
14
|
+
const result = await plugin.process(currentContext, response);
|
|
15
|
+
if (!result || !result.context) {
|
|
16
|
+
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
17
|
+
}
|
|
18
|
+
currentContext = result.context;
|
|
19
|
+
// First plugin to set response becomes the generator
|
|
20
|
+
if (result.response !== undefined &&
|
|
21
|
+
(response === undefined || response === null)) {
|
|
22
|
+
logger.log("pipeline", `Plugin ${plugin.name} generated response`);
|
|
23
|
+
response = result.response;
|
|
24
|
+
}
|
|
25
|
+
else if (result.response !== undefined && response !== undefined) {
|
|
26
|
+
logger.log("pipeline", `Plugin ${plugin.name} transformed response`);
|
|
27
|
+
response = result.response;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.log("pipeline", `Plugin ${plugin.name} failed: ${errorMessage(error)}`);
|
|
32
|
+
// Try error handling if plugin has onError hook
|
|
33
|
+
if (plugin.onError) {
|
|
34
|
+
try {
|
|
35
|
+
const pluginError = error instanceof Error ? error : new Error(errorMessage(error));
|
|
36
|
+
const errorResult = await plugin.onError(pluginError, currentContext);
|
|
37
|
+
if (errorResult) {
|
|
38
|
+
logger.log("pipeline", `Plugin ${plugin.name} handled error`);
|
|
39
|
+
// Error return → transform the thrown error
|
|
40
|
+
if (errorResult instanceof Error) {
|
|
41
|
+
throw new PluginError(plugin.name, errorResult);
|
|
42
|
+
}
|
|
43
|
+
// ResponseResult return → recover, stop pipeline
|
|
44
|
+
if (typeof errorResult === "object" &&
|
|
45
|
+
errorResult !== null &&
|
|
46
|
+
"status" in errorResult) {
|
|
47
|
+
response = errorResult;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// void/falsy return → propagate original error below
|
|
52
|
+
}
|
|
53
|
+
catch (hookError) {
|
|
54
|
+
// If the hook itself threw (including our PluginError above), re-throw it
|
|
55
|
+
if (hookError instanceof PluginError) {
|
|
56
|
+
throw hookError;
|
|
57
|
+
}
|
|
58
|
+
logger.log("pipeline", `Plugin ${plugin.name} error handler failed: ${errorMessage(hookError)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const cause = error instanceof Error ? error : new Error(errorMessage(error));
|
|
62
|
+
throw new PluginError(plugin.name, cause);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { context: currentContext, response };
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse and normalize response result into Response object
|
|
3
|
+
* Handles tuple format [status, body, headers], direct values, and response objects
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseResponse(result: unknown, routeConfig: Schmock.RouteConfig): Schmock.Response;
|
|
6
|
+
//# sourceMappingURL=response-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-parser.d.ts","sourceRoot":"","sources":["../src/response-parser.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,WAAW,EAAE,OAAO,CAAC,WAAW,GAC/B,OAAO,CAAC,QAAQ,CAmDlB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isStatusTuple } from "./constants.js";
|
|
2
|
+
function isResponseObject(value) {
|
|
3
|
+
return (typeof value === "object" &&
|
|
4
|
+
value !== null &&
|
|
5
|
+
"status" in value &&
|
|
6
|
+
"body" in value);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse and normalize response result into Response object
|
|
10
|
+
* Handles tuple format [status, body, headers], direct values, and response objects
|
|
11
|
+
*/
|
|
12
|
+
export function parseResponse(result, routeConfig) {
|
|
13
|
+
let status = 200;
|
|
14
|
+
let body = result;
|
|
15
|
+
let headers = {};
|
|
16
|
+
let tupleFormat = false;
|
|
17
|
+
// Handle already-formed response objects (from plugin error recovery)
|
|
18
|
+
if (isResponseObject(result)) {
|
|
19
|
+
return {
|
|
20
|
+
status: result.status,
|
|
21
|
+
body: result.body,
|
|
22
|
+
headers: result.headers || {},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Handle tuple response format [status, body, headers?]
|
|
26
|
+
if (isStatusTuple(result)) {
|
|
27
|
+
[status, body, headers = {}] = result;
|
|
28
|
+
tupleFormat = true;
|
|
29
|
+
}
|
|
30
|
+
// Handle null/undefined responses with 204 No Content
|
|
31
|
+
// But don't auto-convert if tuple format was used (status was explicitly provided)
|
|
32
|
+
if (body === null || body === undefined) {
|
|
33
|
+
if (!tupleFormat) {
|
|
34
|
+
status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
|
|
35
|
+
}
|
|
36
|
+
body = undefined; // Ensure body is undefined for null responses
|
|
37
|
+
}
|
|
38
|
+
// Add content-type header from route config if it exists and headers don't already have it
|
|
39
|
+
// But only if this isn't a tuple response (where headers are explicitly controlled)
|
|
40
|
+
if (!headers["content-type"] && routeConfig.contentType && !tupleFormat) {
|
|
41
|
+
headers["content-type"] = routeConfig.contentType;
|
|
42
|
+
// Handle special conversion cases when contentType is explicitly set
|
|
43
|
+
if (routeConfig.contentType === "text/plain" && body !== undefined) {
|
|
44
|
+
if (typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
45
|
+
body = JSON.stringify(body);
|
|
46
|
+
}
|
|
47
|
+
else if (typeof body !== "string") {
|
|
48
|
+
body = String(body);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
status,
|
|
54
|
+
body,
|
|
55
|
+
headers,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled callable route with pattern matching
|
|
3
|
+
*/
|
|
4
|
+
export interface CompiledCallableRoute {
|
|
5
|
+
pattern: RegExp;
|
|
6
|
+
params: string[];
|
|
7
|
+
method: Schmock.HttpMethod;
|
|
8
|
+
path: string;
|
|
9
|
+
generator: Schmock.Generator;
|
|
10
|
+
config: Schmock.RouteConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare function isGeneratorFunction(gen: Schmock.Generator): gen is Schmock.GeneratorFunction;
|
|
13
|
+
/**
|
|
14
|
+
* Find a route that matches the given method and path
|
|
15
|
+
* Uses two-pass matching: static routes first, then parameterized routes
|
|
16
|
+
* Matches routes in registration order (first registered wins)
|
|
17
|
+
*/
|
|
18
|
+
export declare function findRoute(method: Schmock.HttpMethod, path: string, staticRoutes: Map<string, CompiledCallableRoute>, routes: CompiledCallableRoute[]): CompiledCallableRoute | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Extract parameter values from path based on route pattern
|
|
21
|
+
* Maps capture groups from regex match to parameter names
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractParams(route: CompiledCallableRoute, path: string): Record<string, string>;
|
|
24
|
+
//# sourceMappingURL=route-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { normalizePath } from "./constants.js";
|
|
2
|
+
export function isGeneratorFunction(gen) {
|
|
3
|
+
return typeof gen === "function";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Find a route that matches the given method and path
|
|
7
|
+
* Uses two-pass matching: static routes first, then parameterized routes
|
|
8
|
+
* Matches routes in registration order (first registered wins)
|
|
9
|
+
*/
|
|
10
|
+
export function findRoute(method, path, staticRoutes, routes) {
|
|
11
|
+
// O(1) lookup for static routes
|
|
12
|
+
const staticMatch = staticRoutes.get(`${method} ${normalizePath(path)}`);
|
|
13
|
+
if (staticMatch) {
|
|
14
|
+
return staticMatch;
|
|
15
|
+
}
|
|
16
|
+
// Fall through to parameterized route scan
|
|
17
|
+
for (const route of routes) {
|
|
18
|
+
if (route.method === method &&
|
|
19
|
+
route.params.length > 0 &&
|
|
20
|
+
route.pattern.test(path)) {
|
|
21
|
+
return route;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract parameter values from path based on route pattern
|
|
28
|
+
* Maps capture groups from regex match to parameter names
|
|
29
|
+
*/
|
|
30
|
+
export function extractParams(route, path) {
|
|
31
|
+
const match = path.match(route.pattern);
|
|
32
|
+
if (!match)
|
|
33
|
+
return {};
|
|
34
|
+
const params = {};
|
|
35
|
+
route.params.forEach((param, index) => {
|
|
36
|
+
params[param] = match[index + 1];
|
|
37
|
+
});
|
|
38
|
+
return params;
|
|
39
|
+
}
|
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.9.
|
|
4
|
+
"version": "1.9.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@amiceli/vitest-cucumber": "^6.2.0",
|
|
36
|
-
"@types/
|
|
36
|
+
"@types/json-schema": "^7.0.15",
|
|
37
|
+
"@types/node": "^25.3.0",
|
|
37
38
|
"@vitest/ui": "^4.0.18",
|
|
38
39
|
"vitest": "^4.0.18"
|
|
39
40
|
}
|