@orpc/openapi 2.0.0-beta.2 → 2.0.0-beta.20

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 (29) hide show
  1. package/README.md +32 -21
  2. package/dist/adapters/fetch/index.d.mts +2 -2
  3. package/dist/adapters/fetch/index.d.ts +2 -2
  4. package/dist/adapters/fetch/index.mjs +3 -3
  5. package/dist/adapters/node/index.d.mts +2 -2
  6. package/dist/adapters/node/index.d.ts +2 -2
  7. package/dist/adapters/node/index.mjs +2 -2
  8. package/dist/adapters/standard/index.d.mts +2 -2
  9. package/dist/adapters/standard/index.d.ts +2 -2
  10. package/dist/adapters/standard/index.mjs +3 -3
  11. package/dist/extensions/route.d.mts +2 -2
  12. package/dist/extensions/route.d.ts +2 -2
  13. package/dist/index.d.mts +55 -34
  14. package/dist/index.d.ts +55 -34
  15. package/dist/index.mjs +682 -661
  16. package/dist/plugins/index.d.mts +1 -1
  17. package/dist/plugins/index.d.ts +1 -1
  18. package/dist/shared/{openapi.BYcWgK1j.d.ts → openapi.B6hEbRyF.d.ts} +18 -14
  19. package/dist/shared/{openapi.C7m7NAmH.d.mts → openapi.BOOA-bde.d.mts} +1 -1
  20. package/dist/shared/{openapi.C7m7NAmH.d.ts → openapi.BOOA-bde.d.ts} +1 -1
  21. package/dist/shared/{openapi.XeW6_5tm.d.mts → openapi.BafbB3uM.d.mts} +18 -14
  22. package/dist/shared/{openapi.CYgMBSUF.d.mts → openapi.ByT4oUeY.d.mts} +2 -2
  23. package/dist/shared/{openapi.CYgMBSUF.d.ts → openapi.ByT4oUeY.d.ts} +2 -2
  24. package/dist/shared/{openapi.B2SK0ZAr.mjs → openapi.C-p_Q2lb.mjs} +4 -4
  25. package/dist/shared/{openapi.DR-y5p-D.mjs → openapi.CVgUshDP.mjs} +28 -25
  26. package/dist/shared/{openapi.BQzzr4-4.d.ts → openapi.DNNo0V-l.d.ts} +23 -9
  27. package/dist/shared/{openapi.BcEtAxQj.d.mts → openapi.hg_rhZ4x.d.mts} +23 -9
  28. package/dist/shared/{openapi.DmAa7YPO.mjs → openapi.zZH_UksW.mjs} +7 -4
  29. package/package.json +28 -12
package/dist/index.mjs CHANGED
@@ -1,594 +1,131 @@
1
1
  export { B as BracketNotationSerializer } from './shared/openapi.Bt87OzTt.mjs';
2
- import { createContractCaller, getEventIteratorSchemaDetails } from '@orpc/contract';
3
- import { O as OpenAPISerializer, D as DEFAULT_OPENAPI_METHOD, g as getDynamicPathParams, a as DEFAULT_OPENAPI_INPUT_STRUCTURE, c as DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, b as DEFAULT_OPENAPI_OUTPUT_STRUCTURE } from './shared/openapi.DmAa7YPO.mjs';
4
- export { d as OpenAPIJsonSerializer } from './shared/openapi.DmAa7YPO.mjs';
5
- import { g as getOpenAPIMeta } from './shared/openapi.B9PQzqBn.mjs';
6
- export { o as openapi } from './shared/openapi.B9PQzqBn.mjs';
2
+ import { createContractClientFactory, getAsyncIteratorObjectSchemaDetails, ProcedureContract, resolveMetaPlugins } from '@orpc/contract';
3
+ import { D as DEFAULT_OPENAPI_METHOD, a as DEFAULT_OPENAPI_INPUT_STRUCTURE, c as DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, b as DEFAULT_OPENAPI_OUTPUT_STRUCTURE, i as isBodylessMethod, O as OpenAPISerializer, g as getDynamicPathParams } from './shared/openapi.zZH_UksW.mjs';
4
+ export { d as OpenAPIJsonSerializer } from './shared/openapi.zZH_UksW.mjs';
5
+ import { g as getOpenAPIMeta, o as openapi } from './shared/openapi.B9PQzqBn.mjs';
7
6
  import { COMMON_ERROR_STATUS_MAP } from '@orpc/client';
8
7
  export { COMMON_ERROR_STATUS_MAP } from '@orpc/client';
9
- import { DelegatingJsonSchemaConverter, StandardJsonSchemaConverter, combineJsonSchemasWithComposition, isUnconstrainedSchema, extractJsonObjectSchemaEntries, combineJsonObjectSchemaEntries, isJsonPrimitiveSchema, matchArrayableJsonSchema, flattenJsonUnionSchema, ensureJsonSchemaObject, isJsonFileSchema, mapJsonSchemaRefs, encodeJsonPointerSegment, decodeJsonPointerSegment } from '@orpc/json-schema';
10
- import { walkProcedureContractsAsync, DEFAULT_SUCCESS_STATUS, DEFAULT_ERROR_STATUS } from '@orpc/server';
11
- import { toArray, clone, value, pathToHttpPath, mergeHttpPath, stringifyJSON, findDeepMatches, isPlainObject, isDeepEqual } from '@orpc/shared';
8
+ import { encodeJsonPointerSegment, ensureJsonSchemaObject, mapJsonSchemaRefs, visitJsonSchemaRefs, decodeJsonPointerSegment, isUnconstrainedSchema, combineJsonSchemasWithComposition, extractJsonObjectSchemaEntries, combineJsonObjectSchemaEntries, flattenJsonUnionSchema, isJsonPrimitiveSchema, matchArrayableJsonSchema, isJsonFileSchema, DelegatingJsonSchemaConverter, StandardJsonSchemaConverter } from '@orpc/json-schema';
9
+ import { DEFAULT_SUCCESS_STATUS, DEFAULT_ERROR_STATUS, walkProcedureContractsAsync } from '@orpc/server';
10
+ import { value, isDeepEqual, findDeepMatches, isPlainObject, stringifyJSON, toArray, clone, pathToHttpPath, mergeHttpPath, isTypescriptObject } from '@orpc/shared';
12
11
  import '@standardserver/core';
13
12
 
14
- function createContractJsonifiedCaller(link, options = {}) {
15
- return createContractCaller(link, options);
13
+ function createContractJsonifiedClientFactory(link, options = {}) {
14
+ return createContractClientFactory(link, options);
16
15
  }
17
16
 
18
- class OpenAPIGeneratorError extends TypeError {
19
- }
20
- class OpenAPIGenerator {
21
- serializer;
22
- converter;
23
- constructor(options = {}) {
24
- this.serializer = options.serializer ?? new OpenAPISerializer();
25
- this.converter = new DelegatingJsonSchemaConverter([
26
- ...toArray(options.converters),
27
- new StandardJsonSchemaConverter()
28
- ]);
29
- }
30
- async generate(router, options = {}) {
31
- const doc = {
32
- ...clone(options.base),
33
- openapi: options.base?.openapi ?? "3.1.2",
34
- info: options.base?.info ?? { title: "API Reference", version: "0.0.0" }
35
- };
36
- const errors = [];
37
- await walkProcedureContractsAsync(router, async (contract, path) => {
38
- if (value(options.filter, contract, path) === false) {
39
- return;
40
- }
41
- try {
42
- const def = contract["~orpc"];
43
- const meta = getOpenAPIMeta(contract);
44
- const method = (meta?.method ?? DEFAULT_OPENAPI_METHOD).toLowerCase();
45
- const postPath = meta?.path ?? pathToHttpPath(path);
46
- const httpPath = meta?.prefix ? mergeHttpPath(meta.prefix, postPath) : postPath;
47
- const dynamicPathParams = getDynamicPathParams(httpPath);
48
- const openApiPath = toOpenAPIPath(httpPath, dynamicPathParams);
49
- let operationRef;
50
- if (meta?.spec !== void 0 && typeof meta.spec !== "function") {
51
- operationRef = meta.spec;
52
- } else {
53
- operationRef = {
54
- operationId: meta?.operationId ?? path.join("."),
55
- summary: meta?.summary,
56
- description: meta?.description,
57
- deprecated: meta?.deprecated,
58
- tags: meta?.tags?.map((tag) => tag)
59
- };
60
- await this.request(doc, operationRef, def, meta, dynamicPathParams, options, path);
61
- await this.successResponse(doc, operationRef, def, meta, options, path);
62
- await this.errorResponse(doc, operationRef, def, meta, options);
63
- }
64
- if (typeof meta?.spec === "function") {
65
- operationRef = meta.spec(operationRef);
66
- }
67
- doc.paths ??= {};
68
- doc.paths[openApiPath] ??= {};
69
- doc.paths[openApiPath][method] = operationRef;
70
- } catch (e) {
71
- if (!(e instanceof OpenAPIGeneratorError)) {
72
- throw e;
73
- }
74
- errors.push(
75
- `[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${path.join(".")}
76
- ${e.message}`
77
- );
78
- }
79
- });
80
- if (errors.length) {
81
- throw new OpenAPIGeneratorError(
82
- `Some error occurred during OpenAPI generation:
83
-
84
- ${errors.join("\n\n")}`
85
- );
86
- }
87
- return this.serializer.serialize(doc, { asFormData: false, useFormDataForBlobFields: false });
88
- }
89
- async convertSchema(schema, direction) {
90
- const [jsonSchema, optional] = await this.converter.convert(schema, direction);
91
- return [strip$schemaField(jsonSchema), optional];
92
- }
93
- async convertSchemas(schemas, direction) {
94
- if (!schemas || schemas.length <= 1) {
95
- return this.convertSchema(schemas?.[0], direction);
96
- }
97
- const results = await Promise.all(schemas.map((s) => this.convertSchema(s, direction)));
98
- const allOfSchemas = [];
99
- let optional = true;
100
- for (const [jsonSchema, opt] of results) {
101
- allOfSchemas.push(jsonSchema);
102
- if (!opt) {
103
- optional = false;
104
- }
105
- }
106
- return [combineJsonSchemasWithComposition("allOf", allOfSchemas), optional];
107
- }
108
- async request(doc, ref, def, meta, dynamicPathParams, options, path) {
109
- const method = meta?.method ?? DEFAULT_OPENAPI_METHOD;
110
- const inputStructure = meta?.inputStructure ?? DEFAULT_OPENAPI_INPUT_STRUCTURE;
111
- const inputSchemas = def.inputSchemas;
112
- if (inputStructure === "compact") {
113
- const eventIteratorDetails = getEventIteratorDetails(inputSchemas);
114
- if (eventIteratorDetails) {
115
- const [yieldSchemas, returnSchemas] = eventIteratorDetails;
116
- const yieldResult = await this.convertSchemas(yieldSchemas, "input");
117
- const returnResult = await this.convertSchemas(returnSchemas, "input");
118
- ref.requestBody = {
119
- required: true,
120
- content: toEventIteratorContent(yieldResult, returnResult, doc, options)
121
- };
122
- return;
123
- }
124
- }
125
- const dynamicParams = dynamicPathParams?.map((v) => v.parameterName);
126
- const [schema, optional] = await this.convertSchemas(inputSchemas, "input");
127
- if (isUnconstrainedSchema(schema) && !dynamicParams?.length) {
128
- return;
129
- }
130
- const objectSchemaEntries = extractJsonObjectSchemaEntries(schema);
131
- if (!objectSchemaEntries) {
132
- if (inputStructure === "detailed") {
133
- throw new OpenAPIGeneratorError(
134
- `Procedure at path "${path.join(".")}" has inputStructure "detailed" but its input schema is not an object.
135
- Expected shape: { params?: Record<string, unknown>, query?: Record<string, unknown>, headers?: Record<string, unknown>, body?: unknown }`
136
- );
137
- }
138
- if (method === "GET") {
139
- throw new OpenAPIGeneratorError(
140
- `Procedure at path "${path.join(".")}" uses method "GET" but its input schema is not an object.
141
- GET procedures map all input fields to query parameters, so the schema must be an object.
142
- Expected: Record<string, unknown>`
143
- );
144
- }
145
- }
146
- const paramsObjectSchemaEntries = inputStructure === "compact" ? objectSchemaEntries?.filter(([name]) => dynamicParams?.includes(name)) : extractJsonObjectSchemaEntries(objectSchemaEntries?.find(([name]) => name === "params")?.[1] ?? false);
147
- const queryObjectSchemaEntries = inputStructure === "compact" ? method === "GET" ? objectSchemaEntries?.filter(([name]) => !dynamicParams?.includes(name)) : void 0 : extractJsonObjectSchemaEntries(objectSchemaEntries?.find(([name]) => name === "query")?.[1] ?? false);
148
- const headersObjectSchemaEntries = inputStructure === "compact" ? void 0 : extractJsonObjectSchemaEntries(objectSchemaEntries?.find(([name]) => name === "headers")?.[1] ?? false);
149
- const bodySchema = inputStructure === "compact" ? method === "GET" || method === "HEAD" ? void 0 : !dynamicParams?.length ? schema : objectSchemaEntries ? combineJsonObjectSchemaEntries(objectSchemaEntries?.filter(([name]) => !dynamicParams?.includes(name))) : void 0 : objectSchemaEntries?.find(([name]) => name === "body")?.[1];
150
- if (dynamicParams?.length) {
151
- if (!paramsObjectSchemaEntries) {
152
- throw new OpenAPIGeneratorError(
153
- `Procedure at path "${path.join(".")}" has dynamic path params (${dynamicParams.map((p) => p).join(", ")}) but its input schema is not an object.
154
- Each dynamic param must appear as a required key in the schema.`
155
- );
156
- }
157
- dynamicParams.forEach((name) => {
158
- const entry = paramsObjectSchemaEntries.find(([n]) => n === name);
159
- if (!entry) {
160
- throw new OpenAPIGeneratorError(
161
- `Procedure at path "${path.join(".")}" is missing dynamic param "${name}" in its input schema.
162
- Route params: ${dynamicParams.map((p) => `{${p}}`).join(", ")}
163
- Schema keys: ${paramsObjectSchemaEntries.map(([n]) => n).join(", ") || "(none)"}`
164
- );
165
- }
166
- if (entry[2]) {
167
- throw new OpenAPIGeneratorError(
168
- `Procedure at path "${path.join(".")}" has dynamic param "${name}" marked as optional in its input schema, but path params must always be required in OpenAPI.`
169
- );
170
- }
171
- ref.parameters ??= [];
172
- ref.parameters.push({
173
- in: "path",
174
- required: true,
175
- name,
176
- schema: toOpenAPISchema(entry[1], doc, options)
177
- });
178
- });
179
- }
180
- if (queryObjectSchemaEntries) {
181
- queryObjectSchemaEntries.forEach(([name, schema2, optional2]) => {
182
- const style = meta?.queryStyles?.[name];
183
- const parameter = {
184
- in: "query",
185
- name,
186
- schema: toOpenAPISchema(schema2, doc, options),
187
- allowEmptyValue: true,
188
- allowReserved: true
189
- };
190
- if (!optional2) {
191
- parameter.required = true;
192
- }
193
- if (style === "comma-delimited-array" || style === "comma-delimited-object") {
194
- parameter.explode = false;
195
- } else if (style === "pipe-delimited-array" || style === "pipe-delimited-object") {
196
- parameter.style = "pipeDelimited";
197
- } else if (style === "space-delimited-array" || style === "space-delimited-object") {
198
- parameter.style = "spaceDelimited";
199
- } else if (style === "json") {
200
- parameter.content = {
201
- "application/json": { schema: parameter.schema }
202
- };
203
- delete parameter.schema;
204
- } else if (style === void 0) {
205
- if (!isJsonPrimitiveSchema(schema2)) {
206
- const arrayable = matchArrayableJsonSchema(schema2);
207
- if (!arrayable || !isJsonPrimitiveSchema(arrayable[0])) {
208
- parameter.style = "deepObject";
209
- parameter.explode = true;
210
- }
211
- }
212
- } else ;
213
- ref.parameters ??= [];
214
- ref.parameters.push(parameter);
215
- });
216
- }
217
- if (headersObjectSchemaEntries) {
218
- headersObjectSchemaEntries.forEach(([name, schema2, optional2]) => {
219
- ref.parameters ??= [];
220
- ref.parameters.push({
221
- in: "header",
222
- name,
223
- required: optional2 ? void 0 : true,
224
- schema: toOpenAPISchema(schema2, doc, options)
225
- });
226
- });
227
- }
228
- if (bodySchema !== void 0) {
229
- const bodyOptional = inputStructure === "compact" ? !dynamicParams?.length ? optional : objectSchemaEntries?.filter(([name]) => !dynamicParams?.includes(name)).every(([, , optional2]) => optional2) : objectSchemaEntries?.find(([name]) => name === "body")?.[2];
230
- ref.requestBody = {
231
- required: bodyOptional ? void 0 : true,
232
- content: toBodyContent(bodySchema, doc, options)
233
- };
234
- }
235
- }
236
- async successResponse(doc, ref, def, meta, options, path) {
237
- const outputSchemas = def.outputSchemas;
238
- const status = meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
239
- const description = meta?.successDescription ?? DEFAULT_OPENAPI_SUCCESS_DESCRIPTION;
240
- const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE;
241
- if (outputStructure === "compact") {
242
- const eventDetails = getEventIteratorDetails(outputSchemas);
243
- if (eventDetails) {
244
- const [yieldSchemas, returnSchemas] = eventDetails;
245
- const yieldResult = await this.convertSchemas(yieldSchemas, "output");
246
- const returnResult = await this.convertSchemas(returnSchemas, "output");
247
- ref.responses ??= {};
248
- ref.responses[status] = {
249
- description,
250
- content: toEventIteratorContent(yieldResult, returnResult, doc, options)
251
- };
252
- return;
253
- }
254
- }
255
- const [schema] = await this.convertSchemas(outputSchemas, "output");
256
- if (isUnconstrainedSchema(schema) || outputStructure === "compact") {
257
- ref.responses ??= {};
258
- ref.responses[status] = {
259
- description,
260
- content: toBodyContent(schema, doc, options)
261
- };
262
- return;
263
- }
264
- const schemasByStatus = /* @__PURE__ */ new Map();
265
- for (const item of flattenJsonUnionSchema(schema)) {
266
- const objectSchemaEntries = extractJsonObjectSchemaEntries(item);
267
- if (!objectSchemaEntries) {
268
- throw new OpenAPIGeneratorError(
269
- `Procedure at path "${path.join(".")}" has outputStructure "detailed" but its output schema is not an object.
270
- Expected shape: { status: number (200-299), headers?: Record<string, unknown>, body?: unknown }`
271
- );
272
- }
273
- const statusSchema = objectSchemaEntries?.find(([name]) => name === "status")?.[1];
274
- if (statusSchema !== void 0 && (typeof statusSchema !== "object" || !Number.isInteger(statusSchema.const) || statusSchema.const < 200 || statusSchema.const >= 300)) {
275
- throw new OpenAPIGeneratorError(
276
- `Procedure at path "${path.join(".")}" has an invalid "status" field in its outputStructure "detailed" schema.
277
- Expected: a const integer in the 200-299 range
278
- Received: ${stringifyJSON(statusSchema)}`
279
- );
280
- }
281
- const status2 = (statusSchema?.const || void 0) ?? meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
282
- const description2 = statusSchema?.description;
283
- const schemas = schemasByStatus.get(status2);
284
- const headersSchema = objectSchemaEntries?.find(([name]) => name === "headers")?.[1];
285
- const bodySchema = objectSchemaEntries?.find(([name]) => name === "body")?.[1];
286
- if (schemas) {
287
- schemas.push({ description: description2, headers: headersSchema, body: bodySchema });
288
- } else {
289
- schemasByStatus.set(status2, [{ description: description2, headers: headersSchema, body: bodySchema }]);
290
- }
291
- }
292
- for (const [status2, schemas] of schemasByStatus.entries()) {
293
- const descriptions = schemas.map(({ description: description2 }) => description2).filter((d) => d !== void 0);
294
- const responseObject = {
295
- description: descriptions.length ? descriptions.join(", ") : description
296
- };
297
- const bodySchemas = schemas.map(({ body }) => body).filter((b) => b !== void 0);
298
- if (bodySchemas.length) {
299
- responseObject.content = toBodyContent(combineJsonSchemasWithComposition("anyOf", bodySchemas), doc, options);
300
- }
301
- const headerSchemas = schemas.map(({ headers }) => headers).filter((b) => b !== void 0);
302
- if (headerSchemas.length) {
303
- const entries = extractJsonObjectSchemaEntries(combineJsonSchemasWithComposition("anyOf", headerSchemas));
304
- entries?.forEach(([name, schema2, optional]) => {
305
- responseObject.headers ??= {};
306
- responseObject.headers[name] = {
307
- required: optional ? void 0 : true,
308
- schema: toOpenAPISchema(schema2, doc, options)
309
- };
310
- });
17
+ class OpenAPIComponentRegistry {
18
+ constructor(doc, shouldHoistDef) {
19
+ this.doc = doc;
20
+ this.shouldHoistDef = shouldHoistDef;
21
+ }
22
+ /**
23
+ * Registers `schema` as a component under `preferredName` (or an equivalent/postfixed name)
24
+ * and returns a `$ref` to it. When hoisting is declined via `shouldHoistDef`, the schema is
25
+ * returned in its local `$defs` form instead.
26
+ */
27
+ register(preferredName, schema) {
28
+ const { $defs, ...body } = schema;
29
+ let defName = preferredName;
30
+ if ($defs) {
31
+ for (let i = 2; defName in $defs; i++) {
32
+ defName = `${preferredName}${i}`;
311
33
  }
312
- ref.responses ??= {};
313
- ref.responses[status2] = responseObject;
314
34
  }
35
+ return this.hoistDefs({
36
+ $defs: { ...$defs, [defName]: body },
37
+ $ref: `#/$defs/${encodeJsonPointerSegment(defName)}`
38
+ });
315
39
  }
316
- async errorResponse(doc, ref, def, meta, options) {
317
- const errorStatusMap = options.errorStatusMap ?? COMMON_ERROR_STATUS_MAP;
318
- const errorMap = def.errorMap;
319
- const errorDefinitionsByStatus = /* @__PURE__ */ new Map();
320
- for (const code in errorMap) {
321
- const config = errorMap[code];
322
- if (!config) {
40
+ /**
41
+ * Moves a schema's root-level `$defs` into `doc.components.schemas` and rewrites
42
+ * its refs accordingly. Defs declined by `shouldHoistDef` stay local unless a
43
+ * hoisted def references them.
44
+ */
45
+ hoistDefs(schema, direction) {
46
+ if (typeof schema !== "object" || !schema.$defs) {
47
+ return schema;
48
+ }
49
+ const { $defs, ...rest } = schema;
50
+ const localDefs = {};
51
+ const hoistedDefs = {};
52
+ for (const defName of Object.keys($defs)) {
53
+ const defSchema = $defs[defName];
54
+ if (defSchema === void 0) {
323
55
  continue;
324
56
  }
325
- const status = errorStatusMap[code] ?? DEFAULT_ERROR_STATUS;
326
- const defaultMessage = config.message;
327
- const [dataJsonSchema, dataOptional] = await this.convertSchema(config.data, "output");
328
- const definitions = errorDefinitionsByStatus.get(status);
329
- if (definitions) {
330
- definitions.push({ code, dataJsonSchema, dataOptional, defaultMessage });
57
+ const normalized = ensureJsonSchemaObject(defSchema);
58
+ if (value(this.shouldHoistDef, defName, normalized) !== false) {
59
+ hoistedDefs[defName] = normalized;
331
60
  } else {
332
- errorDefinitionsByStatus.set(status, [{ code, dataJsonSchema, dataOptional, defaultMessage }]);
333
- }
334
- }
335
- if (errorDefinitionsByStatus.size) {
336
- const undefinedErrorSchema = hoistDefs({
337
- $defs: {
338
- UndefinedError: {
339
- type: "object",
340
- properties: {
341
- defined: { const: false },
342
- inferable: { type: "boolean" },
343
- code: { type: "string" },
344
- status: { type: "number" },
345
- message: { type: "string" },
346
- data: {}
347
- },
348
- required: ["defined", "inferable", "code", "status", "message"]
349
- }
350
- },
351
- $ref: "#/$defs/UndefinedError"
352
- }, doc, options);
353
- for (const [status, definitions] of errorDefinitionsByStatus.entries()) {
354
- const descriptions = definitions.map(({ defaultMessage }) => defaultMessage).filter((m) => m !== void 0);
355
- const customBodySchema = value(
356
- options.customErrorResponseBodySchema,
357
- definitions.map((def2) => ({ ...def2, dataJsonSchema: hoistDefs(def2.dataJsonSchema, doc, options) })),
358
- status
359
- );
360
- const responseSchema = customBodySchema ?? combineJsonSchemasWithComposition("oneOf", [
361
- ...definitions.map(({ code, dataJsonSchema, dataOptional, defaultMessage }) => {
362
- return combineJsonObjectSchemaEntries([
363
- ["defined", { const: true }, false],
364
- ["inferable", { type: "boolean" }, false],
365
- ["code", { const: code }, false],
366
- ["status", { const: status }, false],
367
- ["message", { type: "string", default: defaultMessage }, false],
368
- ["data", dataJsonSchema, dataOptional]
369
- ]);
370
- }),
371
- undefinedErrorSchema
372
- ]);
373
- ref.responses ??= {};
374
- ref.responses[status] = {
375
- description: descriptions.length ? descriptions.join(", ") : status.toString(),
376
- content: {
377
- "application/json": {
378
- schema: toOpenAPISchema(responseSchema, doc, options)
379
- }
380
- }
381
- };
61
+ localDefs[defName] = normalized;
382
62
  }
383
63
  }
384
- }
385
- }
386
- function toOpenAPISchema(schema, doc, options) {
387
- return ensureJsonSchemaObject(hoistDefs(
388
- schema,
389
- doc,
390
- options
391
- ));
392
- }
393
- function toOpenAPIPath(path, dynamicPathParams) {
394
- if (!dynamicPathParams?.length) {
395
- return path;
396
- }
397
- let normalized = "";
398
- let currentIndex = 0;
399
- for (const param of dynamicPathParams) {
400
- normalized += path.slice(currentIndex, param.startIndex);
401
- normalized += `{${param.parameterName}}`;
402
- currentIndex = param.startIndex + param.segment.length;
403
- }
404
- normalized += path.slice(currentIndex);
405
- return normalized;
406
- }
407
- function strip$schemaField(schema) {
408
- if (typeof schema !== "object") {
409
- return schema;
410
- }
411
- const { $schema, ...rest } = schema;
412
- return rest;
413
- }
414
- function getEventIteratorDetails(schemas) {
415
- if (!schemas || schemas.length === 0) {
416
- return void 0;
417
- }
418
- const yieldSchemas = [];
419
- const returnSchemas = [];
420
- for (const s of schemas) {
421
- const details = getEventIteratorSchemaDetails(s);
422
- if (!details) {
423
- return void 0;
424
- }
425
- yieldSchemas.push(details.yieldSchema);
426
- if (details.returnSchema) {
427
- returnSchemas.push(details.returnSchema);
428
- }
429
- }
430
- return yieldSchemas.length || returnSchemas.length ? [yieldSchemas, returnSchemas] : void 0;
431
- }
432
- function toEventIteratorContent([yieldSchema, yieldOptional], [returnSchema, returnOptional], doc, options) {
433
- const schema = combineJsonSchemasWithComposition("oneOf", [
434
- combineJsonObjectSchemaEntries([
435
- ["event", { const: "message" }, false],
436
- ["data", yieldSchema, yieldOptional],
437
- ["id", { type: "string" }, true],
438
- ["retry", { type: "number" }, true]
439
- ]),
440
- combineJsonObjectSchemaEntries([
441
- ["event", { const: "close" }, false],
442
- ["data", returnSchema, returnOptional],
443
- ["id", { type: "string" }, true],
444
- ["retry", { type: "number" }, true]
445
- ]),
446
- {
447
- type: "object",
448
- properties: {
449
- event: { const: "error" },
450
- data: {},
451
- id: { type: "string" },
452
- retry: { type: "number" }
453
- },
454
- required: ["event"]
455
- }
456
- ]);
457
- return {
458
- "text/event-stream": {
459
- schema: toOpenAPISchema(schema, doc, options)
460
- }
461
- };
462
- }
463
- function toBodyContent(schema, doc, options) {
464
- const fileSchemasByMediaType = /* @__PURE__ */ new Map();
465
- const rest = flattenJsonUnionSchema(schema).filter((s) => {
466
- if (!isJsonFileSchema(s)) {
467
- return !isUnconstrainedSchema(s);
468
- }
469
- const contentMediaType = s.contentMediaType ?? "*/*";
470
- const schemas = fileSchemasByMediaType.get(contentMediaType);
471
- if (schemas) {
472
- schemas.push(s);
473
- } else {
474
- fileSchemasByMediaType.set(contentMediaType, [s]);
475
- }
476
- return false;
477
- });
478
- const content = {};
479
- if (rest.length > 0) {
480
- const restSchema = fileSchemasByMediaType.size ? combineJsonSchemasWithComposition("anyOf", rest) : schema;
481
- const hasNestedFiles = findDeepMatches(
482
- (v) => isPlainObject(v) && isJsonFileSchema(v),
483
- restSchema
484
- ).values.length > 0;
485
- const contentType = hasNestedFiles ? "multipart/form-data" : "application/json";
486
- const fileSchemas = fileSchemasByMediaType.get(contentType);
487
- fileSchemasByMediaType.delete(contentType);
488
- content[contentType] = {
489
- schema: toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", [restSchema, ...toArray(fileSchemas)]), doc, options)
490
- };
491
- }
492
- for (const [contentType, schemas] of fileSchemasByMediaType.entries()) {
493
- content[contentType] = {
494
- schema: toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", schemas), doc, options)
495
- };
496
- }
497
- return content;
498
- }
499
- function hoistDefs(schema, doc, options) {
500
- if (typeof schema !== "object") {
501
- return schema;
502
- }
503
- if (!schema.$defs) {
504
- return schema;
505
- }
506
- const { $defs, ...rest } = schema;
507
- const localDefs = {};
508
- const hoistedDefs = {};
509
- for (const defName of Object.keys($defs)) {
510
- const defSchema = $defs[defName];
511
- if (defSchema === void 0) {
512
- continue;
513
- }
514
- const normalized = normalizeHoistedDefSchema(defSchema);
515
- if (value(options.shouldHoistDef, defName, normalized) !== false) {
516
- hoistedDefs[defName] = normalized;
517
- } else {
518
- localDefs[defName] = normalized;
64
+ hoistReferencedLocalDefs(hoistedDefs, localDefs);
65
+ if (Object.keys(hoistedDefs).length === 0) {
66
+ return schema;
519
67
  }
520
- }
521
- hoistReferencedLocalDefs(hoistedDefs, localDefs);
522
- if (Object.keys(hoistedDefs).length === 0) {
523
- return schema;
524
- }
525
- doc.components ??= {};
526
- doc.components.schemas ??= {};
527
- const componentsSchemas = doc.components.schemas;
528
- const identityRenameMap = Object.fromEntries(
529
- Object.keys(hoistedDefs).map((defName) => [defName, defName])
530
- );
531
- const renameMap = {};
532
- const pendingSchemas = [];
533
- for (const defName of Object.keys(hoistedDefs)) {
534
- const cleanSchema = hoistedDefs[defName];
535
- const existingSchema = componentsSchemas[defName];
536
- const candidateSchemas = Object.fromEntries(
537
- Object.keys(hoistedDefs).map((currentDefName) => [
538
- currentDefName,
539
- rewriteComponentSchemaRefs(
540
- withReferencedLocalDefs(hoistedDefs[currentDefName], localDefs),
541
- {
542
- ...identityRenameMap,
543
- ...renameMap
544
- }
545
- )
546
- ])
68
+ this.doc.components ??= {};
69
+ this.doc.components.schemas ??= {};
70
+ const componentsSchemas = this.doc.components.schemas;
71
+ const identityRenameMap = Object.fromEntries(
72
+ Object.keys(hoistedDefs).map((defName) => [defName, defName])
547
73
  );
548
- const prelimSchema = candidateSchemas[defName];
549
- if (existingSchema !== void 0) {
550
- const reusableComponentName = findReusableComponentName(componentsSchemas, defName, prelimSchema, candidateSchemas);
551
- if (reusableComponentName !== void 0) {
552
- renameMap[defName] = reusableComponentName;
553
- continue;
554
- }
555
- const componentName = findUniqueComponentName(componentsSchemas, defName);
74
+ const renameMap = {};
75
+ const pendingSchemas = [];
76
+ for (const defName of Object.keys(hoistedDefs)) {
77
+ const cleanSchema = hoistedDefs[defName];
78
+ const candidateSchemas = Object.fromEntries(
79
+ Object.keys(hoistedDefs).map((currentDefName) => [
80
+ currentDefName,
81
+ rewriteComponentSchemaRefs(
82
+ withReferencedLocalDefs(hoistedDefs[currentDefName], localDefs),
83
+ {
84
+ ...identityRenameMap,
85
+ ...renameMap
86
+ }
87
+ )
88
+ ])
89
+ );
90
+ const prelimSchema = candidateSchemas[defName];
91
+ const [componentName, reuseExisting] = resolveComponentName(
92
+ componentsSchemas,
93
+ new Set(Object.values(renameMap)),
94
+ defName,
95
+ prelimSchema,
96
+ candidateSchemas,
97
+ direction
98
+ );
556
99
  renameMap[defName] = componentName;
557
- pendingSchemas.push({ cleanSchema, componentName });
558
- } else {
559
- const reusableComponentName = findReusableComponentName(componentsSchemas, defName, prelimSchema, candidateSchemas);
560
- if (reusableComponentName !== void 0) {
561
- renameMap[defName] = reusableComponentName;
562
- continue;
100
+ if (!reuseExisting) {
101
+ pendingSchemas.push({ cleanSchema, componentName });
563
102
  }
564
- renameMap[defName] = defName;
565
- pendingSchemas.push({ cleanSchema, componentName: defName });
566
103
  }
104
+ for (const { cleanSchema, componentName } of pendingSchemas) {
105
+ componentsSchemas[componentName] = rewriteComponentSchemaRefs(
106
+ withReferencedLocalDefs(cleanSchema, localDefs),
107
+ renameMap
108
+ );
109
+ }
110
+ return rewriteComponentSchemaRefs(withReferencedLocalDefs(rest, localDefs), renameMap);
567
111
  }
568
- for (const { cleanSchema, componentName } of pendingSchemas) {
569
- componentsSchemas[componentName] = rewriteComponentSchemaRefs(
570
- withReferencedLocalDefs(cleanSchema, localDefs),
571
- renameMap
572
- );
112
+ toOpenAPISchema(schema, direction) {
113
+ return ensureJsonSchemaObject(this.hoistDefs(schema, direction));
573
114
  }
574
- return rewriteComponentSchemaRefs(withReferencedLocalDefs(rest, localDefs), renameMap);
575
115
  }
576
- function normalizeHoistedDefSchema(schema) {
577
- let cleanSchema = typeof schema === "boolean" ? schema ? {} : { not: {} } : { ...schema };
578
- if (cleanSchema.additionalProperties === false) {
579
- const { additionalProperties: _ignored, ...withoutAdditionalProperties } = cleanSchema;
580
- cleanSchema = withoutAdditionalProperties;
581
- }
582
- return cleanSchema;
116
+ function visitLocalDefRefs(schema, onRef) {
117
+ visitJsonSchemaRefs(schema, (ref) => {
118
+ const refName = parseLocalDefRefName(ref);
119
+ if (refName !== void 0) {
120
+ onRef(refName);
121
+ }
122
+ });
583
123
  }
584
124
  function hoistReferencedLocalDefs(hoistedDefs, localDefs) {
585
125
  const queue = Object.values(hoistedDefs);
586
126
  while (queue.length > 0) {
587
127
  const current = queue.shift();
588
- if (current === void 0) {
589
- continue;
590
- }
591
- visitSchemaRefs(current, (refName) => {
128
+ visitLocalDefRefs(current, (refName) => {
592
129
  const referenced = localDefs[refName];
593
130
  if (referenced === void 0) {
594
131
  return;
@@ -620,125 +157,64 @@ function collectReferencedLocalDefNames(schema, localDefs) {
620
157
  return [];
621
158
  }
622
159
  const referenced = /* @__PURE__ */ new Set();
623
- const queued = /* @__PURE__ */ new Set();
624
160
  const queue = [schema];
625
161
  while (queue.length > 0) {
626
162
  const current = queue.shift();
627
- if (current === void 0) {
628
- continue;
629
- }
630
- visitSchemaRefs(current, (refName) => {
163
+ visitLocalDefRefs(current, (refName) => {
631
164
  if (localDefs[refName] === void 0 || referenced.has(refName)) {
632
165
  return;
633
166
  }
634
167
  referenced.add(refName);
635
- if (!queued.has(refName)) {
636
- queued.add(refName);
637
- queue.push(localDefs[refName]);
638
- }
168
+ queue.push(localDefs[refName]);
639
169
  });
640
170
  }
641
171
  return [...referenced];
642
172
  }
643
- function visitSchemaRefs(schema, onRef, seen = /* @__PURE__ */ new Set()) {
644
- if (typeof schema !== "object" || schema === null) {
645
- return;
646
- }
647
- if (seen.has(schema)) {
648
- return;
649
- }
650
- seen.add(schema);
651
- if (typeof schema.$ref === "string") {
652
- const refName = parseLocalDefRefName(schema.$ref);
653
- if (refName !== void 0) {
654
- onRef(refName);
655
- }
656
- }
657
- for (const keyword of ["allOf", "anyOf", "oneOf"]) {
658
- if (Array.isArray(schema[keyword])) {
659
- for (const item of schema[keyword]) {
660
- visitSchemaRefs(item, onRef, seen);
661
- }
662
- }
663
- }
664
- if (schema.properties) {
665
- for (const property of Object.values(schema.properties)) {
666
- visitSchemaRefs(property, onRef, seen);
667
- }
668
- }
669
- if (schema.items !== void 0) {
670
- visitSchemaRefs(schema.items, onRef, seen);
671
- }
672
- if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
673
- visitSchemaRefs(schema.additionalProperties, onRef, seen);
674
- }
675
- if (schema.not !== void 0) {
676
- visitSchemaRefs(schema.not, onRef, seen);
677
- }
678
- if (schema.if !== void 0) {
679
- visitSchemaRefs(schema.if, onRef, seen);
680
- }
681
- if (schema.then !== void 0) {
682
- visitSchemaRefs(schema.then, onRef, seen);
683
- }
684
- if (schema.else !== void 0) {
685
- visitSchemaRefs(schema.else, onRef, seen);
686
- }
687
- if (Array.isArray(schema.prefixItems)) {
688
- for (const item of schema.prefixItems) {
689
- visitSchemaRefs(item, onRef, seen);
690
- }
691
- }
692
- if (schema.$defs) {
693
- for (const def of Object.values(schema.$defs)) {
694
- if (def !== void 0) {
695
- visitSchemaRefs(def, onRef, seen);
173
+ function resolveComponentName(componentsSchemas, claimedNames, defName, schema, candidateSchemas, direction) {
174
+ let mintName;
175
+ for (let i = 1; ; i++) {
176
+ const [componentName, mintable, tail] = componentNameCandidate(defName, direction, i);
177
+ const existingSchema = componentsSchemas[componentName];
178
+ if (existingSchema === void 0) {
179
+ if (mintable && !claimedNames.has(componentName)) {
180
+ mintName ??= componentName;
181
+ if (tail) {
182
+ return [mintName, false];
183
+ }
696
184
  }
697
- }
698
- }
699
- }
700
- function findUniqueComponentName(componentsSchemas, baseName) {
701
- const candidate = `${baseName}`;
702
- if (componentsSchemas[candidate] === void 0)
703
- return candidate;
704
- let i = 2;
705
- while (componentsSchemas[`${baseName}${i}`] !== void 0) {
706
- i++;
707
- }
708
- return `${baseName}${i}`;
709
- }
710
- function findReusableComponentName(componentsSchemas, defName, schema, candidateSchemas) {
711
- const exactMatch = componentsSchemas[defName];
712
- if (exactMatch !== void 0 && areSchemasEquivalentForReuse(
713
- schema,
714
- exactMatch,
715
- schema,
716
- exactMatch,
717
- candidateSchemas,
718
- componentsSchemas,
719
- /* @__PURE__ */ new Map([[defName, defName]]),
720
- /* @__PURE__ */ new Map([[defName, defName]])
721
- )) {
722
- return defName;
723
- }
724
- for (const [componentName, componentSchema] of Object.entries(componentsSchemas)) {
725
- if (componentName === defName) {
726
185
  continue;
727
186
  }
728
187
  if (areSchemasEquivalentForReuse(
729
188
  schema,
730
- componentSchema,
189
+ existingSchema,
731
190
  schema,
732
- componentSchema,
191
+ existingSchema,
733
192
  candidateSchemas,
734
193
  componentsSchemas,
735
194
  /* @__PURE__ */ new Map([[defName, componentName]]),
736
195
  /* @__PURE__ */ new Map([[componentName, defName]])
737
196
  )) {
738
- return componentName;
197
+ return [componentName, true];
739
198
  }
740
199
  }
741
- return void 0;
200
+ }
201
+ function componentNameCandidate(defName, direction, attempt) {
202
+ if (attempt === 1) {
203
+ return [defName, true, false];
204
+ }
205
+ if (direction !== void 0) {
206
+ if (attempt === 2) {
207
+ return [`${defName}${direction === "input" ? "Input" : "Output"}`, true, false];
208
+ }
209
+ if (attempt === 3) {
210
+ return [`${defName}${direction === "input" ? "Output" : "Input"}`, false, false];
211
+ }
212
+ return [`${defName}${attempt - 2}`, true, true];
213
+ }
214
+ return [`${defName}${attempt}`, true, true];
215
+ }
216
+ function definedKeysOf(object) {
217
+ return Object.keys(object).filter((key) => object[key] !== void 0).sort();
742
218
  }
743
219
  function areSchemasEquivalentForReuse(candidate, existing, candidateRootSchema, existingRootSchema, candidateSchemas, existingSchemas, candidateToExistingComponentNames, existingToCandidateComponentNames, visited = /* @__PURE__ */ new WeakMap()) {
744
220
  if (candidate === existing) {
@@ -780,8 +256,8 @@ function areSchemasEquivalentForReuse(candidate, existing, candidateRootSchema,
780
256
  }
781
257
  const candidateObject = candidate;
782
258
  const existingObject = existing;
783
- const candidateKeys = Object.keys(candidateObject).sort();
784
- const existingKeys = Object.keys(existingObject).sort();
259
+ const candidateKeys = definedKeysOf(candidateObject);
260
+ const existingKeys = definedKeysOf(existingObject);
785
261
  if (!isDeepEqual(candidateKeys, existingKeys)) {
786
262
  return false;
787
263
  }
@@ -898,4 +374,549 @@ function rewriteComponentSchemaRefs(schema, renameMap) {
898
374
  });
899
375
  }
900
376
 
901
- export { DEFAULT_OPENAPI_INPUT_STRUCTURE, DEFAULT_OPENAPI_METHOD, DEFAULT_OPENAPI_OUTPUT_STRUCTURE, DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, OpenAPIGenerator, OpenAPIGeneratorError, OpenAPISerializer, createContractJsonifiedCaller, getDynamicPathParams, getOpenAPIMeta };
377
+ class OpenAPIGeneratorError extends TypeError {
378
+ }
379
+ function toOpenAPIPath(path, dynamicPathParams) {
380
+ if (!dynamicPathParams?.length) {
381
+ return path;
382
+ }
383
+ let normalized = "";
384
+ let currentIndex = 0;
385
+ for (const param of dynamicPathParams) {
386
+ normalized += path.slice(currentIndex, param.startIndex);
387
+ normalized += `{${param.parameterName}}`;
388
+ currentIndex = param.startIndex + param.segment.length;
389
+ }
390
+ normalized += path.slice(currentIndex);
391
+ return normalized;
392
+ }
393
+ function buildRequest(ctx, operation, def, meta, dynamicPathParams) {
394
+ const method = meta?.method ?? DEFAULT_OPENAPI_METHOD;
395
+ const inputStructure = meta?.inputStructure ?? DEFAULT_OPENAPI_INPUT_STRUCTURE;
396
+ if (inputStructure === "compact") {
397
+ const iteratorDetails = getAsyncIteratorObjectDetails(def.inputSchemas);
398
+ if (iteratorDetails) {
399
+ operation.requestBody = {
400
+ required: true,
401
+ content: toAsyncIteratorObjectContent(
402
+ ctx,
403
+ "input",
404
+ ctx.convertSchemas(iteratorDetails[0], "input"),
405
+ ctx.convertSchemas(iteratorDetails[1], "input")
406
+ )
407
+ };
408
+ return;
409
+ }
410
+ }
411
+ const dynamicParams = dynamicPathParams?.map((v) => v.parameterName);
412
+ const [schema, optional] = ctx.convertSchemas(def.inputSchemas, "input");
413
+ if (isUnconstrainedSchema(schema) && !dynamicParams?.length) {
414
+ return;
415
+ }
416
+ const parts = inputStructure === "compact" ? extractCompactRequestParts(schema, optional, method, dynamicParams) : extractDetailedRequestParts(schema);
417
+ renderPathParameters(ctx, operation, dynamicParams, parts.paramsEntries, meta?.paramsStyles);
418
+ renderQueryParameters(ctx, operation, parts.queryEntries, meta?.queryStyles);
419
+ renderHeaderParameters(ctx, operation, parts.headersEntries);
420
+ if (parts.bodySchema !== void 0) {
421
+ operation.requestBody = {
422
+ required: parts.bodyOptional ? void 0 : true,
423
+ content: toBodyContent(ctx, "input", parts.bodySchema)
424
+ };
425
+ }
426
+ }
427
+ function extractCompactRequestParts(schema, optional, method, dynamicParams) {
428
+ const entries = extractJsonObjectSchemaEntries(schema);
429
+ if (!entries && isBodylessMethod(method)) {
430
+ throw new OpenAPIGeneratorError(
431
+ `method is ${method} but the input schema is not an object.
432
+ ${method} sends every input field as a query parameter, so the input must be an object schema.
433
+ Fix: make the input an object, or use a method with a request body (POST, PUT, PATCH, DELETE).`
434
+ );
435
+ }
436
+ const paramsEntries = entries?.filter(([name]) => dynamicParams?.includes(name));
437
+ const restEntries = entries?.filter(([name]) => !dynamicParams?.includes(name));
438
+ const hasBody = !isBodylessMethod(method);
439
+ const bodySchema = !hasBody ? void 0 : !dynamicParams?.length ? schema : restEntries ? combineJsonObjectSchemaEntries(restEntries) : void 0;
440
+ return {
441
+ paramsEntries,
442
+ queryEntries: isBodylessMethod(method) ? restEntries : void 0,
443
+ headersEntries: void 0,
444
+ bodySchema,
445
+ bodyOptional: !dynamicParams?.length ? optional : restEntries?.every(([, , optional2]) => optional2)
446
+ };
447
+ }
448
+ function extractDetailedRequestParts(schema) {
449
+ const entries = extractJsonObjectSchemaEntries(schema);
450
+ if (!entries) {
451
+ throw new OpenAPIGeneratorError(
452
+ `inputStructure is "detailed" but the input schema is not an object.
453
+ Expected an object shaped like: { params?: object, query?: object, headers?: object, body?: unknown }`
454
+ );
455
+ }
456
+ const section = (name) => entries.find(([entryName]) => entryName === name);
457
+ return {
458
+ paramsEntries: extractJsonObjectSchemaEntries(section("params")?.[1] ?? false),
459
+ queryEntries: extractJsonObjectSchemaEntries(section("query")?.[1] ?? false),
460
+ headersEntries: extractJsonObjectSchemaEntries(section("headers")?.[1] ?? false),
461
+ bodySchema: section("body")?.[1],
462
+ bodyOptional: section("body")?.[2]
463
+ };
464
+ }
465
+ function renderPathParameters(ctx, operation, dynamicParams, paramsEntries, paramsStyles) {
466
+ if (!dynamicParams?.length) {
467
+ return;
468
+ }
469
+ if (!paramsEntries) {
470
+ throw new OpenAPIGeneratorError(
471
+ `the route declares path params (${dynamicParams.map((p) => `{${p}}`).join(", ")}) but there is no object schema to source them from.
472
+ compact mode: the input schema must be an object containing each param.
473
+ detailed mode: the input's "params" section must be an object containing each param.`
474
+ );
475
+ }
476
+ for (const name of dynamicParams) {
477
+ const entry = paramsEntries.find(([entryName]) => entryName === name);
478
+ if (!entry) {
479
+ throw new OpenAPIGeneratorError(
480
+ `dynamic param "{${name}}" is missing from the input schema.
481
+ Route params: ${dynamicParams.map((p) => `{${p}}`).join(", ")}
482
+ Schema keys: ${paramsEntries.map(([n]) => n).join(", ") || "(none)"}`
483
+ );
484
+ }
485
+ if (entry[2]) {
486
+ throw new OpenAPIGeneratorError(
487
+ `dynamic param "${name}" is optional in the input schema.
488
+ OpenAPI requires path params to be required. Make "${name}" required.`
489
+ );
490
+ }
491
+ const style = paramsStyles?.[name];
492
+ const parameter = {
493
+ in: "path",
494
+ required: true,
495
+ name,
496
+ schema: ctx.registry.toOpenAPISchema(entry[1], "input")
497
+ };
498
+ if (style === "comma-delimited-array" || style === "comma-delimited-object") {
499
+ parameter.style = "simple";
500
+ parameter.explode = false;
501
+ }
502
+ operation.parameters ??= [];
503
+ operation.parameters.push(parameter);
504
+ }
505
+ }
506
+ function renderQueryParameters(ctx, operation, queryEntries, queryStyles) {
507
+ for (const [name, schema, optional] of queryEntries ?? []) {
508
+ const style = queryStyles?.[name];
509
+ const parameter = {
510
+ in: "query",
511
+ name,
512
+ schema: ctx.registry.toOpenAPISchema(schema, "input"),
513
+ allowEmptyValue: true,
514
+ allowReserved: true
515
+ };
516
+ if (!optional) {
517
+ parameter.required = true;
518
+ }
519
+ if (style === "comma-delimited-array" || style === "comma-delimited-object") {
520
+ parameter.explode = false;
521
+ } else if (style === "pipe-delimited-array" || style === "pipe-delimited-object") {
522
+ parameter.style = "pipeDelimited";
523
+ } else if (style === "space-delimited-array" || style === "space-delimited-object") {
524
+ parameter.style = "spaceDelimited";
525
+ } else if (style === "json") {
526
+ parameter.content = {
527
+ "application/json": { schema: parameter.schema }
528
+ };
529
+ delete parameter.schema;
530
+ } else if (style === void 0) {
531
+ if (flattenJsonUnionSchema(schema).some((s) => !isJsonPrimitiveSchema(s))) {
532
+ const arrayable = matchArrayableJsonSchema(schema);
533
+ if (!arrayable || flattenJsonUnionSchema(arrayable[0]).some((s) => !isJsonPrimitiveSchema(s))) {
534
+ parameter.style = "deepObject";
535
+ parameter.explode = true;
536
+ }
537
+ }
538
+ } else ;
539
+ operation.parameters ??= [];
540
+ operation.parameters.push(parameter);
541
+ }
542
+ }
543
+ function renderHeaderParameters(ctx, operation, headersEntries) {
544
+ for (const [name, schema, optional] of headersEntries ?? []) {
545
+ operation.parameters ??= [];
546
+ operation.parameters.push({
547
+ in: "header",
548
+ name,
549
+ required: optional ? void 0 : true,
550
+ schema: ctx.registry.toOpenAPISchema(schema, "input")
551
+ });
552
+ }
553
+ }
554
+ function buildSuccessResponse(ctx, operation, def, meta) {
555
+ const status = meta?.successStatus ?? DEFAULT_SUCCESS_STATUS;
556
+ const description = meta?.successDescription ?? DEFAULT_OPENAPI_SUCCESS_DESCRIPTION;
557
+ const outputStructure = meta?.outputStructure ?? DEFAULT_OPENAPI_OUTPUT_STRUCTURE;
558
+ if (outputStructure === "compact") {
559
+ const iteratorDetails = getAsyncIteratorObjectDetails(def.outputSchemas);
560
+ if (iteratorDetails) {
561
+ operation.responses ??= {};
562
+ operation.responses[status] = {
563
+ description,
564
+ content: toAsyncIteratorObjectContent(
565
+ ctx,
566
+ "output",
567
+ ctx.convertSchemas(iteratorDetails[0], "output"),
568
+ ctx.convertSchemas(iteratorDetails[1], "output")
569
+ )
570
+ };
571
+ return;
572
+ }
573
+ }
574
+ const [schema] = ctx.convertSchemas(def.outputSchemas, "output");
575
+ if (isUnconstrainedSchema(schema) || outputStructure === "compact") {
576
+ operation.responses ??= {};
577
+ operation.responses[status] = {
578
+ description,
579
+ content: toBodyContent(ctx, "output", schema)
580
+ };
581
+ return;
582
+ }
583
+ for (const [responseStatus, parts] of extractDetailedResponseParts(schema, status)) {
584
+ const responseObject = {
585
+ description: parts.descriptions.length ? parts.descriptions.join(", ") : description
586
+ };
587
+ if (parts.bodies.length) {
588
+ responseObject.content = toBodyContent(ctx, "output", combineJsonSchemasWithComposition("anyOf", parts.bodies));
589
+ }
590
+ if (parts.headers.length) {
591
+ const entries = extractJsonObjectSchemaEntries(combineJsonSchemasWithComposition("anyOf", parts.headers));
592
+ entries?.forEach(([name, schema2, optional]) => {
593
+ responseObject.headers ??= {};
594
+ responseObject.headers[name] = {
595
+ required: optional ? void 0 : true,
596
+ schema: ctx.registry.toOpenAPISchema(schema2, "output")
597
+ };
598
+ });
599
+ }
600
+ operation.responses ??= {};
601
+ operation.responses[responseStatus] = responseObject;
602
+ }
603
+ }
604
+ function extractDetailedResponseParts(schema, defaultStatus) {
605
+ const partsByStatus = /* @__PURE__ */ new Map();
606
+ for (const item of flattenJsonUnionSchema(schema)) {
607
+ const entries = extractJsonObjectSchemaEntries(item);
608
+ if (!entries) {
609
+ throw new OpenAPIGeneratorError(
610
+ `outputStructure is "detailed" but the output schema (or one of its union members) is not an object.
611
+ Expected each member shaped like: { status?: number (< 400), headers?: object, body?: unknown }`
612
+ );
613
+ }
614
+ const statusSchema = entries.find(([name]) => name === "status")?.[1];
615
+ if (statusSchema !== void 0 && (typeof statusSchema !== "object" || !Number.isInteger(statusSchema.const) || statusSchema.const >= 400)) {
616
+ throw new OpenAPIGeneratorError(
617
+ `invalid "status" field in the detailed output schema.
618
+ Expected: a literal (const) integer below 400
619
+ Received: ${stringifyJSON(statusSchema)}`
620
+ );
621
+ }
622
+ const status = (statusSchema?.const || void 0) ?? defaultStatus;
623
+ let parts = partsByStatus.get(status);
624
+ if (!parts) {
625
+ parts = { descriptions: [], bodies: [], headers: [] };
626
+ partsByStatus.set(status, parts);
627
+ }
628
+ if (statusSchema?.description !== void 0) {
629
+ parts.descriptions.push(statusSchema.description);
630
+ }
631
+ const headersSchema = entries.find(([name]) => name === "headers")?.[1];
632
+ if (headersSchema !== void 0) {
633
+ parts.headers.push(headersSchema);
634
+ }
635
+ const bodySchema = entries.find(([name]) => name === "body")?.[1];
636
+ if (bodySchema !== void 0) {
637
+ parts.bodies.push(bodySchema);
638
+ }
639
+ }
640
+ return partsByStatus;
641
+ }
642
+ function buildErrorResponse(ctx, operation, def) {
643
+ const definitionsByStatus = /* @__PURE__ */ new Map();
644
+ for (const code in def.errorMap) {
645
+ const config = def.errorMap[code];
646
+ if (!config) {
647
+ continue;
648
+ }
649
+ const status = ctx.errorStatusMap[code] ?? DEFAULT_ERROR_STATUS;
650
+ const [dataJsonSchema, dataOptional] = ctx.convertSchemas(config.data !== void 0 ? [config.data] : void 0, "output");
651
+ let definitions = definitionsByStatus.get(status);
652
+ if (!definitions) {
653
+ definitions = [];
654
+ definitionsByStatus.set(status, definitions);
655
+ }
656
+ definitions.push({ code, dataJsonSchema, dataOptional, defaultMessage: config.message });
657
+ }
658
+ if (!definitionsByStatus.size) {
659
+ return;
660
+ }
661
+ const undefinedErrorSchema = ctx.registry.register("UndefinedError", {
662
+ type: "object",
663
+ properties: {
664
+ defined: { const: false },
665
+ inferable: { type: "boolean" },
666
+ code: { type: "string" },
667
+ status: { type: "number" },
668
+ message: { type: "string" },
669
+ data: {}
670
+ },
671
+ required: ["defined", "inferable", "code", "status", "message"]
672
+ });
673
+ for (const [status, definitions] of definitionsByStatus.entries()) {
674
+ const descriptions = definitions.map(({ defaultMessage }) => defaultMessage).filter((m) => m !== void 0);
675
+ const customBodySchema = value(
676
+ ctx.customErrorResponseBodySchema,
677
+ definitions.map((def2) => ({ ...def2, dataJsonSchema: ctx.registry.hoistDefs(def2.dataJsonSchema, "output") })),
678
+ status
679
+ );
680
+ const responseSchema = customBodySchema ?? combineJsonSchemasWithComposition("oneOf", [
681
+ ...definitions.map(({ code, dataJsonSchema, dataOptional, defaultMessage }) => {
682
+ return ctx.registry.register(toErrorComponentName(code), combineJsonObjectSchemaEntries([
683
+ ["defined", { const: true }, false],
684
+ ["inferable", { type: "boolean" }, false],
685
+ ["code", { const: code }, false],
686
+ ["status", { const: status }, false],
687
+ ["message", { type: "string", default: defaultMessage }, false],
688
+ ["data", ctx.registry.hoistDefs(dataJsonSchema, "output"), dataOptional]
689
+ ]));
690
+ }),
691
+ undefinedErrorSchema
692
+ ]);
693
+ operation.responses ??= {};
694
+ operation.responses[status] = {
695
+ description: descriptions.length ? descriptions.join(", ") : status.toString(),
696
+ content: {
697
+ "application/json": {
698
+ schema: ctx.registry.toOpenAPISchema(responseSchema, "output")
699
+ }
700
+ }
701
+ };
702
+ }
703
+ }
704
+ function toErrorComponentName(code) {
705
+ const name = code.split(/[^a-z0-9]+/i).filter(Boolean).map((part) => {
706
+ const rest = part.slice(1);
707
+ return part.charAt(0).toUpperCase() + (rest === rest.toUpperCase() ? rest.toLowerCase() : rest);
708
+ }).join("");
709
+ return name || "Error";
710
+ }
711
+ function getAsyncIteratorObjectDetails(schemas) {
712
+ if (!schemas || schemas.length === 0) {
713
+ return void 0;
714
+ }
715
+ const yieldSchemas = [];
716
+ const returnSchemas = [];
717
+ for (const s of schemas) {
718
+ const details = getAsyncIteratorObjectSchemaDetails(s);
719
+ if (!details) {
720
+ return void 0;
721
+ }
722
+ yieldSchemas.push(details.yieldSchema);
723
+ if (details.returnSchema) {
724
+ returnSchemas.push(details.returnSchema);
725
+ }
726
+ }
727
+ return [yieldSchemas, returnSchemas];
728
+ }
729
+ function toAsyncIteratorObjectContent(ctx, direction, [yieldSchema, yieldOptional], [returnSchema, returnOptional]) {
730
+ const schema = combineJsonSchemasWithComposition("oneOf", [
731
+ combineJsonObjectSchemaEntries([
732
+ ["event", { const: "message" }, false],
733
+ ["data", yieldSchema, yieldOptional],
734
+ ["id", { type: "string" }, true],
735
+ ["retry", { type: "number" }, true]
736
+ ]),
737
+ combineJsonObjectSchemaEntries([
738
+ ["event", { const: "close" }, false],
739
+ ["data", returnSchema, returnOptional],
740
+ ["id", { type: "string" }, true],
741
+ ["retry", { type: "number" }, true]
742
+ ]),
743
+ {
744
+ type: "object",
745
+ properties: {
746
+ event: { const: "error" },
747
+ data: {},
748
+ id: { type: "string" },
749
+ retry: { type: "number" }
750
+ },
751
+ required: ["event"]
752
+ }
753
+ ]);
754
+ return {
755
+ "text/event-stream": {
756
+ schema: ctx.registry.toOpenAPISchema(schema, direction)
757
+ }
758
+ };
759
+ }
760
+ function toBodyContent(ctx, direction, schema) {
761
+ const fileSchemasByMediaType = /* @__PURE__ */ new Map();
762
+ const rest = flattenJsonUnionSchema(schema).filter((s) => {
763
+ if (!isJsonFileSchema(s)) {
764
+ return !isUnconstrainedSchema(s);
765
+ }
766
+ const contentMediaType = s.contentMediaType ?? "*/*";
767
+ const schemas = fileSchemasByMediaType.get(contentMediaType);
768
+ if (schemas) {
769
+ schemas.push(s);
770
+ } else {
771
+ fileSchemasByMediaType.set(contentMediaType, [s]);
772
+ }
773
+ return false;
774
+ });
775
+ const content = {};
776
+ if (rest.length > 0) {
777
+ const restSchema = fileSchemasByMediaType.size ? combineJsonSchemasWithComposition("anyOf", rest) : schema;
778
+ const hasNestedFiles = findDeepMatches(
779
+ (v) => isPlainObject(v) && isJsonFileSchema(v),
780
+ restSchema
781
+ ).values.length > 0;
782
+ const contentType = hasNestedFiles ? "multipart/form-data" : "application/json";
783
+ const fileSchemas = fileSchemasByMediaType.get(contentType);
784
+ fileSchemasByMediaType.delete(contentType);
785
+ content[contentType] = {
786
+ schema: ctx.registry.toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", [restSchema, ...fileSchemas ?? []]), direction)
787
+ };
788
+ }
789
+ for (const [contentType, schemas] of fileSchemasByMediaType.entries()) {
790
+ content[contentType] = {
791
+ schema: ctx.registry.toOpenAPISchema(combineJsonSchemasWithComposition("anyOf", schemas), direction)
792
+ };
793
+ }
794
+ return content;
795
+ }
796
+
797
+ class OpenAPIGenerator {
798
+ serializer;
799
+ converter;
800
+ constructor(options = {}) {
801
+ this.serializer = options.serializer ?? new OpenAPISerializer();
802
+ this.converter = new DelegatingJsonSchemaConverter([
803
+ ...toArray(options.converters),
804
+ new StandardJsonSchemaConverter()
805
+ ]);
806
+ }
807
+ async generate(router, options = {}) {
808
+ const doc = {
809
+ ...clone(options.base),
810
+ openapi: options.base?.openapi ?? "3.1.2",
811
+ info: options.base?.info ?? { title: "API Reference", version: "0.0.0" }
812
+ };
813
+ const ctx = {
814
+ registry: new OpenAPIComponentRegistry(doc, options.shouldHoistDef),
815
+ convertSchemas: (schemas, direction) => this.convertSchemas(schemas, direction),
816
+ errorStatusMap: options.errorStatusMap ?? COMMON_ERROR_STATUS_MAP,
817
+ customErrorResponseBodySchema: options.customErrorResponseBodySchema
818
+ };
819
+ const errors = [];
820
+ await walkProcedureContractsAsync(router, (contract, path) => {
821
+ if (value(options.filter, contract, path) === false) {
822
+ return;
823
+ }
824
+ try {
825
+ const def = contract["~orpc"];
826
+ const meta = getOpenAPIMeta(contract);
827
+ const method = (meta?.method ?? DEFAULT_OPENAPI_METHOD).toLowerCase();
828
+ const postPath = meta?.path ?? pathToHttpPath(path);
829
+ const httpPath = meta?.prefix ? mergeHttpPath(meta.prefix, postPath) : postPath;
830
+ const dynamicPathParams = getDynamicPathParams(httpPath);
831
+ const openApiPath = toOpenAPIPath(httpPath, dynamicPathParams);
832
+ let operation;
833
+ if (meta?.spec !== void 0 && typeof meta.spec !== "function") {
834
+ operation = meta.spec;
835
+ } else {
836
+ operation = {
837
+ operationId: meta?.operationId ?? path.join("."),
838
+ summary: meta?.summary,
839
+ description: meta?.description,
840
+ deprecated: meta?.deprecated,
841
+ tags: meta?.tags?.map((tag) => tag)
842
+ };
843
+ buildRequest(ctx, operation, def, meta, dynamicPathParams);
844
+ buildSuccessResponse(ctx, operation, def, meta);
845
+ buildErrorResponse(ctx, operation, def);
846
+ }
847
+ if (typeof meta?.spec === "function") {
848
+ operation = meta.spec(operation);
849
+ }
850
+ doc.paths ??= {};
851
+ doc.paths[openApiPath] ??= {};
852
+ doc.paths[openApiPath][method] = operation;
853
+ } catch (e) {
854
+ if (!(e instanceof OpenAPIGeneratorError)) {
855
+ throw e;
856
+ }
857
+ errors.push(`Procedure at ${path.join(".") || "(root)"}: ${e.message}`);
858
+ }
859
+ });
860
+ if (errors.length) {
861
+ throw new OpenAPIGeneratorError(
862
+ `[OpenAPIGenerator] Failed to generate the OpenAPI document (${errors.length} error${errors.length === 1 ? "" : "s"}):
863
+
864
+ ${errors.join("\n\n")}`
865
+ );
866
+ }
867
+ return this.serializer.serialize(doc, { asFormData: false, useFormDataForBlobFields: false });
868
+ }
869
+ convertSchema(schema, direction) {
870
+ const [jsonSchema, optional] = this.converter.convert(schema, direction);
871
+ return [strip$schemaField(jsonSchema), optional];
872
+ }
873
+ convertSchemas(schemas, direction) {
874
+ if (!schemas || schemas.length <= 1) {
875
+ return this.convertSchema(schemas?.[0], direction);
876
+ }
877
+ const results = schemas.map((s) => this.convertSchema(s, direction));
878
+ return [
879
+ combineJsonSchemasWithComposition("allOf", results.map(([jsonSchema]) => jsonSchema)),
880
+ results.every(([, optional]) => optional)
881
+ ];
882
+ }
883
+ }
884
+ function strip$schemaField(schema) {
885
+ if (typeof schema !== "object") {
886
+ return schema;
887
+ }
888
+ const { $schema, ...rest } = schema;
889
+ return rest;
890
+ }
891
+
892
+ function populateRouterContractOpenAPIPaths(router, options = {}) {
893
+ const path = toArray(options.path);
894
+ if (router instanceof ProcedureContract) {
895
+ if (getOpenAPIMeta(router)?.path !== void 0) {
896
+ return router;
897
+ }
898
+ const [meta, metaPlugins] = resolveMetaPlugins(
899
+ router["~orpc"].meta,
900
+ router["~orpc"].metaPlugins,
901
+ [openapi({ path: pathToHttpPath(path) })]
902
+ );
903
+ return new ProcedureContract({
904
+ ...router["~orpc"],
905
+ meta,
906
+ metaPlugins
907
+ });
908
+ }
909
+ if (!isTypescriptObject(router)) {
910
+ return router;
911
+ }
912
+ const populated = {};
913
+ for (const key in router) {
914
+ populated[key] = populateRouterContractOpenAPIPaths(
915
+ router[key],
916
+ { ...options, path: [...path, key] }
917
+ );
918
+ }
919
+ return populated;
920
+ }
921
+
922
+ export { DEFAULT_OPENAPI_INPUT_STRUCTURE, DEFAULT_OPENAPI_METHOD, DEFAULT_OPENAPI_OUTPUT_STRUCTURE, DEFAULT_OPENAPI_SUCCESS_DESCRIPTION, OpenAPIGenerator, OpenAPIGeneratorError, OpenAPISerializer, createContractJsonifiedClientFactory, getDynamicPathParams, getOpenAPIMeta, isBodylessMethod, openapi, populateRouterContractOpenAPIPaths };