@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.
@@ -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.0",
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/node": "^25.2.3",
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
  }