@zudojs/openapi 1.4.0 → 1.5.1

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.
Files changed (33) hide show
  1. package/README.md +83 -2
  2. package/dist/index.d.ts +10 -6
  3. package/dist/index.js +8 -4
  4. package/dist/openApiConstants/index.d.ts +1 -1
  5. package/dist/openApiConstants/index.js +1 -1
  6. package/dist/openApiConstants/openApiConstants.core.d.ts +5 -0
  7. package/dist/openApiConstants/openApiConstants.core.js +5 -0
  8. package/dist/openApiFromRoutes/index.d.ts +11 -0
  9. package/dist/openApiFromRoutes/index.js +10 -0
  10. package/dist/openApiFromRoutes/openApiFromRoutes.document.d.ts +43 -0
  11. package/dist/openApiFromRoutes/openApiFromRoutes.document.js +64 -0
  12. package/dist/openApiFromRoutes/openApiFromRoutes.type.d.ts +56 -0
  13. package/dist/openApiFromRoutes/openApiFromRoutes.type.js +5 -0
  14. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +28 -1
  15. package/dist/openApiHttp/openApiHttpAdapter.core.js +40 -1
  16. package/dist/openApiRouting/index.d.ts +3 -1
  17. package/dist/openApiRouting/index.js +2 -0
  18. package/dist/openApiRouting/routeContent.core.d.ts +22 -0
  19. package/dist/openApiRouting/routeContent.core.js +101 -0
  20. package/dist/openApiRouting/routeConverter.core.d.ts +11 -5
  21. package/dist/openApiRouting/routeConverter.core.js +23 -28
  22. package/dist/openApiRouting/routeMetadata.type.d.ts +36 -2
  23. package/dist/openApiRouting/routeScanner.core.d.ts +7 -3
  24. package/dist/openApiRouting/routeScanner.core.js +7 -3
  25. package/dist/openApiRouting/routeSchema.core.d.ts +19 -0
  26. package/dist/openApiRouting/routeSchema.core.js +93 -0
  27. package/dist/openApiSchema/index.d.ts +1 -0
  28. package/dist/openApiSchema/index.js +1 -0
  29. package/dist/openApiSchema/schemaInput.core.d.ts +28 -0
  30. package/dist/openApiSchema/schemaInput.core.js +36 -0
  31. package/dist/openApiSchema/schemaInput.type.d.ts +48 -0
  32. package/dist/openApiSchema/schemaInput.type.js +5 -0
  33. package/package.json +7 -6
package/README.md CHANGED
@@ -51,8 +51,12 @@ const yaml = manager.toYAML();
51
51
  ```
52
52
 
53
53
  Every documented response reaches the document — `4xx`, `5xx` and `default`
54
- included. `:id` becomes `{id}`, and a path parameter is marked required
55
- because the specification requires it.
54
+ included. `:id` becomes `{id}`, every template slot is documented as a
55
+ required path parameter (declared or not), and `params` / `query` /
56
+ `headers` / `body` / response `schema` fields accept `@zudojs/schema` schemas
57
+ — see [Generating from a route table](#generating-from-a-route-table). To
58
+ generate from an `@zudojs/http` router instead of adding routes by hand, use
59
+ that package's `generateOpenAPIDocument(router)` or `mountOpenAPI(router)`.
56
60
 
57
61
  Generation is idempotent: call `generate()` as often as you like.
58
62
 
@@ -134,6 +138,83 @@ scheme is read after stripping the control characters browsers ignore, so
134
138
  `customCss` containing `</style>` throws — that sequence ends the style block
135
139
  and lets the rest be parsed as HTML.
136
140
 
141
+ ## Generating from a route table
142
+
143
+ `createOpenAPIDocumentFromRoutes(routes, options)` builds a document from a
144
+ list of structural `OpenAPIRouteDescriptor`s, so any route source can be
145
+ documented from what it actually registers. `@zudojs/http`'s
146
+ `generateOpenAPIDocument(router)` and `mountOpenAPI(router)` feed it the
147
+ router's routes; other sources (such as `@zudojs/api` operations) build the
148
+ same descriptors.
149
+
150
+ ```typescript
151
+ import { createOpenAPIDocumentFromRoutes } from "@zudojs/openapi";
152
+ import { objectSchema, stringSchema } from "@zudojs/schema";
153
+
154
+ const user = objectSchema({ id: stringSchema().uuid(), name: stringSchema() });
155
+
156
+ const document = createOpenAPIDocumentFromRoutes(
157
+ [
158
+ { method: "GET", path: "/users/:id", summary: "Get a user", tags: ["users"],
159
+ responses: { "200": { schema: user }, "404": { description: "No such user" } } },
160
+ { method: "POST", path: "/users", operationId: "users.create",
161
+ body: objectSchema({ name: stringSchema() }),
162
+ responses: { "201": { schema: user } } },
163
+ ],
164
+ {
165
+ info: { title: "Users API", version: "1.0.0" },
166
+ securitySchemes: { bearer: { type: "http", scheme: "bearer" } },
167
+ security: [{ bearer: [] }],
168
+ schemas: { User: user }, // components; converted when they are @zudojs/schema schemas
169
+ validate: true,
170
+ },
171
+ );
172
+ ```
173
+
174
+ An `OpenAPIRouteDescriptor` is `RouteOpenAPIMetadata` plus `method` and
175
+ `path`, flattened:
176
+
177
+ | Field | Meaning |
178
+ | --- | --- |
179
+ | `method` | Any case; must be `get put post delete options head patch trace` |
180
+ | `path` | `/users/:id` or `/users/{id}`; optional/regex/wildcard segments are resolved by the source |
181
+ | `summary`, `description`, `operationId`, `tags`, `deprecated`, `servers`, `externalDocs` | Copied to the operation |
182
+ | `security` | Operation security; `[]` marks it public (overrides document security) |
183
+ | `params`, `query`, `headers`, `cookies` | One object schema each; every property becomes a parameter (`required` from the schema; path parameters always required) |
184
+ | `body` | A schema (sent as `application/json`, required) or `{ schema, contentType?, required?, description?, example? }`; a raw `requestBody` wins |
185
+ | `responses` | Per status / `NXX` / `default`: a Response Object, or `{ schema, description?, contentType?, headers?, example? }` (description defaults to the reason phrase, e.g. "Not Found") |
186
+ | `parameters` | Explicit parameters, highest precedence |
187
+ | `inferredParameters` | Parameters the source derived itself (e.g. a regex constraint), lowest precedence |
188
+ | `hidden` | `true` leaves the operation out |
189
+
190
+ Every path template slot is documented as a required string parameter even
191
+ when nothing declares it, and a declared path parameter the template does
192
+ not contain is dropped with a warning rather than producing an invalid
193
+ document. The same metadata fields work in `manager.addRoute({ method, path,
194
+ metadata: { openapi } })`. Conversion warnings reach `onRouteWarning`
195
+ (one message at a time), `onSchemaWarning` under the name `"routes"`, and
196
+ `manager.routeWarnings()`, each prefixed with the route (`DELETE /users/:id: ...`).
197
+
198
+ No response status is invented. A route that documents no responses (or
199
+ `responses: {}`) gets a spec-valid `default` response described as
200
+ `"Undocumented response"` (`UNDOCUMENTED_RESPONSE_DESCRIPTION`) and a route
201
+ warning, so the omission shows up instead of a `204` DELETE being published
202
+ as `200`:
203
+
204
+ ```typescript
205
+ const document = createOpenAPIDocumentFromRoutes(
206
+ [{ method: "DELETE", path: "/users/:id" }],
207
+ { info, onRouteWarning: (message) => logger.warn(message) },
208
+ );
209
+ // paths["/users/{id}"].delete.responses → { default: { description: "Undocumented response" } }
210
+ // warned: "DELETE /users/:id: no responses are documented; ..."
211
+ ```
212
+
213
+ `createOpenAPIManagerFromRoutes(routes, options)` returns the manager instead
214
+ (for `toResponse` / `toUIResponse`); `manager.setRoutes(routes)` replaces its
215
+ whole route set, rejecting duplicates before anything changes, and
216
+ `routeDescriptorToRouteInfo` converts a descriptor to `addRoute`'s shape.
217
+
137
218
  ## Branding
138
219
 
139
220
  ReDoc, Scalar and several other viewers read a logo from the non-standard
package/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * API contract and documentation engine for the Zudojs framework.
5
5
  *
6
6
  * Generates OpenAPI specifications from application routes, schemas,
7
- * and metadata. Supports OpenAPI 3.0 and 3.1.
7
+ * and metadata. Supports OpenAPI 3.0 and 3.1. A route table from any source
8
+ * becomes a document through `createOpenAPIDocumentFromRoutes`;
9
+ * `@zudojs/http` uses it to document a router's registered routes.
8
10
  *
9
11
  * @example
10
12
  * ```ts
@@ -42,11 +44,13 @@ export { OpenAPIDocumentBuilder, createOpenAPIDocumentBuilder, type OpenAPIDocum
42
44
  export { OpenAPIRegistryImpl } from "./openApiRegistry/index.js";
43
45
  export type { OpenAPIRegistry, OpenAPIRoute, OpenAPIComponentRegistration, } from "./openApiRegistry/index.js";
44
46
  export { OpenAPIError, OpenAPIValidationError, OpenAPIDocumentError, OpenAPIComponentError, OpenAPIComponentConflictError, OpenAPIReferenceError, OpenAPIRouteError, OpenAPISchemaError, OpenAPISerializationError, OpenAPIVersionError, OpenAPIOperationError, createOpenAPIError, isOpenAPIError, formatIssuePath, type OpenAPIErrorOptions, type OpenAPIValidationIssue, } from "./openApiErrors/index.js";
45
- export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, } from "./openApiConstants/index.js";
46
- export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, OpenAPIRouteScannerImpl, } from "./openApiRouting/index.js";
47
- export type { RouteMetadata, RouteOpenAPIMetadata, RouteParameterMetadata, RouteInfo, OpenAPIHttpMethod, } from "./openApiRouting/index.js";
48
- export { convertSchema, createSchemaConverter, isVersion31, SchemaRegistryImpl, createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, } from "./openApiSchema/index.js";
49
- export type { SchemaConverter, SchemaConversionResult, SchemaConversionOptions, SchemaRegistry, SchemaRegistryOptions, ComponentSection, } from "./openApiSchema/index.js";
47
+ export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, UNDOCUMENTED_RESPONSE_DESCRIPTION, } from "./openApiConstants/index.js";
48
+ export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, OpenAPIRouteScannerImpl, buildOperationParameters, buildOperationRequestBody, buildOperationResponses, describeResponseKey, } from "./openApiRouting/index.js";
49
+ export type { RouteMetadata, RouteOpenAPIMetadata, RouteParameterMetadata, RouteInfo, OpenAPIHttpMethod, OpenAPISchemaInput, OpenAPIRouteBody, OpenAPIRouteResponse, RouteConversionOptions, } from "./openApiRouting/index.js";
50
+ export { createOpenAPIDocumentFromRoutes, createOpenAPIManagerFromRoutes, routeDescriptorToRouteInfo, } from "./openApiFromRoutes/index.js";
51
+ export type { OpenAPIRouteDescriptor, OpenAPIDocumentFromRoutesOptions, } from "./openApiFromRoutes/index.js";
52
+ export { convertSchema, createSchemaConverter, isVersion31, SchemaRegistryImpl, createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, isSchemaDefinition, resolveSchemaInput, } from "./openApiSchema/index.js";
53
+ export type { SchemaInputOptions, SchemaConverter, SchemaConversionResult, SchemaConversionOptions, SchemaRegistry, SchemaRegistryOptions, ComponentSection, } from "./openApiSchema/index.js";
50
54
  export { OpenAPIValidatorImpl, createOpenAPIValidator, } from "./openApiValidation/index.js";
51
55
  export type { OpenAPIValidator, OpenAPIValidationResult, } from "./openApiValidation/index.js";
52
56
  export { toOpenAPIJSON, toOpenAPIYAML, } from "./openApiSerialization/openApiSerializer.core.js";
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@
4
4
  * API contract and documentation engine for the Zudojs framework.
5
5
  *
6
6
  * Generates OpenAPI specifications from application routes, schemas,
7
- * and metadata. Supports OpenAPI 3.0 and 3.1.
7
+ * and metadata. Supports OpenAPI 3.0 and 3.1. A route table from any source
8
+ * becomes a document through `createOpenAPIDocumentFromRoutes`;
9
+ * `@zudojs/http` uses it to document a router's registered routes.
8
10
  *
9
11
  * @example
10
12
  * ```ts
@@ -46,11 +48,13 @@ export { OpenAPIRegistryImpl } from "./openApiRegistry/index.js";
46
48
  /* ─── Errors ────────────────────────────────────────────────────────────── */
47
49
  export { OpenAPIError, OpenAPIValidationError, OpenAPIDocumentError, OpenAPIComponentError, OpenAPIComponentConflictError, OpenAPIReferenceError, OpenAPIRouteError, OpenAPISchemaError, OpenAPISerializationError, OpenAPIVersionError, OpenAPIOperationError, createOpenAPIError, isOpenAPIError, formatIssuePath, } from "./openApiErrors/index.js";
48
50
  /* ─── Constants ─────────────────────────────────────────────────────────── */
49
- export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, } from "./openApiConstants/index.js";
51
+ export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, UNDOCUMENTED_RESPONSE_DESCRIPTION, } from "./openApiConstants/index.js";
50
52
  /* ─── Routing ───────────────────────────────────────────────────────────── */
51
- export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, OpenAPIRouteScannerImpl, } from "./openApiRouting/index.js";
53
+ export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, OpenAPIRouteScannerImpl, buildOperationParameters, buildOperationRequestBody, buildOperationResponses, describeResponseKey, } from "./openApiRouting/index.js";
54
+ /* ─── Generation from a route table ────────────────────────────────────── */
55
+ export { createOpenAPIDocumentFromRoutes, createOpenAPIManagerFromRoutes, routeDescriptorToRouteInfo, } from "./openApiFromRoutes/index.js";
52
56
  /* ─── Schema conversion ─────────────────────────────────────────────────── */
53
- export { convertSchema, createSchemaConverter, isVersion31, SchemaRegistryImpl, createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, } from "./openApiSchema/index.js";
57
+ export { convertSchema, createSchemaConverter, isVersion31, SchemaRegistryImpl, createComponentReference, escapeJsonPointerSegment, unescapeJsonPointerSegment, isSchemaDefinition, resolveSchemaInput, } from "./openApiSchema/index.js";
54
58
  /* ─── Validation ────────────────────────────────────────────────────────── */
55
59
  export { OpenAPIValidatorImpl, createOpenAPIValidator, } from "./openApiValidation/index.js";
56
60
  /* ─── Serialization ─────────────────────────────────────────────────────── */
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @zudojs/openapi/openApiConstants
3
3
  */
4
- export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, } from "./openApiConstants.core.js";
4
+ export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, UNDOCUMENTED_RESPONSE_DESCRIPTION, } from "./openApiConstants.core.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @zudojs/openapi/openApiConstants
3
3
  */
4
- export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, } from "./openApiConstants.core.js";
4
+ export { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, MAX_OPERATION_ID_LENGTH, COMPONENT_REF_PREFIX, DEFAULT_MEDIA_TYPE, STATUS_CODE_CATEGORIES, RESPONSE_KEY_PATTERN, PATH_TEMPLATE_PARAMETER, DEFAULT_SERVER_URL, DOCUMENT_CACHE_TTL_MS, UNDOCUMENTED_RESPONSE_DESCRIPTION, } from "./openApiConstants.core.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -39,6 +39,11 @@ export declare const STATUS_CODE_CATEGORIES: {
39
39
  export declare const RESPONSE_KEY_PATTERN: RegExp;
40
40
  /** OpenAPI path template parameter, e.g. `{orderId}`. */
41
41
  export declare const PATH_TEMPLATE_PARAMETER: RegExp;
42
+ /**
43
+ * Description of the `default` response emitted for an operation that
44
+ * documents no responses. No status code is invented for it.
45
+ */
46
+ export declare const UNDOCUMENTED_RESPONSE_DESCRIPTION = "Undocumented response";
42
47
  /**
43
48
  * Default server URL.
44
49
  */
@@ -46,6 +46,11 @@ export const STATUS_CODE_CATEGORIES = {
46
46
  export const RESPONSE_KEY_PATTERN = /^(default|[1-5](XX|\d{2}))$/;
47
47
  /** OpenAPI path template parameter, e.g. `{orderId}`. */
48
48
  export const PATH_TEMPLATE_PARAMETER = /\{([^{}]+)\}/g;
49
+ /**
50
+ * Description of the `default` response emitted for an operation that
51
+ * documents no responses. No status code is invented for it.
52
+ */
53
+ export const UNDOCUMENTED_RESPONSE_DESCRIPTION = "Undocumented response";
49
54
  /**
50
55
  * Default server URL.
51
56
  */
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @zudojs/openapi/openApiFromRoutes
3
+ *
4
+ * Generates a document from a route table described by transport-neutral
5
+ * {@link OpenAPIRouteDescriptor}s, so any route source — an `@zudojs/http`
6
+ * router, an `@zudojs/api` registry, a hand-written list — can be documented
7
+ * from what it actually registers.
8
+ */
9
+ export type { OpenAPIRouteDescriptor, OpenAPIDocumentFromRoutesOptions, } from "./openApiFromRoutes.type.js";
10
+ export { createOpenAPIDocumentFromRoutes, createOpenAPIManagerFromRoutes, routeDescriptorToRouteInfo, } from "./openApiFromRoutes.document.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/openapi/openApiFromRoutes
3
+ *
4
+ * Generates a document from a route table described by transport-neutral
5
+ * {@link OpenAPIRouteDescriptor}s, so any route source — an `@zudojs/http`
6
+ * router, an `@zudojs/api` registry, a hand-written list — can be documented
7
+ * from what it actually registers.
8
+ */
9
+ export { createOpenAPIDocumentFromRoutes, createOpenAPIManagerFromRoutes, routeDescriptorToRouteInfo, } from "./openApiFromRoutes.document.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Document generation from a route table.
3
+ */
4
+ import type { OpenAPIDocument } from "../openApiTypes/openApiTypes.core.js";
5
+ import type { RouteInfo } from "../openApiRouting/routeMetadata.type.js";
6
+ import { OpenAPIManager } from "../openApiHttp/openApiHttpAdapter.core.js";
7
+ import type { OpenAPIDocumentFromRoutesOptions, OpenAPIRouteDescriptor } from "./openApiFromRoutes.type.js";
8
+ /**
9
+ * Splits a descriptor into the manager's `{ method, path, metadata }` shape.
10
+ *
11
+ * @throws {OpenAPIRouteError} When the method has no OpenAPI path item field.
12
+ */
13
+ export declare function routeDescriptorToRouteInfo(descriptor: OpenAPIRouteDescriptor): RouteInfo;
14
+ /**
15
+ * Creates an {@link OpenAPIManager} holding `routes`, ready to generate,
16
+ * serialize and serve (`toResponse`, `toUIResponse`). Refresh it later with
17
+ * `manager.setRoutes(descriptors.map(routeDescriptorToRouteInfo))`.
18
+ *
19
+ * @throws {OpenAPIRouteError} On an unsupported method or a duplicate route.
20
+ */
21
+ export declare function createOpenAPIManagerFromRoutes(routes: readonly OpenAPIRouteDescriptor[], options: OpenAPIDocumentFromRoutesOptions): OpenAPIManager;
22
+ /**
23
+ * Generates an OpenAPI document from a list of route descriptors.
24
+ *
25
+ * This is the transport-neutral entry point: `@zudojs/http` feeds it the
26
+ * routes a router actually has registered, and any other source can feed it
27
+ * the same structural {@link OpenAPIRouteDescriptor}s.
28
+ *
29
+ * ```ts
30
+ * const document = createOpenAPIDocumentFromRoutes(
31
+ * [{ method: "GET", path: "/users/:id", summary: "Get a user",
32
+ * responses: { "200": { schema: userSchema } } }],
33
+ * { info: { title: "Users", version: "1.0.0" }, validate: true },
34
+ * );
35
+ * ```
36
+ *
37
+ * @throws {OpenAPIRouteError} On an unsupported method, an inexpressible
38
+ * path or a duplicate route.
39
+ * @throws {OpenAPIValidationError} When `validate` is set and the document
40
+ * is invalid.
41
+ */
42
+ export declare function createOpenAPIDocumentFromRoutes(routes: readonly OpenAPIRouteDescriptor[], options: OpenAPIDocumentFromRoutesOptions): OpenAPIDocument;
43
+ //# sourceMappingURL=openApiFromRoutes.document.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Document generation from a route table.
3
+ */
4
+ import { isOpenAPIMethod } from "../openApiRouting/routeConverter.core.js";
5
+ import { isSchemaDefinition } from "../openApiSchema/schemaInput.core.js";
6
+ import { OpenAPIManager } from "../openApiHttp/openApiHttpAdapter.core.js";
7
+ import { OpenAPIRouteError } from "../openApiErrors/openApiError.types.js";
8
+ /**
9
+ * Splits a descriptor into the manager's `{ method, path, metadata }` shape.
10
+ *
11
+ * @throws {OpenAPIRouteError} When the method has no OpenAPI path item field.
12
+ */
13
+ export function routeDescriptorToRouteInfo(descriptor) {
14
+ const { method, path, ...openapi } = descriptor;
15
+ if (typeof method !== "string" || !isOpenAPIMethod(method)) {
16
+ throw new OpenAPIRouteError(`HTTP method "${String(method)}" of route ${String(path)} has no OpenAPI path item field.`, { metadata: { method, path } });
17
+ }
18
+ return { method: method.toLowerCase(), path, metadata: { openapi } };
19
+ }
20
+ /**
21
+ * Creates an {@link OpenAPIManager} holding `routes`, ready to generate,
22
+ * serialize and serve (`toResponse`, `toUIResponse`). Refresh it later with
23
+ * `manager.setRoutes(descriptors.map(routeDescriptorToRouteInfo))`.
24
+ *
25
+ * @throws {OpenAPIRouteError} On an unsupported method or a duplicate route.
26
+ */
27
+ export function createOpenAPIManagerFromRoutes(routes, options) {
28
+ const { validate: _validate, securitySchemes, schemas, ...managerOptions } = options;
29
+ const manager = new OpenAPIManager({ ...managerOptions, cacheTtlMs: 0 });
30
+ for (const [name, scheme] of Object.entries(securitySchemes ?? {})) {
31
+ manager.addSecurityScheme(name, scheme);
32
+ }
33
+ for (const [name, schema] of Object.entries(schemas ?? {})) {
34
+ if (isSchemaDefinition(schema))
35
+ manager.addSchema(name, schema);
36
+ else
37
+ manager.addRawSchema(name, schema);
38
+ }
39
+ return manager.setRoutes(routes.map(routeDescriptorToRouteInfo));
40
+ }
41
+ /**
42
+ * Generates an OpenAPI document from a list of route descriptors.
43
+ *
44
+ * This is the transport-neutral entry point: `@zudojs/http` feeds it the
45
+ * routes a router actually has registered, and any other source can feed it
46
+ * the same structural {@link OpenAPIRouteDescriptor}s.
47
+ *
48
+ * ```ts
49
+ * const document = createOpenAPIDocumentFromRoutes(
50
+ * [{ method: "GET", path: "/users/:id", summary: "Get a user",
51
+ * responses: { "200": { schema: userSchema } } }],
52
+ * { info: { title: "Users", version: "1.0.0" }, validate: true },
53
+ * );
54
+ * ```
55
+ *
56
+ * @throws {OpenAPIRouteError} On an unsupported method, an inexpressible
57
+ * path or a duplicate route.
58
+ * @throws {OpenAPIValidationError} When `validate` is set and the document
59
+ * is invalid.
60
+ */
61
+ export function createOpenAPIDocumentFromRoutes(routes, options) {
62
+ return createOpenAPIManagerFromRoutes(routes, options).generate(options.validate ?? false);
63
+ }
64
+ //# sourceMappingURL=openApiFromRoutes.document.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Transport-neutral input for generating a document from a route table.
3
+ */
4
+ import type { OpenAPIInfo, OpenAPISecurityScheme } from "../openApiTypes/openApiTypes.core.js";
5
+ import type { RouteOpenAPIMetadata } from "../openApiRouting/routeMetadata.type.js";
6
+ import type { OpenAPIManagerOptions } from "../openApiHttp/openApiHttpAdapter.core.js";
7
+ /**
8
+ * One operation, described structurally so that any route source can feed
9
+ * {@link createOpenAPIDocumentFromRoutes}: an `@zudojs/http` router, an
10
+ * `@zudojs/api` operation registry, or a hand-written list.
11
+ *
12
+ * It is {@link RouteOpenAPIMetadata} plus the operation's method and path,
13
+ * flattened into one object:
14
+ *
15
+ * - `method` — any case (`"GET"`, `"post"`); it must be one an OpenAPI path
16
+ * item can carry (`get put post delete options head patch trace`).
17
+ * - `path` — `/users/:id` or `/users/{id}`. Optional segments, typed or
18
+ * regex-constrained `:name(...)` segments and wildcards have no OpenAPI
19
+ * spelling; the source resolves them (see `inferredParameters`) before
20
+ * handing the path over.
21
+ * - `summary`, `description`, `operationId`, `tags`, `deprecated`,
22
+ * `security` (`[]` marks a public operation), `servers`, `externalDocs`.
23
+ * - `params`, `query`, `headers`, `cookies` — one object schema each; every
24
+ * property becomes a parameter. A `@zudojs/schema` schema is converted, any
25
+ * other object is used as an OpenAPI schema.
26
+ * - `body` — a schema, or `{ schema, contentType?, required?, description? }`.
27
+ * - `responses` — keyed by status, `NXX` range or `default`; each entry is a
28
+ * Response Object or `{ schema, description?, contentType?, headers? }`.
29
+ * - `parameters` — explicit parameters, highest precedence.
30
+ * - `inferredParameters` — parameters the source derived itself, lowest
31
+ * precedence.
32
+ * - `hidden: true` — leave the operation out of the document.
33
+ *
34
+ * Every path template slot is documented even when nothing declares it.
35
+ */
36
+ export interface OpenAPIRouteDescriptor extends RouteOpenAPIMetadata {
37
+ /** HTTP method, any case. */
38
+ readonly method: string;
39
+ /** Route path, `/users/:id` or `/users/{id}`. */
40
+ readonly path: string;
41
+ }
42
+ /** Options for {@link createOpenAPIDocumentFromRoutes}. */
43
+ export interface OpenAPIDocumentFromRoutesOptions extends Omit<OpenAPIManagerOptions, "info" | "cacheTtlMs" | "now"> {
44
+ /** Document metadata (title and version are required by the spec). */
45
+ readonly info: OpenAPIInfo;
46
+ /** Validate the result and throw `OpenAPIValidationError` if invalid. */
47
+ readonly validate?: boolean;
48
+ /** Registered under `components.securitySchemes`. */
49
+ readonly securitySchemes?: Readonly<Record<string, OpenAPISecurityScheme>>;
50
+ /**
51
+ * Component schemas, registered under `components.schemas`. A
52
+ * `@zudojs/schema` schema is converted; any other object is used as-is.
53
+ */
54
+ readonly schemas?: Readonly<Record<string, unknown>>;
55
+ }
56
+ //# sourceMappingURL=openApiFromRoutes.type.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Transport-neutral input for generating a document from a route table.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=openApiFromRoutes.type.js.map
@@ -17,8 +17,20 @@ export interface OpenAPIManagerOptions {
17
17
  * invalidates the cache regardless.
18
18
  */
19
19
  readonly cacheTtlMs?: number;
20
- /** Reports what a schema conversion could not express. */
20
+ /**
21
+ * Reports what a schema conversion could not express: component schemas
22
+ * under their component name, route-declared schemas and parameters under
23
+ * `"routes"` once per generation.
24
+ */
21
25
  readonly onSchemaWarning?: (name: string, warnings: readonly string[]) => void;
26
+ /**
27
+ * Receives each route warning of a generation as it is produced, each
28
+ * prefixed with the route (`DELETE /users/:id: ...`): route-declared
29
+ * schemas or parameters that could not be expressed, and operations that
30
+ * document no responses. The same messages are returned by
31
+ * {@link OpenAPIManager.routeWarnings}.
32
+ */
33
+ readonly onRouteWarning?: (message: string) => void;
22
34
  /** Supplies the clock, for tests. */
23
35
  readonly now?: () => number;
24
36
  /**
@@ -59,6 +71,9 @@ export declare class OpenAPIManager {
59
71
  private readonly logo;
60
72
  /** True when `branding` was a caller-supplied logo rather than the default. */
61
73
  private readonly customLogo;
74
+ private readonly onSchemaWarning;
75
+ private readonly onRouteWarning;
76
+ private lastRouteWarnings;
62
77
  private cachedDocument?;
63
78
  private cachedAt;
64
79
  /** Whether the cached document was produced by a validating generate. */
@@ -76,6 +91,12 @@ export declare class OpenAPIManager {
76
91
  addRoute(route: RouteInfo): this;
77
92
  /** Registers a route, replacing any existing one for the same method+path. */
78
93
  setRoute(route: RouteInfo): this;
94
+ /**
95
+ * Replaces every registered route with `routes`, for keeping the document
96
+ * in step with a live route table. Duplicates are rejected before anything
97
+ * is replaced, so a failed call leaves the previous routes in place.
98
+ */
99
+ setRoutes(routes: readonly RouteInfo[]): this;
79
100
  /** Removes a route. Returns whether one was removed. */
80
101
  removeRoute(method: OpenAPIHttpMethod, path: string): boolean;
81
102
  /**
@@ -89,6 +110,12 @@ export declare class OpenAPIManager {
89
110
  addRawSchema(name: string, schema: OpenAPISchema): this;
90
111
  /** Conversion warnings, keyed by component name. */
91
112
  schemaWarnings(): ReadonlyMap<string, readonly string[]>;
113
+ /**
114
+ * The route warnings of the last generation, each prefixed with the route
115
+ * (`GET /users/:id: ...`): what could not be expressed for route-declared
116
+ * schemas and parameters, and operations that document no responses.
117
+ */
118
+ routeWarnings(): readonly string[];
92
119
  /**
93
120
  * Builds the document from the registered routes and components.
94
121
  *
@@ -24,6 +24,9 @@ export class OpenAPIManager {
24
24
  logo;
25
25
  /** True when `branding` was a caller-supplied logo rather than the default. */
26
26
  customLogo;
27
+ onSchemaWarning;
28
+ onRouteWarning;
29
+ lastRouteWarnings = [];
27
30
  cachedDocument;
28
31
  cachedAt = 0;
29
32
  /** Whether the cached document was produced by a validating generate. */
@@ -39,6 +42,8 @@ export class OpenAPIManager {
39
42
  version: options.version ?? DEFAULT_OPENAPI_VERSION,
40
43
  onWarning: options.onSchemaWarning,
41
44
  });
45
+ this.onSchemaWarning = options.onSchemaWarning;
46
+ this.onRouteWarning = options.onRouteWarning;
42
47
  this.cacheTtlMs = options.cacheTtlMs ?? DOCUMENT_CACHE_TTL_MS;
43
48
  this.now = options.now ?? (() => Date.now());
44
49
  this.logo =
@@ -95,6 +100,20 @@ export class OpenAPIManager {
95
100
  this.scanner.setRoute(route);
96
101
  return this.invalidateCache();
97
102
  }
103
+ /**
104
+ * Replaces every registered route with `routes`, for keeping the document
105
+ * in step with a live route table. Duplicates are rejected before anything
106
+ * is replaced, so a failed call leaves the previous routes in place.
107
+ */
108
+ setRoutes(routes) {
109
+ const next = new OpenAPIRouteScannerImpl();
110
+ for (const route of routes)
111
+ next.addRoute(route);
112
+ this.scanner.clear();
113
+ for (const route of routes)
114
+ this.scanner.addRoute(route);
115
+ return this.invalidateCache();
116
+ }
98
117
  /** Removes a route. Returns whether one was removed. */
99
118
  removeRoute(method, path) {
100
119
  const removed = this.scanner.removeRoute(method, path);
@@ -122,6 +141,14 @@ export class OpenAPIManager {
122
141
  schemaWarnings() {
123
142
  return this.schemas.warnings();
124
143
  }
144
+ /**
145
+ * The route warnings of the last generation, each prefixed with the route
146
+ * (`GET /users/:id: ...`): what could not be expressed for route-declared
147
+ * schemas and parameters, and operations that document no responses.
148
+ */
149
+ routeWarnings() {
150
+ return this.lastRouteWarnings;
151
+ }
125
152
  /* ── Generation ──────────────────────────────────────────────────────── */
126
153
  /**
127
154
  * Builds the document from the registered routes and components.
@@ -134,9 +161,21 @@ export class OpenAPIManager {
134
161
  // hidden, so `removeRoute()` had no effect once a document had been
135
162
  // generated.
136
163
  this.registry.clearRoutes();
137
- for (const route of this.scanner.scan()) {
164
+ const routeWarnings = [];
165
+ const scanned = this.scanner.scan({
166
+ version: this.registry.version,
167
+ onWarning: (message) => {
168
+ routeWarnings.push(message);
169
+ this.onRouteWarning?.(message);
170
+ },
171
+ });
172
+ for (const route of scanned) {
138
173
  this.registry.setRoute(route);
139
174
  }
175
+ this.lastRouteWarnings = Object.freeze(routeWarnings);
176
+ if (routeWarnings.length > 0) {
177
+ this.onSchemaWarning?.("routes", this.lastRouteWarnings);
178
+ }
140
179
  // Brand the document unless the caller supplied a logo or opted out.
141
180
  const info = this.registry.getInfo();
142
181
  if (this.logo && !info["x-logo"]) {
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Route metadata, conversion, and scanning for OpenAPI generation.
5
5
  */
6
- export type { RouteMetadata, RouteOpenAPIMetadata, RouteParameterMetadata, RouteInfo, OpenAPIHttpMethod, } from "./routeMetadata.type.js";
6
+ export type { RouteMetadata, RouteOpenAPIMetadata, RouteParameterMetadata, RouteInfo, OpenAPIHttpMethod, OpenAPISchemaInput, OpenAPIRouteBody, OpenAPIRouteResponse, RouteConversionOptions, } from "./routeMetadata.type.js";
7
7
  export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, } from "./routeConverter.core.js";
8
8
  export { OpenAPIRouteScannerImpl } from "./routeScanner.core.js";
9
+ export { buildOperationParameters } from "./routeSchema.core.js";
10
+ export { buildOperationRequestBody, buildOperationResponses, describeResponseKey, } from "./routeContent.core.js";
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,6 @@
5
5
  */
6
6
  export { toOpenAPIPath, extractPathParameters, convertRouteToOpenAPI, buildResponses, isOpenAPIMethod, ZUDOLIB_TO_OPENAPI_METHODS, } from "./routeConverter.core.js";
7
7
  export { OpenAPIRouteScannerImpl } from "./routeScanner.core.js";
8
+ export { buildOperationParameters } from "./routeSchema.core.js";
9
+ export { buildOperationRequestBody, buildOperationResponses, describeResponseKey, } from "./routeContent.core.js";
8
10
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
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 type { OpenAPIRequestBody, OpenAPIResponses } from "../openApiTypes/openApiTypes.core.js";
8
+ import type { RouteConversionOptions, RouteOpenAPIMetadata } from "./routeMetadata.type.js";
9
+ /** A default description for a response key: `404` → "Not Found". */
10
+ export declare function describeResponseKey(key: string): string;
11
+ /** Builds the Request Body Object from `requestBody` or `body`. */
12
+ export declare function buildOperationRequestBody(meta: RouteOpenAPIMetadata | undefined, context: string, options?: RouteConversionOptions): OpenAPIRequestBody | undefined;
13
+ /**
14
+ * Builds the `responses` object for an operation.
15
+ *
16
+ * Every documented response is carried through; an entry with a `schema` is
17
+ * expanded into a Response Object. A route that documents none gets a
18
+ * `default` "Undocumented response" (`responses` is required) and a warning
19
+ * through `options.onWarning`. No status is invented.
20
+ */
21
+ export declare function buildOperationResponses(meta: RouteOpenAPIMetadata | undefined, context: string, options?: RouteConversionOptions): OpenAPIResponses;
22
+ //# sourceMappingURL=routeContent.core.d.ts.map
@@ -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. Only when a route documents
30
- * none at all is a `200` synthesized, because `responses` is required.
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. Only when a route documents
91
- * none at all is a `200` synthesized, because `responses` is required.
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
- const declared = metadata?.openapi?.responses;
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
- ...(meta?.parameters?.length
117
- ? { parameters: meta.parameters.map(toParameter) }
118
- : {}),
119
- ...(meta?.requestBody ? { requestBody: meta.requestBody } : {}),
120
- ...(meta?.security?.length ? { security: [...meta.security] } : {}),
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: buildResponses(metadata),
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
- /** Converts every registered route into an OpenAPI operation. */
24
- scan(): readonly OpenAPIRoute[];
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
@@ -66,13 +66,17 @@ export class OpenAPIRouteScannerImpl {
66
66
  get size() {
67
67
  return this.routes.size;
68
68
  }
69
- /** Converts every registered route into an OpenAPI operation. */
70
- scan() {
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) {
71
75
  const result = [];
72
76
  for (const route of this.routes.values()) {
73
77
  if (route.metadata?.openapi?.hidden === true)
74
78
  continue;
75
- const converted = convertRouteToOpenAPI(route.method, route.path, route.metadata);
79
+ const converted = convertRouteToOpenAPI(route.method, route.path, route.metadata, options);
76
80
  result.push({
77
81
  method: converted.method,
78
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
@@ -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
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Schema-bearing inputs a route or operation may declare.
3
+ */
4
+ import type { OpenAPIHeader, OpenAPIReference, OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
5
+ import type { SchemaInputOptions } from "./schemaInput.core.js";
6
+ /**
7
+ * A schema a route may declare: a `@zudojs/schema` schema (anything carrying
8
+ * a string `_type`, converted with `convertSchema` for the document's
9
+ * version), or an OpenAPI Schema / Reference Object used as-is.
10
+ */
11
+ export type OpenAPISchemaInput = OpenAPISchema | OpenAPIReference | {
12
+ readonly _type: string;
13
+ };
14
+ /**
15
+ * A request body declared by schema rather than as a raw Request Body Object.
16
+ */
17
+ export interface OpenAPIRouteBody {
18
+ /** The body's schema. */
19
+ readonly schema: OpenAPISchemaInput;
20
+ /** Media type(s) the body is accepted as. Default: `application/json`. */
21
+ readonly contentType?: string | readonly string[];
22
+ /** Whether the body is required. Default: `true`. */
23
+ readonly required?: boolean;
24
+ readonly description?: string;
25
+ readonly example?: unknown;
26
+ }
27
+ /**
28
+ * A response declared by schema rather than as a raw Response Object.
29
+ *
30
+ * An entry in `responses` is read as this shape when it has a `schema` key;
31
+ * otherwise it is an OpenAPI Response Object and passes through unchanged.
32
+ */
33
+ export interface OpenAPIRouteResponse {
34
+ /** The response body's schema. */
35
+ readonly schema: OpenAPISchemaInput;
36
+ /** Default: the status code's reason phrase, e.g. "Not Found". */
37
+ readonly description?: string;
38
+ /** Media type(s) the body is sent as. Default: `application/json`. */
39
+ readonly contentType?: string | readonly string[];
40
+ readonly headers?: Readonly<Record<string, OpenAPIHeader>>;
41
+ readonly example?: unknown;
42
+ }
43
+ /**
44
+ * Options for converting a route into an operation: the version declared
45
+ * schemas are converted for, and a sink for what they could not express.
46
+ */
47
+ export type RouteConversionOptions = SchemaInputOptions;
48
+ //# sourceMappingURL=schemaInput.type.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Schema-bearing inputs a route or operation may declare.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=schemaInput.type.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/openapi",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "OpenAPI 3.0 and 3.1 specification generation, validation, and serialization for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -26,13 +26,14 @@
26
26
  "!dist/.tsbuildinfo"
27
27
  ],
28
28
  "dependencies": {
29
- "@zudojs/constants": "1.1.1",
30
- "@zudojs/errors": "1.2.0"
29
+ "@zudojs/constants": "1.1.3",
30
+ "@zudojs/errors": "1.3.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^26.4.1",
33
+ "@types/node": "^26.6.2",
34
+ "@zudojs/schema": "1.2.2",
34
35
  "typescript": "7.0.2",
35
- "vitest": "^4.1.11"
36
+ "vitest": "^5.0.1"
36
37
  },
37
38
  "engines": {
38
39
  "node": ">=24.0.0"
@@ -46,7 +47,7 @@
46
47
  "swagger",
47
48
  "api-docs"
48
49
  ],
49
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
50
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-openapi",
50
51
  "bugs": {
51
52
  "url": "https://github.com/oyinlola-tech/zudo/issues"
52
53
  },