@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.
Files changed (35) 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 +46 -10
  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/schemaConverter.core.d.ts +2 -0
  30. package/dist/openApiSchema/schemaConverter.core.js +9 -4
  31. package/dist/openApiSchema/schemaInput.core.d.ts +28 -0
  32. package/dist/openApiSchema/schemaInput.core.js +36 -0
  33. package/dist/openApiSchema/schemaInput.type.d.ts +48 -0
  34. package/dist/openApiSchema/schemaInput.type.js +5 -0
  35. 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. 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
@@ -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 = `${route.method.toLowerCase()}:${route.path}`;
16
- if (this.routes.has(key)) {
17
- throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered.`, { metadata: { method: route.method, path: route.path } });
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(`${route.method.toLowerCase()}:${route.path}`, route);
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(`${method.toLowerCase()}:${path}`);
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(`${method.toLowerCase()}:${path}`);
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
- /** Converts every registered route into an OpenAPI operation. */
38
- 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) {
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
- const unknownKeys = c["unknownKeys"];
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 === "strip" || unknownKeys === "strict"
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