@zudojs/openapi 1.3.0 → 1.5.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 +83 -2
- package/dist/index.d.ts +10 -6
- package/dist/index.js +8 -4
- package/dist/openApiConstants/index.d.ts +1 -1
- package/dist/openApiConstants/index.js +1 -1
- package/dist/openApiConstants/openApiConstants.core.d.ts +5 -0
- package/dist/openApiConstants/openApiConstants.core.js +5 -0
- package/dist/openApiFromRoutes/index.d.ts +11 -0
- package/dist/openApiFromRoutes/index.js +10 -0
- package/dist/openApiFromRoutes/openApiFromRoutes.document.d.ts +43 -0
- package/dist/openApiFromRoutes/openApiFromRoutes.document.js +64 -0
- package/dist/openApiFromRoutes/openApiFromRoutes.type.d.ts +56 -0
- package/dist/openApiFromRoutes/openApiFromRoutes.type.js +5 -0
- package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +28 -1
- package/dist/openApiHttp/openApiHttpAdapter.core.js +40 -1
- package/dist/openApiRouting/index.d.ts +3 -1
- package/dist/openApiRouting/index.js +2 -0
- package/dist/openApiRouting/routeContent.core.d.ts +22 -0
- package/dist/openApiRouting/routeContent.core.js +101 -0
- package/dist/openApiRouting/routeConverter.core.d.ts +11 -5
- package/dist/openApiRouting/routeConverter.core.js +23 -28
- package/dist/openApiRouting/routeMetadata.type.d.ts +36 -2
- package/dist/openApiRouting/routeScanner.core.d.ts +7 -3
- package/dist/openApiRouting/routeScanner.core.js +46 -10
- package/dist/openApiRouting/routeSchema.core.d.ts +19 -0
- package/dist/openApiRouting/routeSchema.core.js +93 -0
- package/dist/openApiSchema/index.d.ts +1 -0
- package/dist/openApiSchema/index.js +1 -0
- package/dist/openApiSchema/schemaConverter.core.d.ts +2 -0
- package/dist/openApiSchema/schemaConverter.core.js +9 -4
- package/dist/openApiSchema/schemaInput.core.d.ts +28 -0
- package/dist/openApiSchema/schemaInput.core.js +36 -0
- package/dist/openApiSchema/schemaInput.type.d.ts +48 -0
- package/dist/openApiSchema/schemaInput.type.js +5 -0
- package/package.json +7 -6
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request body and response construction for route conversion.
|
|
3
|
+
*
|
|
4
|
+
* Both accept schemas as well as raw OpenAPI objects, so a route that
|
|
5
|
+
* declares `body: userSchema` documents exactly the payload it parses.
|
|
6
|
+
*/
|
|
7
|
+
import { HttpStatus } from "@zudojs/constants";
|
|
8
|
+
import { isSchemaDefinition, resolveSchemaInput, } from "../openApiSchema/schemaInput.core.js";
|
|
9
|
+
import { DEFAULT_MEDIA_TYPE, UNDOCUMENTED_RESPONSE_DESCRIPTION } from "../openApiConstants/openApiConstants.core.js";
|
|
10
|
+
const REASON_PHRASES = new Map(Object.entries(HttpStatus).map(([name, code]) => [
|
|
11
|
+
String(code),
|
|
12
|
+
name
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split("_")
|
|
15
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
16
|
+
.join(" "),
|
|
17
|
+
]));
|
|
18
|
+
const RANGE_DESCRIPTIONS = {
|
|
19
|
+
"1XX": "Informational",
|
|
20
|
+
"2XX": "Success",
|
|
21
|
+
"3XX": "Redirection",
|
|
22
|
+
"4XX": "Client error",
|
|
23
|
+
"5XX": "Server error",
|
|
24
|
+
default: "Unexpected response",
|
|
25
|
+
};
|
|
26
|
+
/** A default description for a response key: `404` → "Not Found". */
|
|
27
|
+
export function describeResponseKey(key) {
|
|
28
|
+
return REASON_PHRASES.get(key) ?? RANGE_DESCRIPTIONS[key] ?? `Response ${key}`;
|
|
29
|
+
}
|
|
30
|
+
function isDescriptorWithSchema(value) {
|
|
31
|
+
return (typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
!isSchemaDefinition(value) &&
|
|
34
|
+
Object.hasOwn(value, "schema"));
|
|
35
|
+
}
|
|
36
|
+
function mediaTypes(contentType, media) {
|
|
37
|
+
const types = contentType === undefined
|
|
38
|
+
? [DEFAULT_MEDIA_TYPE]
|
|
39
|
+
: typeof contentType === "string" ? [contentType] : contentType;
|
|
40
|
+
return Object.fromEntries(types.map((type) => [type, media]));
|
|
41
|
+
}
|
|
42
|
+
/** Builds the Request Body Object from `requestBody` or `body`. */
|
|
43
|
+
export function buildOperationRequestBody(meta, context, options = {}) {
|
|
44
|
+
if (meta?.requestBody)
|
|
45
|
+
return meta.requestBody;
|
|
46
|
+
if (meta?.body === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const body = isDescriptorWithSchema(meta.body)
|
|
49
|
+
? meta.body
|
|
50
|
+
: { schema: meta.body };
|
|
51
|
+
const schema = resolveSchemaInput(body.schema, `${context} body`, options);
|
|
52
|
+
return {
|
|
53
|
+
...(body.description ? { description: body.description } : {}),
|
|
54
|
+
required: body.required ?? true,
|
|
55
|
+
content: mediaTypes(body.contentType, {
|
|
56
|
+
schema,
|
|
57
|
+
...(body.example !== undefined ? { example: body.example } : {}),
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function toResponse(key, entry, context, options) {
|
|
62
|
+
if (!isDescriptorWithSchema(entry))
|
|
63
|
+
return entry;
|
|
64
|
+
const response = entry;
|
|
65
|
+
const schema = resolveSchemaInput(response.schema, `${context} response ${key}`, options);
|
|
66
|
+
return {
|
|
67
|
+
description: response.description ?? describeResponseKey(key),
|
|
68
|
+
...(response.headers ? { headers: response.headers } : {}),
|
|
69
|
+
content: mediaTypes(response.contentType, {
|
|
70
|
+
schema,
|
|
71
|
+
...(response.example !== undefined ? { example: response.example } : {}),
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds the `responses` object for an operation.
|
|
77
|
+
*
|
|
78
|
+
* Every documented response is carried through; an entry with a `schema` is
|
|
79
|
+
* expanded into a Response Object. A route that documents none gets a
|
|
80
|
+
* `default` "Undocumented response" (`responses` is required) and a warning
|
|
81
|
+
* through `options.onWarning`. No status is invented.
|
|
82
|
+
*/
|
|
83
|
+
export function buildOperationResponses(meta, context, options = {}) {
|
|
84
|
+
const declared = meta?.responses;
|
|
85
|
+
if (!declared || Object.keys(declared).length === 0) {
|
|
86
|
+
options.onWarning?.(`${context}: no responses are documented; emitted "default: ${UNDOCUMENTED_RESPONSE_DESCRIPTION}".`);
|
|
87
|
+
const fallback = { description: UNDOCUMENTED_RESPONSE_DESCRIPTION };
|
|
88
|
+
return Object.freeze({ default: Object.freeze(fallback) });
|
|
89
|
+
}
|
|
90
|
+
const responses = {};
|
|
91
|
+
for (const [key, entry] of Object.entries(declared)) {
|
|
92
|
+
Object.defineProperty(responses, key, {
|
|
93
|
+
value: toResponse(key, entry, context, options),
|
|
94
|
+
enumerable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return Object.freeze(responses);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=routeContent.core.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenAPIOperation, OpenAPIResponses } from "../openApiTypes/openApiTypes.core.js";
|
|
2
|
-
import type { OpenAPIHttpMethod, RouteMetadata } from "./routeMetadata.type.js";
|
|
2
|
+
import type { OpenAPIHttpMethod, RouteConversionOptions, RouteMetadata } from "./routeMetadata.type.js";
|
|
3
3
|
/**
|
|
4
4
|
* Maps Zudojs HTTP methods to OpenAPI methods.
|
|
5
5
|
*/
|
|
@@ -26,14 +26,20 @@ export declare function extractPathParameters(path: string): readonly string[];
|
|
|
26
26
|
/**
|
|
27
27
|
* Builds the `responses` object for an operation.
|
|
28
28
|
*
|
|
29
|
-
* Every documented response is carried through.
|
|
30
|
-
*
|
|
29
|
+
* Every documented response is carried through. A route that documents none
|
|
30
|
+
* gets a `default` "Undocumented response" and a warning through
|
|
31
|
+
* `options.onWarning`; no `200` is invented.
|
|
31
32
|
*/
|
|
32
|
-
export declare function buildResponses(metadata?: RouteMetadata): OpenAPIResponses;
|
|
33
|
+
export declare function buildResponses(metadata?: RouteMetadata, options?: RouteConversionOptions): OpenAPIResponses;
|
|
33
34
|
/**
|
|
34
35
|
* Converts a route with metadata into an OpenAPI operation.
|
|
36
|
+
*
|
|
37
|
+
* Every path template slot is documented as a required path parameter even
|
|
38
|
+
* when the route declares none; schemas declared as `params`, `query`,
|
|
39
|
+
* `headers`, `cookies`, `body` or response `schema` are converted for
|
|
40
|
+
* `options.version`.
|
|
35
41
|
*/
|
|
36
|
-
export declare function convertRouteToOpenAPI(method: string, path: string, metadata?: RouteMetadata): {
|
|
42
|
+
export declare function convertRouteToOpenAPI(method: string, path: string, metadata?: RouteMetadata, options?: RouteConversionOptions): {
|
|
37
43
|
method: OpenAPIHttpMethod;
|
|
38
44
|
path: string;
|
|
39
45
|
operation: OpenAPIOperation;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildOperationParameters } from "./routeSchema.core.js";
|
|
2
|
+
import { buildOperationRequestBody, buildOperationResponses, } from "./routeContent.core.js";
|
|
1
3
|
import { OpenAPIRouteError } from "../openApiErrors/openApiError.types.js";
|
|
2
4
|
import { PATH_TEMPLATE_PARAMETER } from "../openApiConstants/openApiConstants.core.js";
|
|
3
5
|
/**
|
|
@@ -71,56 +73,49 @@ export function extractPathParameters(path) {
|
|
|
71
73
|
}
|
|
72
74
|
return names;
|
|
73
75
|
}
|
|
74
|
-
function toParameter(parameter) {
|
|
75
|
-
return {
|
|
76
|
-
name: parameter.name,
|
|
77
|
-
in: parameter.in,
|
|
78
|
-
...(parameter.description ? { description: parameter.description } : {}),
|
|
79
|
-
// A path parameter is required by the specification, so declaring one
|
|
80
|
-
// that is not required is a document that cannot validate.
|
|
81
|
-
required: parameter.in === "path" ? true : (parameter.required ?? false),
|
|
82
|
-
...(parameter.deprecated ? { deprecated: true } : {}),
|
|
83
|
-
...(parameter.schema !== undefined ? { schema: parameter.schema } : {}),
|
|
84
|
-
...(parameter.example !== undefined ? { example: parameter.example } : {}),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
76
|
/**
|
|
88
77
|
* Builds the `responses` object for an operation.
|
|
89
78
|
*
|
|
90
|
-
* Every documented response is carried through.
|
|
91
|
-
*
|
|
79
|
+
* Every documented response is carried through. A route that documents none
|
|
80
|
+
* gets a `default` "Undocumented response" and a warning through
|
|
81
|
+
* `options.onWarning`; no `200` is invented.
|
|
92
82
|
*/
|
|
93
|
-
export function buildResponses(metadata) {
|
|
94
|
-
|
|
95
|
-
if (declared && Object.keys(declared).length > 0) {
|
|
96
|
-
return Object.freeze({ ...declared });
|
|
97
|
-
}
|
|
98
|
-
return Object.freeze({ "200": { description: "OK" } });
|
|
83
|
+
export function buildResponses(metadata, options) {
|
|
84
|
+
return buildOperationResponses(metadata?.openapi, "responses", options);
|
|
99
85
|
}
|
|
100
86
|
/**
|
|
101
87
|
* Converts a route with metadata into an OpenAPI operation.
|
|
88
|
+
*
|
|
89
|
+
* Every path template slot is documented as a required path parameter even
|
|
90
|
+
* when the route declares none; schemas declared as `params`, `query`,
|
|
91
|
+
* `headers`, `cookies`, `body` or response `schema` are converted for
|
|
92
|
+
* `options.version`.
|
|
102
93
|
*/
|
|
103
|
-
export function convertRouteToOpenAPI(method, path, metadata) {
|
|
94
|
+
export function convertRouteToOpenAPI(method, path, metadata, options = {}) {
|
|
104
95
|
if (!isOpenAPIMethod(method)) {
|
|
105
96
|
throw new OpenAPIRouteError(`HTTP method "${method}" has no OpenAPI path item field. ` +
|
|
106
97
|
`Supported: ${ZUDOLIB_TO_OPENAPI_METHODS.join(", ")}.`, { metadata: { method, path } });
|
|
107
98
|
}
|
|
108
99
|
const openApiPath = toOpenAPIPath(path);
|
|
109
100
|
const meta = metadata?.openapi;
|
|
101
|
+
const context = `${method.toUpperCase()} ${path}`;
|
|
102
|
+
const parameters = buildOperationParameters(extractPathParameters(openApiPath), meta, context, options);
|
|
103
|
+
const requestBody = buildOperationRequestBody(meta, context, options);
|
|
110
104
|
const operation = {
|
|
111
105
|
...(meta?.operationId ? { operationId: meta.operationId } : {}),
|
|
112
106
|
...(meta?.summary ? { summary: meta.summary } : {}),
|
|
113
107
|
...(meta?.description ? { description: meta.description } : {}),
|
|
114
108
|
...(meta?.tags?.length ? { tags: [...meta.tags] } : {}),
|
|
115
109
|
...(meta?.deprecated !== undefined ? { deprecated: meta.deprecated } : {}),
|
|
116
|
-
...(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
...(parameters.length > 0 ? { parameters } : {}),
|
|
111
|
+
...(requestBody ? { requestBody } : {}),
|
|
112
|
+
// An empty list is meaningful: it marks the operation public, overriding
|
|
113
|
+
// document-level security. Dropping it documented a public route as
|
|
114
|
+
// requiring every global scheme.
|
|
115
|
+
...(meta?.security !== undefined ? { security: [...meta.security] } : {}),
|
|
121
116
|
...(meta?.servers?.length ? { servers: [...meta.servers] } : {}),
|
|
122
117
|
...(meta?.externalDocs ? { externalDocs: meta.externalDocs } : {}),
|
|
123
|
-
responses:
|
|
118
|
+
responses: buildOperationResponses(meta, context, options),
|
|
124
119
|
};
|
|
125
120
|
return {
|
|
126
121
|
method: method.toLowerCase(),
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* use.
|
|
9
9
|
*/
|
|
10
10
|
import type { OpenAPIExternalDocumentation, OpenAPIRequestBody, OpenAPIResponse, OpenAPISecurityRequirement, OpenAPIServer } from "../openApiTypes/openApiTypes.core.js";
|
|
11
|
+
import type { OpenAPIRouteBody, OpenAPIRouteResponse, OpenAPISchemaInput } from "../openApiSchema/schemaInput.type.js";
|
|
12
|
+
export type { OpenAPIRouteBody, OpenAPIRouteResponse, OpenAPISchemaInput, RouteConversionOptions, } from "../openApiSchema/schemaInput.type.js";
|
|
11
13
|
/** HTTP methods an OpenAPI path item can carry. */
|
|
12
14
|
export type OpenAPIHttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
|
|
13
15
|
/**
|
|
@@ -19,6 +21,7 @@ export interface RouteParameterMetadata {
|
|
|
19
21
|
readonly description?: string;
|
|
20
22
|
readonly required?: boolean;
|
|
21
23
|
readonly deprecated?: boolean;
|
|
24
|
+
/** An {@link OpenAPISchemaInput}; a `@zudojs/schema` schema is converted. */
|
|
22
25
|
readonly schema?: unknown;
|
|
23
26
|
readonly example?: unknown;
|
|
24
27
|
}
|
|
@@ -31,13 +34,44 @@ export interface RouteOpenAPIMetadata {
|
|
|
31
34
|
readonly description?: string;
|
|
32
35
|
readonly tags?: readonly string[];
|
|
33
36
|
readonly deprecated?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Explicit parameters. They take precedence over parameters derived from
|
|
39
|
+
* `params` / `query` / `headers` / `cookies` with the same name and
|
|
40
|
+
* location, which in turn take precedence over `inferredParameters`.
|
|
41
|
+
*/
|
|
34
42
|
readonly parameters?: readonly RouteParameterMetadata[];
|
|
43
|
+
/**
|
|
44
|
+
* Path parameters as one object schema; each property becomes an
|
|
45
|
+
* `in: "path"` parameter. Template slots it does not cover are still
|
|
46
|
+
* documented, as required strings.
|
|
47
|
+
*/
|
|
48
|
+
readonly params?: OpenAPISchemaInput;
|
|
49
|
+
/** Query parameters as one object schema; one parameter per property. */
|
|
50
|
+
readonly query?: OpenAPISchemaInput;
|
|
51
|
+
/** Request headers as one object schema; one parameter per property. */
|
|
52
|
+
readonly headers?: OpenAPISchemaInput;
|
|
53
|
+
/** Cookies as one object schema; one parameter per property. */
|
|
54
|
+
readonly cookies?: OpenAPISchemaInput;
|
|
55
|
+
/**
|
|
56
|
+
* Parameters the route source inferred on its own (for example a regular
|
|
57
|
+
* expression constraint on a path segment). Lowest precedence: any
|
|
58
|
+
* declared parameter with the same name and location replaces one.
|
|
59
|
+
*/
|
|
60
|
+
readonly inferredParameters?: readonly RouteParameterMetadata[];
|
|
61
|
+
/**
|
|
62
|
+
* The request body as a schema (`application/json`) or an
|
|
63
|
+
* {@link OpenAPIRouteBody}. Ignored when `requestBody` is set.
|
|
64
|
+
*/
|
|
65
|
+
readonly body?: OpenAPISchemaInput | OpenAPIRouteBody;
|
|
66
|
+
/** A raw Request Body Object; takes precedence over `body`. */
|
|
35
67
|
readonly requestBody?: OpenAPIRequestBody;
|
|
36
68
|
/**
|
|
37
69
|
* Responses keyed by status code, `default`, or a `2XX`-style range.
|
|
38
|
-
* Every entry reaches the document — this is not a 200-only field.
|
|
70
|
+
* Every entry reaches the document — this is not a 200-only field. An
|
|
71
|
+
* entry is a Response Object, or an {@link OpenAPIRouteResponse} when it
|
|
72
|
+
* carries a `schema`.
|
|
39
73
|
*/
|
|
40
|
-
readonly responses?: Readonly<Record<string, OpenAPIResponse>>;
|
|
74
|
+
readonly responses?: Readonly<Record<string, OpenAPIResponse | OpenAPIRouteResponse>>;
|
|
41
75
|
readonly security?: readonly OpenAPISecurityRequirement[];
|
|
42
76
|
readonly servers?: readonly OpenAPIServer[];
|
|
43
77
|
readonly externalDocs?: OpenAPIExternalDocumentation;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenAPIRoute } from "../openApiRegistry/openApiRegistry.type.js";
|
|
2
|
-
import type { RouteInfo } from "./routeMetadata.type.js";
|
|
2
|
+
import type { RouteConversionOptions, RouteInfo } from "./routeMetadata.type.js";
|
|
3
3
|
/**
|
|
4
4
|
* Collects routes and converts them into OpenAPI operations.
|
|
5
5
|
*
|
|
@@ -20,8 +20,12 @@ export declare class OpenAPIRouteScannerImpl {
|
|
|
20
20
|
removeRoute(method: string, path: string): boolean;
|
|
21
21
|
/** Number of registered routes. */
|
|
22
22
|
get size(): number;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Converts every registered route into an OpenAPI operation. `options`
|
|
25
|
+
* sets the version declared schemas are converted for and receives their
|
|
26
|
+
* conversion warnings.
|
|
27
|
+
*/
|
|
28
|
+
scan(options?: RouteConversionOptions): readonly OpenAPIRoute[];
|
|
25
29
|
clear(): void;
|
|
26
30
|
}
|
|
27
31
|
//# sourceMappingURL=routeScanner.core.d.ts.map
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
import { convertRouteToOpenAPI } from "./routeConverter.core.js";
|
|
1
|
+
import { convertRouteToOpenAPI, toOpenAPIPath } from "./routeConverter.core.js";
|
|
2
2
|
import { OpenAPIRouteError } from "../openApiErrors/openApiError.types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Identity of a route inside the generated document.
|
|
5
|
+
*
|
|
6
|
+
* Keyed on the OpenAPI path template rather than the source path, because
|
|
7
|
+
* that is what the document is keyed on: `/users/:id` and `/users/{id}` are
|
|
8
|
+
* one path item. Keying on the raw spelling let both register, and the
|
|
9
|
+
* second then replaced the first during generation — one operation vanished
|
|
10
|
+
* from the published spec with `validate()` reporting nothing.
|
|
11
|
+
*
|
|
12
|
+
* A path `toOpenAPIPath` cannot express keeps its raw spelling here so the
|
|
13
|
+
* conversion error still surfaces from `scan()`, where it always has.
|
|
14
|
+
*/
|
|
15
|
+
function routeKey(method, path) {
|
|
16
|
+
let template;
|
|
17
|
+
try {
|
|
18
|
+
template = toOpenAPIPath(path);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
template = path;
|
|
22
|
+
}
|
|
23
|
+
return `${method.toLowerCase()}:${template}`;
|
|
24
|
+
}
|
|
3
25
|
/**
|
|
4
26
|
* Collects routes and converts them into OpenAPI operations.
|
|
5
27
|
*
|
|
@@ -12,35 +34,49 @@ export class OpenAPIRouteScannerImpl {
|
|
|
12
34
|
routes = new Map();
|
|
13
35
|
/** Registers a route. */
|
|
14
36
|
addRoute(route) {
|
|
15
|
-
const key =
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
const key = routeKey(route.method, route.path);
|
|
38
|
+
const existing = this.routes.get(key);
|
|
39
|
+
if (existing !== undefined) {
|
|
40
|
+
throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered` +
|
|
41
|
+
(existing.path === route.path
|
|
42
|
+
? "."
|
|
43
|
+
: ` as ${existing.method.toUpperCase()} ${existing.path}; both describe the same OpenAPI path.`), {
|
|
44
|
+
metadata: {
|
|
45
|
+
method: route.method,
|
|
46
|
+
path: route.path,
|
|
47
|
+
existingPath: existing.path,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
18
50
|
}
|
|
19
51
|
this.routes.set(key, route);
|
|
20
52
|
}
|
|
21
53
|
/** Registers a route, replacing any existing one for the same method+path. */
|
|
22
54
|
setRoute(route) {
|
|
23
|
-
this.routes.set(
|
|
55
|
+
this.routes.set(routeKey(route.method, route.path), route);
|
|
24
56
|
}
|
|
25
57
|
/** True when a route is registered for this method and path. */
|
|
26
58
|
hasRoute(method, path) {
|
|
27
|
-
return this.routes.has(
|
|
59
|
+
return this.routes.has(routeKey(method, path));
|
|
28
60
|
}
|
|
29
61
|
/** Removes a route. Returns whether one was removed. */
|
|
30
62
|
removeRoute(method, path) {
|
|
31
|
-
return this.routes.delete(
|
|
63
|
+
return this.routes.delete(routeKey(method, path));
|
|
32
64
|
}
|
|
33
65
|
/** Number of registered routes. */
|
|
34
66
|
get size() {
|
|
35
67
|
return this.routes.size;
|
|
36
68
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Converts every registered route into an OpenAPI operation. `options`
|
|
71
|
+
* sets the version declared schemas are converted for and receives their
|
|
72
|
+
* conversion warnings.
|
|
73
|
+
*/
|
|
74
|
+
scan(options) {
|
|
39
75
|
const result = [];
|
|
40
76
|
for (const route of this.routes.values()) {
|
|
41
77
|
if (route.metadata?.openapi?.hidden === true)
|
|
42
78
|
continue;
|
|
43
|
-
const converted = convertRouteToOpenAPI(route.method, route.path, route.metadata);
|
|
79
|
+
const converted = convertRouteToOpenAPI(route.method, route.path, route.metadata, options);
|
|
44
80
|
result.push({
|
|
45
81
|
method: converted.method,
|
|
46
82
|
path: converted.path,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-aware parameter derivation for route conversion: path parameters,
|
|
3
|
+
* query, headers and cookies declared as one object schema each become one
|
|
4
|
+
* parameter per property.
|
|
5
|
+
*/
|
|
6
|
+
import type { OpenAPIParameter } from "../openApiTypes/openApiTypes.core.js";
|
|
7
|
+
import type { RouteConversionOptions, RouteOpenAPIMetadata } from "./routeMetadata.type.js";
|
|
8
|
+
/**
|
|
9
|
+
* Builds an operation's parameter list.
|
|
10
|
+
*
|
|
11
|
+
* `slots` are the path template's parameter names. Layers, lowest precedence
|
|
12
|
+
* first: a required string per slot, `inferredParameters`, the `params` /
|
|
13
|
+
* `query` / `headers` / `cookies` schemas, then explicit `parameters`; a
|
|
14
|
+
* later layer replaces a parameter with the same name and location. A path
|
|
15
|
+
* parameter no slot names is dropped with a warning: emitting it would make
|
|
16
|
+
* a document that cannot validate.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildOperationParameters(slots: readonly string[], meta: RouteOpenAPIMetadata | undefined, context: string, options?: RouteConversionOptions): readonly OpenAPIParameter[];
|
|
19
|
+
//# sourceMappingURL=routeSchema.core.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-aware parameter derivation for route conversion: path parameters,
|
|
3
|
+
* query, headers and cookies declared as one object schema each become one
|
|
4
|
+
* parameter per property.
|
|
5
|
+
*/
|
|
6
|
+
import { resolveSchemaInput } from "../openApiSchema/schemaInput.core.js";
|
|
7
|
+
function isObjectSchema(schema) {
|
|
8
|
+
const type = schema.type;
|
|
9
|
+
if (type === "object")
|
|
10
|
+
return true;
|
|
11
|
+
if (Array.isArray(type))
|
|
12
|
+
return type.includes("object");
|
|
13
|
+
return type === undefined && schema.properties !== undefined;
|
|
14
|
+
}
|
|
15
|
+
/** One parameter per property of an object schema. */
|
|
16
|
+
function parametersFromSchema(input, location, context, options) {
|
|
17
|
+
const schema = resolveSchemaInput(input, context, options);
|
|
18
|
+
if (!isObjectSchema(schema) || schema.properties === undefined) {
|
|
19
|
+
options.onWarning?.(`${context}: ${location} parameters must be declared as an object schema; ignored.`);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const required = new Set(schema.required ?? []);
|
|
23
|
+
return Object.entries(schema.properties).map(([name, property]) => ({
|
|
24
|
+
name,
|
|
25
|
+
in: location,
|
|
26
|
+
required: location === "path" ? true : required.has(name),
|
|
27
|
+
...(property.description ? { description: property.description } : {}),
|
|
28
|
+
...(property.deprecated ? { deprecated: true } : {}),
|
|
29
|
+
schema: property,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function toParameter(parameter, context, options) {
|
|
33
|
+
return {
|
|
34
|
+
name: parameter.name,
|
|
35
|
+
in: parameter.in,
|
|
36
|
+
...(parameter.description ? { description: parameter.description } : {}),
|
|
37
|
+
// A path parameter is required by the specification, so declaring one
|
|
38
|
+
// that is not required is a document that cannot validate.
|
|
39
|
+
required: parameter.in === "path" ? true : (parameter.required ?? false),
|
|
40
|
+
...(parameter.deprecated ? { deprecated: true } : {}),
|
|
41
|
+
...(parameter.schema === undefined ? {} : {
|
|
42
|
+
schema: resolveSchemaInput(parameter.schema, `${context} "${parameter.name}"`, options),
|
|
43
|
+
}),
|
|
44
|
+
...(parameter.example !== undefined ? { example: parameter.example } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const SCHEMA_LOCATIONS = [
|
|
48
|
+
["params", "path"],
|
|
49
|
+
["query", "query"],
|
|
50
|
+
["headers", "header"],
|
|
51
|
+
["cookies", "cookie"],
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Builds an operation's parameter list.
|
|
55
|
+
*
|
|
56
|
+
* `slots` are the path template's parameter names. Layers, lowest precedence
|
|
57
|
+
* first: a required string per slot, `inferredParameters`, the `params` /
|
|
58
|
+
* `query` / `headers` / `cookies` schemas, then explicit `parameters`; a
|
|
59
|
+
* later layer replaces a parameter with the same name and location. A path
|
|
60
|
+
* parameter no slot names is dropped with a warning: emitting it would make
|
|
61
|
+
* a document that cannot validate.
|
|
62
|
+
*/
|
|
63
|
+
export function buildOperationParameters(slots, meta, context, options = {}) {
|
|
64
|
+
const merged = new Map();
|
|
65
|
+
const put = (parameter) => void merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
66
|
+
for (const name of slots) {
|
|
67
|
+
put({ name, in: "path", required: true, schema: { type: "string" } });
|
|
68
|
+
}
|
|
69
|
+
for (const parameter of meta?.inferredParameters ?? []) {
|
|
70
|
+
put(toParameter(parameter, context, options));
|
|
71
|
+
}
|
|
72
|
+
for (const [key, location] of SCHEMA_LOCATIONS) {
|
|
73
|
+
const input = meta?.[key];
|
|
74
|
+
if (input === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
for (const parameter of parametersFromSchema(input, location, context, options)) {
|
|
77
|
+
put(parameter);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const parameter of meta?.parameters ?? []) {
|
|
81
|
+
put(toParameter(parameter, context, options));
|
|
82
|
+
}
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const parameter of merged.values()) {
|
|
85
|
+
if (parameter.in === "path" && !slots.includes(parameter.name)) {
|
|
86
|
+
options.onWarning?.(`${context}: path parameter "${parameter.name}" is not in the path template; ignored.`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
result.push(parameter);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=routeSchema.core.js.map
|
|
@@ -8,4 +8,5 @@ export { convertSchema, createSchemaConverter, isVersion31, } from "./schemaConv
|
|
|
8
8
|
export type { SchemaRegistry, SchemaRegistryOptions, } from "./schemaRegistry.core.js";
|
|
9
9
|
export { SchemaRegistryImpl } from "./schemaRegistry.core.js";
|
|
10
10
|
export { createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, type ComponentSection, } from "./references.core.js";
|
|
11
|
+
export { isSchemaDefinition, resolveSchemaInput, type SchemaInputOptions, } from "./schemaInput.core.js";
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
export { convertSchema, createSchemaConverter, isVersion31, } from "./schemaConverter.core.js";
|
|
7
7
|
export { SchemaRegistryImpl } from "./schemaRegistry.core.js";
|
|
8
8
|
export { createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, } from "./references.core.js";
|
|
9
|
+
export { isSchemaDefinition, resolveSchemaInput, } from "./schemaInput.core.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,6 +11,8 @@ import type { OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
|
|
|
11
11
|
*
|
|
12
12
|
* Field names as of `@zudojs/schema@0.1.0`:
|
|
13
13
|
* object `_config.shape`, `_config.requiredKeys` (a Set), `_config.unknownKeys`
|
|
14
|
+
* (`"strip" | "strict" | "passthrough"`; absent means the parser's
|
|
15
|
+
* own `?? "strip"` default, so it is read with that same default)
|
|
14
16
|
* array `_config.itemSchema`, `_config.min`, `_config.max`, `_config.length`
|
|
15
17
|
* (no `max` means the parser's implicit ceiling, which is emitted)
|
|
16
18
|
* string `_config.min|max|length|pattern|format`
|
|
@@ -309,14 +309,19 @@ function convertSchemaNode(schema, state) {
|
|
|
309
309
|
if (forced || !acceptsMissingKey(value))
|
|
310
310
|
required.push(key);
|
|
311
311
|
}
|
|
312
|
-
|
|
312
|
+
// `ObjectSchema` applies `?? "strip"` internally, so an absent
|
|
313
|
+
// `unknownKeys` is the same contract as an explicit `.strip()` and
|
|
314
|
+
// must document the same. Only `strict` emits
|
|
315
|
+
// `additionalProperties: false`: that is OpenAPI for "reject the
|
|
316
|
+
// payload", while strip accepts it and discards the extra key, so
|
|
317
|
+
// emitting it for strip made a generated client refuse what the
|
|
318
|
+
// service accepts.
|
|
319
|
+
const unknownKeys = c["unknownKeys"] ?? "strip";
|
|
313
320
|
return {
|
|
314
321
|
type: "object",
|
|
315
322
|
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
316
323
|
...(required.length > 0 ? { required } : {}),
|
|
317
|
-
...(unknownKeys === "
|
|
318
|
-
? { additionalProperties: false }
|
|
319
|
-
: {}),
|
|
324
|
+
...(unknownKeys === "strict" ? { additionalProperties: false } : {}),
|
|
320
325
|
};
|
|
321
326
|
}
|
|
322
327
|
case "record": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of schemas declared on routes and operations.
|
|
3
|
+
*
|
|
4
|
+
* A declared schema is either a `@zudojs/schema` schema, converted here for
|
|
5
|
+
* the target version, or an OpenAPI Schema / Reference Object used as-is.
|
|
6
|
+
*/
|
|
7
|
+
import type { OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
|
|
8
|
+
/** Options for {@link resolveSchemaInput}. */
|
|
9
|
+
export interface SchemaInputOptions {
|
|
10
|
+
/** Specification version to convert for. Default: 3.1.0. */
|
|
11
|
+
readonly version?: string;
|
|
12
|
+
/** Receives everything a conversion could not express exactly. */
|
|
13
|
+
readonly onWarning?: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/** True when `value` is a `@zudojs/schema` schema (it carries a string `_type`). */
|
|
16
|
+
export declare function isSchemaDefinition(value: unknown): value is {
|
|
17
|
+
readonly _type: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a declared schema to an OpenAPI schema.
|
|
21
|
+
*
|
|
22
|
+
* A `@zudojs/schema` schema is converted and each conversion warning is
|
|
23
|
+
* reported prefixed with `context`; any other object is taken to be an
|
|
24
|
+
* OpenAPI Schema or Reference Object already. A non-object is reported and
|
|
25
|
+
* becomes `{}`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSchemaInput(input: unknown, context: string, options?: SchemaInputOptions): OpenAPISchema;
|
|
28
|
+
//# sourceMappingURL=schemaInput.core.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of schemas declared on routes and operations.
|
|
3
|
+
*
|
|
4
|
+
* A declared schema is either a `@zudojs/schema` schema, converted here for
|
|
5
|
+
* the target version, or an OpenAPI Schema / Reference Object used as-is.
|
|
6
|
+
*/
|
|
7
|
+
import { convertSchema } from "./schemaConverter.core.js";
|
|
8
|
+
/** True when `value` is a `@zudojs/schema` schema (it carries a string `_type`). */
|
|
9
|
+
export function isSchemaDefinition(value) {
|
|
10
|
+
return (typeof value === "object" &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
typeof value._type === "string");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a declared schema to an OpenAPI schema.
|
|
16
|
+
*
|
|
17
|
+
* A `@zudojs/schema` schema is converted and each conversion warning is
|
|
18
|
+
* reported prefixed with `context`; any other object is taken to be an
|
|
19
|
+
* OpenAPI Schema or Reference Object already. A non-object is reported and
|
|
20
|
+
* becomes `{}`.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveSchemaInput(input, context, options = {}) {
|
|
23
|
+
if (isSchemaDefinition(input)) {
|
|
24
|
+
const result = convertSchema(input, { version: options.version });
|
|
25
|
+
for (const warning of result.warnings) {
|
|
26
|
+
options.onWarning?.(`${context}: ${warning}`);
|
|
27
|
+
}
|
|
28
|
+
return result.schema;
|
|
29
|
+
}
|
|
30
|
+
if (typeof input === "object" && input !== null) {
|
|
31
|
+
return input;
|
|
32
|
+
}
|
|
33
|
+
options.onWarning?.(`${context}: expected a schema, got ${typeof input}; emitted {}.`);
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=schemaInput.core.js.map
|