japa-openapi-assertions 0.1.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/README.md +240 -0
- package/build/coverage.d.ts +53 -0
- package/build/coverage.d.ts.map +1 -0
- package/build/coverage.js +123 -0
- package/build/coverage.js.map +1 -0
- package/build/index.d.ts +33 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +42 -0
- package/build/index.js.map +1 -0
- package/build/openapi_assertions.d.ts +49 -0
- package/build/openapi_assertions.d.ts.map +1 -0
- package/build/openapi_assertions.js +114 -0
- package/build/openapi_assertions.js.map +1 -0
- package/build/path_matcher.d.ts +18 -0
- package/build/path_matcher.d.ts.map +1 -0
- package/build/path_matcher.js +89 -0
- package/build/path_matcher.js.map +1 -0
- package/build/response_validator.d.ts +10 -0
- package/build/response_validator.d.ts.map +1 -0
- package/build/response_validator.js +173 -0
- package/build/response_validator.js.map +1 -0
- package/build/schema_builder.d.ts +22 -0
- package/build/schema_builder.d.ts.map +1 -0
- package/build/schema_builder.js +215 -0
- package/build/schema_builder.js.map +1 -0
- package/build/types.d.ts +126 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi_assertions.js","sourceRoot":"","sources":["../src/openapi_assertions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQ/C;;;;GAIG;AACH,MAAM,OAAO,iBAAiB;IAC5B;;OAEG;IACK,MAAM,CAAC,UAAU,GAA8B,IAAI,CAAA;IAE3D;;OAEG;IACK,MAAM,CAAC,aAAa,GAAY,KAAK,CAAA;IAE7C;;OAEG;IACK,MAAM,CAAC,eAAe,GAAoB,EAAE,CAAA;IAEpD;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAClB,iBAAmC,EACnC,UAA2B,EAAE;QAE7B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;QACzB,IAAI,CAAC,eAAe,GAAG,OAAO,CAAA;QAE9B,6BAA6B;QAC7B,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACxC,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACxC,CAAA;QAED,iCAAiC;QACjC,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAA;QACzC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;QAEnC,4BAA4B;QAC5B,IAAI,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YACrD,eAAe,CAAC,iBAAiB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;YACzD,eAAe,CAAC,eAAe,CAC7B,OAAO,CAAC,cAAc,IAAI,KAAK,EAC/B,OAAO,CAAC,cAAc,IAAI,KAAK,CAChC,CAAA;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,KAAK;QACV,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;QACtB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,IAAI,CAAC,eAAe,GAAG,EAAE,CAAA;IAC3B,CAAC;IAED;;OAEG;IACK,MAAM,CAAC,aAAa;QAC1B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,0DAA0D;kBACxD,2CAA2C,CAC9C,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAA;IACxB,CAAC;IAED;;;;;;OAMG;IACH,eAAe,CAAC,QAAiB;QAC/B,IAAI,CAAC,iBAAiB,CAAC,aAAa,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CACb,0DAA0D;kBACxD,2CAA2C,CAC9C,CAAA;QACH,CAAC;QAED,MAAM,UAAU,GAAG,iBAAiB,CAAC,aAAa,EAAE,CAAA;QACpD,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;QAErD,6BAA6B;QAC7B,IACE,iBAAiB,CAAC,eAAe,CAAC,cAAc;eAC7C,iBAAiB,CAAC,eAAe,CAAC,cAAc,EACnD,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;gBACtC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;gBACzC,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;gBAEnD,IAAI,SAAS,EAAE,CAAC;oBACd,eAAe,CAAC,cAAc,CAC5B,SAAS,CAAC,OAAO,EACjB,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CACtB,CAAA;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,kCAAkC;YACpC,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM;gBAChC,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;gBACV,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;gBACxD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAC7B,GAAG,IAAI,eAAe,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAA;gBACrD,CAAC;gBACD,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;oBAC3B,GAAG,IAAI,aAAa,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAA;gBACjD,CAAC;gBACD,OAAO,GAAG,CAAA;YACZ,CAAC,CAAC;iBACD,IAAI,CAAC,MAAM,CAAC,CAAA;YAEf,MAAM,IAAI,cAAc,CAAC;gBACvB,OAAO,EAAE,0CAA0C,YAAY,EAAE;gBACjE,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,wCAAwC;aACnD,CAAC,CAAA;QACJ,CAAC;IACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PathMatchResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Match a request path against OpenAPI path definitions.
|
|
4
|
+
*
|
|
5
|
+
* Handles path parameters (e.g., /pets/{petId} matches /pets/123)
|
|
6
|
+
* and scores matches by specificity (exact segments preferred over parameters).
|
|
7
|
+
*/
|
|
8
|
+
export declare function matchPath(requestPath: string, specPaths: string[]): PathMatchResult | null;
|
|
9
|
+
/**
|
|
10
|
+
* Get the base path from a server URL.
|
|
11
|
+
* E.g., "http://localhost:3333/api/v1" -> "/api/v1"
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractBasePath(serverUrl: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a path by removing base path prefix if present.
|
|
16
|
+
*/
|
|
17
|
+
export declare function removeBasePath(requestPath: string, basePath: string): string;
|
|
18
|
+
//# sourceMappingURL=path_matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path_matcher.d.ts","sourceRoot":"","sources":["../src/path_matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,GAClB,eAAe,GAAG,IAAI,CAwBxB;AA6CD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW5E"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match a request path against OpenAPI path definitions.
|
|
3
|
+
*
|
|
4
|
+
* Handles path parameters (e.g., /pets/{petId} matches /pets/123)
|
|
5
|
+
* and scores matches by specificity (exact segments preferred over parameters).
|
|
6
|
+
*/
|
|
7
|
+
export function matchPath(requestPath, specPaths) {
|
|
8
|
+
// Clean the request path: remove query string and trailing slash
|
|
9
|
+
const cleanPath = requestPath.split('?')[0].replace(/\/$/, '') || '/';
|
|
10
|
+
const matches = [];
|
|
11
|
+
for (const specPath of specPaths) {
|
|
12
|
+
const result = tryMatch(cleanPath, specPath);
|
|
13
|
+
if (result) {
|
|
14
|
+
matches.push(result);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (matches.length === 0) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Sort by score (higher = more specific = fewer parameters)
|
|
21
|
+
matches.sort((a, b) => b.score - a.score);
|
|
22
|
+
return {
|
|
23
|
+
matched: matches[0].specPath,
|
|
24
|
+
params: matches[0].params,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Try to match a request path against a single spec path.
|
|
29
|
+
* Returns match result with score, or null if no match.
|
|
30
|
+
*/
|
|
31
|
+
function tryMatch(requestPath, specPath) {
|
|
32
|
+
const requestSegments = requestPath.split('/').filter(Boolean);
|
|
33
|
+
const specSegments = specPath.split('/').filter(Boolean);
|
|
34
|
+
// Must have same number of segments
|
|
35
|
+
if (requestSegments.length !== specSegments.length) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const params = {};
|
|
39
|
+
let score = 0;
|
|
40
|
+
for (let i = 0; i < specSegments.length; i++) {
|
|
41
|
+
const specSegment = specSegments[i];
|
|
42
|
+
const requestSegment = requestSegments[i];
|
|
43
|
+
// Check if it's a path parameter
|
|
44
|
+
const paramMatch = specSegment.match(/^\{(\w+)\}$/);
|
|
45
|
+
if (paramMatch) {
|
|
46
|
+
// It's a parameter - extract the value
|
|
47
|
+
params[paramMatch[1]] = requestSegment;
|
|
48
|
+
// Parameters score lower than exact matches
|
|
49
|
+
score += 1;
|
|
50
|
+
}
|
|
51
|
+
else if (specSegment === requestSegment) {
|
|
52
|
+
// Exact match - higher score
|
|
53
|
+
score += 10;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// No match
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { specPath, params, score };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the base path from a server URL.
|
|
64
|
+
* E.g., "http://localhost:3333/api/v1" -> "/api/v1"
|
|
65
|
+
*/
|
|
66
|
+
export function extractBasePath(serverUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(serverUrl);
|
|
69
|
+
return url.pathname.replace(/\/$/, '') || '/';
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// If it's not a valid URL, assume it's already a path
|
|
73
|
+
return serverUrl.replace(/\/$/, '') || '/';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Normalize a path by removing base path prefix if present.
|
|
78
|
+
*/
|
|
79
|
+
export function removeBasePath(requestPath, basePath) {
|
|
80
|
+
if (basePath === '/' || basePath === '') {
|
|
81
|
+
return requestPath;
|
|
82
|
+
}
|
|
83
|
+
if (requestPath.startsWith(basePath)) {
|
|
84
|
+
const remaining = requestPath.slice(basePath.length);
|
|
85
|
+
return remaining.startsWith('/') ? remaining : `/${remaining}`;
|
|
86
|
+
}
|
|
87
|
+
return requestPath;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=path_matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path_matcher.js","sourceRoot":"","sources":["../src/path_matcher.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,WAAmB,EACnB,SAAmB;IAEnB,iEAAiE;IACjE,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;IAErE,MAAM,OAAO,GAA+E,EAAE,CAAA;IAE9F,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QAC5C,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,4DAA4D;IAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;IAEzC,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ;QAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM;KAC1B,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CACf,WAAmB,EACnB,QAAgB;IAEhB,MAAM,eAAe,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC9D,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAExD,oCAAoC;IACpC,IAAI,eAAe,CAAC,MAAM,KAAK,YAAY,CAAC,MAAM,EAAE,CAAC;QACnD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAA2B,EAAE,CAAA;IACzC,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;QACnC,MAAM,cAAc,GAAG,eAAe,CAAC,CAAC,CAAC,CAAA;QAEzC,iCAAiC;QACjC,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;QAEnD,IAAI,UAAU,EAAE,CAAC;YACf,uCAAuC;YACvC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc,CAAA;YACtC,4CAA4C;YAC5C,KAAK,IAAI,CAAC,CAAA;QACZ,CAAC;aAAM,IAAI,WAAW,KAAK,cAAc,EAAE,CAAC;YAC1C,6BAA6B;YAC7B,KAAK,IAAI,EAAE,CAAA;QACb,CAAC;aAAM,CAAC;YACN,WAAW;YACX,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QAC9B,OAAO,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;QACtD,OAAO,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;IAC5C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB,EAAE,QAAgB;IAClE,IAAI,QAAQ,KAAK,GAAG,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QACxC,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,IAAI,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;QACpD,OAAO,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,EAAE,CAAA;IAChE,CAAC;IAED,OAAO,WAAW,CAAA;AACpB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompiledValidators, ParsedResponse, ValidationResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate an HTTP response against compiled OpenAPI validators.
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateResponse(response: unknown, validators: CompiledValidators): ValidationResult;
|
|
6
|
+
/**
|
|
7
|
+
* Parse response from various HTTP client formats.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseResponse(response: unknown): ParsedResponse;
|
|
10
|
+
//# sourceMappingURL=response_validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response_validator.d.ts","sourceRoot":"","sources":["../src/response_validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAEjB,MAAM,YAAY,CAAA;AAGnB;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,kBAAkB,GAC7B,gBAAgB,CAyElB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,cAAc,CA8D/D"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { matchPath } from './path_matcher.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate an HTTP response against compiled OpenAPI validators.
|
|
4
|
+
*/
|
|
5
|
+
export function validateResponse(response, validators) {
|
|
6
|
+
// Parse the response from various HTTP client formats
|
|
7
|
+
const parsed = parseResponse(response);
|
|
8
|
+
// Find matching path in spec
|
|
9
|
+
const specPaths = Object.keys(validators);
|
|
10
|
+
const pathMatch = matchPath(parsed.path, specPaths);
|
|
11
|
+
if (!pathMatch) {
|
|
12
|
+
return {
|
|
13
|
+
valid: false,
|
|
14
|
+
errors: [{
|
|
15
|
+
path: '',
|
|
16
|
+
message: `No matching path found in OpenAPI spec for ${parsed.method} ${parsed.path}`,
|
|
17
|
+
actual: parsed.path,
|
|
18
|
+
expected: specPaths,
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const method = parsed.method.toLowerCase();
|
|
23
|
+
const statusCode = String(parsed.status);
|
|
24
|
+
// Get validators for this path/method
|
|
25
|
+
const pathValidators = validators[pathMatch.matched]?.[method];
|
|
26
|
+
if (!pathValidators) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
errors: [{
|
|
30
|
+
path: '',
|
|
31
|
+
message: `No ${parsed.method} operation defined for ${pathMatch.matched}`,
|
|
32
|
+
actual: method,
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Get validator for this status code
|
|
37
|
+
// Try exact match first, then 'default', then 2XX/4XX/5XX patterns
|
|
38
|
+
const responseValidator = pathValidators.responses[statusCode]
|
|
39
|
+
|| pathValidators.responses['default']
|
|
40
|
+
|| pathValidators.responses[`${statusCode[0]}XX`];
|
|
41
|
+
if (!responseValidator) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
errors: [{
|
|
45
|
+
path: '',
|
|
46
|
+
message: `No response schema defined for ${parsed.method} ${pathMatch.matched} with status ${statusCode}`,
|
|
47
|
+
actual: statusCode,
|
|
48
|
+
expected: Object.keys(pathValidators.responses),
|
|
49
|
+
}],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Validate response body if there's a body validator
|
|
53
|
+
if (responseValidator.body) {
|
|
54
|
+
const valid = responseValidator.body(parsed.body);
|
|
55
|
+
if (!valid && responseValidator.body.errors) {
|
|
56
|
+
const errors = responseValidator.body.errors.map((err) => ({
|
|
57
|
+
path: err.instancePath || '',
|
|
58
|
+
message: err.message || 'Validation failed',
|
|
59
|
+
keyword: err.keyword,
|
|
60
|
+
expected: err.params,
|
|
61
|
+
actual: getValueAtPath(parsed.body, err.instancePath || ''),
|
|
62
|
+
}));
|
|
63
|
+
return { valid: false, errors };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { valid: true };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse response from various HTTP client formats.
|
|
70
|
+
*/
|
|
71
|
+
export function parseResponse(response) {
|
|
72
|
+
if (!response || typeof response !== 'object') {
|
|
73
|
+
throw new Error('Invalid response: expected an object');
|
|
74
|
+
}
|
|
75
|
+
const res = response;
|
|
76
|
+
// Japa @japa/api-client format
|
|
77
|
+
// The response object itself has the properties we need
|
|
78
|
+
if ('response' in res && res.response && typeof res.response === 'object') {
|
|
79
|
+
// This is the ApiResponse wrapper, unwrap it
|
|
80
|
+
return parseResponse(res.response);
|
|
81
|
+
}
|
|
82
|
+
// Japa api-client direct response (has .request and .body())
|
|
83
|
+
if ('request' in res && 'statusCode' in res) {
|
|
84
|
+
const request = res.request;
|
|
85
|
+
return {
|
|
86
|
+
method: String(request.method || 'GET').toUpperCase(),
|
|
87
|
+
path: extractPath(request.url || '/'),
|
|
88
|
+
status: Number(res.statusCode) || 0,
|
|
89
|
+
headers: normalizeHeaders(res.headers || {}),
|
|
90
|
+
body: typeof res.body === 'function' ? res.body() : res.body,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Axios format (has .data, .config, .status)
|
|
94
|
+
if ('data' in res && 'config' in res && 'status' in res) {
|
|
95
|
+
const config = res.config;
|
|
96
|
+
return {
|
|
97
|
+
method: String(config.method || 'GET').toUpperCase(),
|
|
98
|
+
path: extractPath(config.url || '/'),
|
|
99
|
+
status: Number(res.status) || 0,
|
|
100
|
+
headers: normalizeHeaders(res.headers || {}),
|
|
101
|
+
body: res.data,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Supertest format (has .body, .req, .statusCode)
|
|
105
|
+
if ('body' in res && 'req' in res && 'statusCode' in res) {
|
|
106
|
+
const req = res.req;
|
|
107
|
+
return {
|
|
108
|
+
method: String(req.method || 'GET').toUpperCase(),
|
|
109
|
+
path: extractPath(req.path || '/'),
|
|
110
|
+
status: Number(res.statusCode) || 0,
|
|
111
|
+
headers: normalizeHeaders(res.headers || {}),
|
|
112
|
+
body: res.body,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Generic format - try to extract what we can
|
|
116
|
+
if ('status' in res || 'statusCode' in res) {
|
|
117
|
+
return {
|
|
118
|
+
method: String(res.method || 'GET').toUpperCase(),
|
|
119
|
+
path: extractPath(res.url || res.path || '/'),
|
|
120
|
+
status: Number(res.status || res.statusCode) || 0,
|
|
121
|
+
headers: normalizeHeaders(res.headers || {}),
|
|
122
|
+
body: res.body ?? res.data ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Unknown response format: could not extract method, path, status, or body');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Extract path from a URL string.
|
|
129
|
+
*/
|
|
130
|
+
function extractPath(url) {
|
|
131
|
+
try {
|
|
132
|
+
// If it's a full URL, parse it
|
|
133
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
134
|
+
const parsed = new URL(url);
|
|
135
|
+
return parsed.pathname;
|
|
136
|
+
}
|
|
137
|
+
// Otherwise, split on query string
|
|
138
|
+
return url.split('?')[0];
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return url.split('?')[0];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Normalize headers to lowercase keys with string values.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeHeaders(headers) {
|
|
148
|
+
const result = {};
|
|
149
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
150
|
+
result[key.toLowerCase()] = String(value);
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get value at a JSON path.
|
|
156
|
+
*/
|
|
157
|
+
function getValueAtPath(obj, path) {
|
|
158
|
+
if (!path || path === '')
|
|
159
|
+
return obj;
|
|
160
|
+
if (!obj || typeof obj !== 'object')
|
|
161
|
+
return undefined;
|
|
162
|
+
const parts = path.split('/').filter(Boolean);
|
|
163
|
+
let current = obj;
|
|
164
|
+
for (const part of parts) {
|
|
165
|
+
if (current === null || current === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
if (typeof current !== 'object')
|
|
168
|
+
return undefined;
|
|
169
|
+
current = current[part];
|
|
170
|
+
}
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=response_validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response_validator.js","sourceRoot":"","sources":["../src/response_validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CompiledValidators, CoverageEntry } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build validators from OpenAPI specification files (async version).
|
|
4
|
+
*
|
|
5
|
+
* @param specPaths - Paths to OpenAPI spec files (JSON or YAML)
|
|
6
|
+
* @returns Compiled validators and coverage entries
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildValidators(specPaths: (string | URL)[]): Promise<{
|
|
9
|
+
validators: CompiledValidators;
|
|
10
|
+
coverageEntries: CoverageEntry[];
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Build validators from OpenAPI specification files (sync version).
|
|
14
|
+
*
|
|
15
|
+
* @param specPaths - Paths to OpenAPI spec files (JSON or YAML)
|
|
16
|
+
* @returns Compiled validators and coverage entries
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildValidatorsSync(specPaths: (string | URL)[]): {
|
|
19
|
+
validators: CompiledValidators;
|
|
20
|
+
coverageEntries: CoverageEntry[];
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=schema_builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema_builder.d.ts","sourceRoot":"","sources":["../src/schema_builder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAGlB,aAAa,EACd,MAAM,YAAY,CAAA;AAUnB;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,GAC1B,OAAO,CAAC;IAAE,UAAU,EAAE,kBAAkB,CAAC;IAAC,eAAe,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC,CAG/E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,GAC1B;IAAE,UAAU,EAAE,kBAAkB,CAAC;IAAC,eAAe,EAAE,aAAa,EAAE,CAAA;CAAE,CAyBtE"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import AjvModule from 'ajv/dist/2020.js';
|
|
2
|
+
import addFormatsModule from 'ajv-formats';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
// Handle ESM/CJS interop - get the actual constructor/function
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
const Ajv2020 = AjvModule.default ?? AjvModule;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
10
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
|
|
11
|
+
/**
|
|
12
|
+
* Build validators from OpenAPI specification files (async version).
|
|
13
|
+
*
|
|
14
|
+
* @param specPaths - Paths to OpenAPI spec files (JSON or YAML)
|
|
15
|
+
* @returns Compiled validators and coverage entries
|
|
16
|
+
*/
|
|
17
|
+
export async function buildValidators(specPaths) {
|
|
18
|
+
// Delegate to sync version - the async wrapper is kept for API compatibility
|
|
19
|
+
return buildValidatorsSync(specPaths);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build validators from OpenAPI specification files (sync version).
|
|
23
|
+
*
|
|
24
|
+
* @param specPaths - Paths to OpenAPI spec files (JSON or YAML)
|
|
25
|
+
* @returns Compiled validators and coverage entries
|
|
26
|
+
*/
|
|
27
|
+
export function buildValidatorsSync(specPaths) {
|
|
28
|
+
const validators = {};
|
|
29
|
+
const coverageEntries = [];
|
|
30
|
+
// Create AJV instance with JSON Schema 2020-12 support
|
|
31
|
+
const ajv = new Ajv2020({
|
|
32
|
+
allErrors: true,
|
|
33
|
+
strict: false,
|
|
34
|
+
validateFormats: true,
|
|
35
|
+
});
|
|
36
|
+
addFormats(ajv);
|
|
37
|
+
// Process each spec file
|
|
38
|
+
for (const specPath of specPaths) {
|
|
39
|
+
const filePath = specPath instanceof URL ? fileURLToPath(specPath) : specPath;
|
|
40
|
+
const spec = loadAndValidateSpecSync(filePath);
|
|
41
|
+
// Resolve all $refs in the spec
|
|
42
|
+
const resolvedSpec = resolveRefsSync(spec);
|
|
43
|
+
// Build validators for each path
|
|
44
|
+
buildPathValidators(resolvedSpec, validators, coverageEntries, ajv);
|
|
45
|
+
}
|
|
46
|
+
return { validators, coverageEntries };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load and validate an OpenAPI specification file (sync version).
|
|
50
|
+
*/
|
|
51
|
+
function loadAndValidateSpecSync(filePath) {
|
|
52
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
53
|
+
// Parse JSON (YAML support could be added with js-yaml)
|
|
54
|
+
let spec;
|
|
55
|
+
try {
|
|
56
|
+
spec = JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`Failed to parse OpenAPI spec at ${filePath}: Invalid JSON`);
|
|
60
|
+
}
|
|
61
|
+
// Note: We skip async validation here since the Validator.validate() is async
|
|
62
|
+
// and we need synchronous operation. The spec will still be validated when
|
|
63
|
+
// resolving refs, and AJV will catch schema issues during compilation.
|
|
64
|
+
return spec;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve all $ref pointers in the spec (sync version).
|
|
68
|
+
* Uses a workaround since the Validator is async.
|
|
69
|
+
*/
|
|
70
|
+
function resolveRefsSync(spec) {
|
|
71
|
+
// For synchronous operation, we'll resolve refs manually
|
|
72
|
+
// by recursively resolving $ref pointers within the spec
|
|
73
|
+
return resolveRefsInObject(spec, spec);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Recursively resolve $ref pointers in an object.
|
|
77
|
+
*/
|
|
78
|
+
function resolveRefsInObject(obj, root) {
|
|
79
|
+
if (!obj || typeof obj !== 'object')
|
|
80
|
+
return obj;
|
|
81
|
+
if (Array.isArray(obj)) {
|
|
82
|
+
return obj.map((item) => resolveRefsInObject(item, root));
|
|
83
|
+
}
|
|
84
|
+
const record = obj;
|
|
85
|
+
// Check for $ref
|
|
86
|
+
if (typeof record.$ref === 'string') {
|
|
87
|
+
const ref = record.$ref;
|
|
88
|
+
// Only handle local refs (starting with #/)
|
|
89
|
+
if (ref.startsWith('#/')) {
|
|
90
|
+
const resolved = resolveLocalRef(ref, root);
|
|
91
|
+
if (resolved !== undefined) {
|
|
92
|
+
// Merge resolved ref with any additional properties (except $ref)
|
|
93
|
+
const { $ref: _ref, ...rest } = record;
|
|
94
|
+
if (Object.keys(rest).length > 0) {
|
|
95
|
+
return { ...resolveRefsInObject(resolved, root), ...rest };
|
|
96
|
+
}
|
|
97
|
+
return resolveRefsInObject(resolved, root);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Recursively process all properties
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [key, value] of Object.entries(record)) {
|
|
104
|
+
result[key] = resolveRefsInObject(value, root);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a local JSON pointer reference.
|
|
110
|
+
*/
|
|
111
|
+
function resolveLocalRef(ref, root) {
|
|
112
|
+
// Remove the #/ prefix and split into path segments
|
|
113
|
+
const path = ref.slice(2).split('/');
|
|
114
|
+
let current = root;
|
|
115
|
+
for (const segment of path) {
|
|
116
|
+
// Decode JSON pointer escapes
|
|
117
|
+
const decoded = segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
118
|
+
if (current && typeof current === 'object' && decoded in current) {
|
|
119
|
+
current = current[decoded];
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build validators for all paths in a spec.
|
|
129
|
+
*/
|
|
130
|
+
function buildPathValidators(spec, validators, coverageEntries,
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
ajv) {
|
|
133
|
+
if (!spec.paths) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
137
|
+
if (!pathItem)
|
|
138
|
+
continue;
|
|
139
|
+
validators[path] = validators[path] || {};
|
|
140
|
+
for (const method of HTTP_METHODS) {
|
|
141
|
+
const operation = pathItem[method];
|
|
142
|
+
if (!operation?.responses)
|
|
143
|
+
continue;
|
|
144
|
+
validators[path][method] = { responses: {} };
|
|
145
|
+
const statuses = [];
|
|
146
|
+
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
147
|
+
statuses.push(statusCode);
|
|
148
|
+
// Build body validator if there's a JSON schema
|
|
149
|
+
const jsonContent = response.content?.['application/json'];
|
|
150
|
+
if (jsonContent?.schema) {
|
|
151
|
+
try {
|
|
152
|
+
const bodyValidator = compileSchema(ajv, jsonContent.schema, `${path}:${method}:${statusCode}:body`);
|
|
153
|
+
validators[path][method].responses[statusCode] = {
|
|
154
|
+
body: bodyValidator,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.warn(`Warning: Failed to compile schema for ${method.toUpperCase()} ${path} ${statusCode}:`, err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
validators[path][method].responses[statusCode] = {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Track for coverage
|
|
166
|
+
if (statuses.length > 0) {
|
|
167
|
+
coverageEntries.push({
|
|
168
|
+
route: path,
|
|
169
|
+
method: method.toUpperCase(),
|
|
170
|
+
statuses,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Compile a JSON schema into a validator function.
|
|
178
|
+
*/
|
|
179
|
+
function compileSchema(
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
ajv, schema, _schemaId) {
|
|
182
|
+
// Clone the schema to avoid mutating the original
|
|
183
|
+
const schemaCopy = JSON.parse(JSON.stringify(schema));
|
|
184
|
+
// Remove all $id fields recursively - resolved refs may have invalid patterns
|
|
185
|
+
// AJV 2020-12 is strict about $id format - must match "^[^#]*#?$"
|
|
186
|
+
removeInvalidIds(schemaCopy);
|
|
187
|
+
return ajv.compile(schemaCopy);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Recursively remove $id fields that don't match JSON Schema 2020-12 pattern.
|
|
191
|
+
*/
|
|
192
|
+
function removeInvalidIds(obj) {
|
|
193
|
+
if (!obj || typeof obj !== 'object')
|
|
194
|
+
return;
|
|
195
|
+
if (Array.isArray(obj)) {
|
|
196
|
+
for (const item of obj) {
|
|
197
|
+
removeInvalidIds(item);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const record = obj;
|
|
202
|
+
// Remove $id if it contains # anywhere except at the very end
|
|
203
|
+
if (typeof record.$id === 'string') {
|
|
204
|
+
const id = record.$id;
|
|
205
|
+
// Valid pattern: ^[^#]*#?$ (no # except optionally at the end)
|
|
206
|
+
if (id.includes('#') && !id.match(/^[^#]*#?$/)) {
|
|
207
|
+
delete record.$id;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Recurse into all properties
|
|
211
|
+
for (const value of Object.values(record)) {
|
|
212
|
+
removeInvalidIds(value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=schema_builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema_builder.js","sourceRoot":"","sources":["../src/schema_builder.ts"],"names":[],"mappings":"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"}
|