@orpc/openapi 0.0.0-next.3cc45a9 → 0.0.0-next.3d25567

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.
@@ -0,0 +1,101 @@
1
+ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
+ import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
+ import { AnyProcedure, AnyRouter } from '@orpc/server';
4
+ import { Promisable } from '@orpc/shared';
5
+ import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
+
7
+ interface SchemaConverterComponent {
8
+ allowedStrategies: readonly SchemaConvertOptions['strategy'][];
9
+ schema: AnySchema;
10
+ required: boolean;
11
+ ref: string;
12
+ }
13
+ interface SchemaConvertOptions {
14
+ strategy: 'input' | 'output';
15
+ /**
16
+ * Common components should use `$ref` to represent themselves if matched.
17
+ */
18
+ components?: readonly SchemaConverterComponent[];
19
+ /**
20
+ * Minimum schema structure depth required before using `$ref` for components.
21
+ *
22
+ * For example, if set to 2, `$ref` will only be used for schemas nested at depth 2 or greater.
23
+ *
24
+ * @default 0 - No depth limit;
25
+ */
26
+ minStructureDepthForRef?: number;
27
+ }
28
+ interface SchemaConverter {
29
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<[required: boolean, jsonSchema: JSONSchema]>;
30
+ }
31
+ interface ConditionalSchemaConverter extends SchemaConverter {
32
+ condition(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<boolean>;
33
+ }
34
+ declare class CompositeSchemaConverter implements SchemaConverter {
35
+ private readonly converters;
36
+ constructor(converters: ConditionalSchemaConverter[]);
37
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
+ }
39
+
40
+ interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
41
+ schemaConverters?: ConditionalSchemaConverter[];
42
+ }
43
+ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document, 'openapi'>> {
44
+ /**
45
+ * Exclude procedures from the OpenAPI specification.
46
+ *
47
+ * @default () => false
48
+ */
49
+ exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
50
+ /**
51
+ * Common schemas to be used for $ref resolution.
52
+ */
53
+ commonSchemas?: Record<string, {
54
+ /**
55
+ * Determines which schema definition to use when input and output schemas differ.
56
+ * This is needed because some schemas transform data differently between input and output,
57
+ * making it impossible to use a single $ref for both cases.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // This schema transforms a string input into a number output
62
+ * const Schema = z.string()
63
+ * .transform(v => Number(v))
64
+ * .pipe(z.number())
65
+ *
66
+ * // Input schema: { type: 'string' }
67
+ * // Output schema: { type: 'number' }
68
+ * ```
69
+ *
70
+ * When schemas differ between input and output, you must explicitly choose
71
+ * which version to use for the OpenAPI specification.
72
+ *
73
+ * @default 'input' - Uses the input schema definition by default
74
+ */
75
+ strategy?: SchemaConvertOptions['strategy'];
76
+ schema: AnySchema;
77
+ } | {
78
+ error: 'UndefinedError';
79
+ schema?: never;
80
+ }>;
81
+ }
82
+ /**
83
+ * The generator that converts oRPC routers/contracts to OpenAPI specifications.
84
+ *
85
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
86
+ */
87
+ declare class OpenAPIGenerator {
88
+ #private;
89
+ private readonly serializer;
90
+ private readonly converter;
91
+ constructor(options?: OpenAPIGeneratorOptions);
92
+ /**
93
+ * Generates OpenAPI specifications from oRPC routers/contracts.
94
+ *
95
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
96
+ */
97
+ generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
98
+ }
99
+
100
+ export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
101
+ export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };
@@ -0,0 +1,101 @@
1
+ import { AnySchema, OpenAPI, AnyContractProcedure, AnyContractRouter } from '@orpc/contract';
2
+ import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
3
+ import { AnyProcedure, AnyRouter } from '@orpc/server';
4
+ import { Promisable } from '@orpc/shared';
5
+ import { JSONSchema } from 'json-schema-typed/draft-2020-12';
6
+
7
+ interface SchemaConverterComponent {
8
+ allowedStrategies: readonly SchemaConvertOptions['strategy'][];
9
+ schema: AnySchema;
10
+ required: boolean;
11
+ ref: string;
12
+ }
13
+ interface SchemaConvertOptions {
14
+ strategy: 'input' | 'output';
15
+ /**
16
+ * Common components should use `$ref` to represent themselves if matched.
17
+ */
18
+ components?: readonly SchemaConverterComponent[];
19
+ /**
20
+ * Minimum schema structure depth required before using `$ref` for components.
21
+ *
22
+ * For example, if set to 2, `$ref` will only be used for schemas nested at depth 2 or greater.
23
+ *
24
+ * @default 0 - No depth limit;
25
+ */
26
+ minStructureDepthForRef?: number;
27
+ }
28
+ interface SchemaConverter {
29
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<[required: boolean, jsonSchema: JSONSchema]>;
30
+ }
31
+ interface ConditionalSchemaConverter extends SchemaConverter {
32
+ condition(schema: AnySchema | undefined, options: SchemaConvertOptions): Promisable<boolean>;
33
+ }
34
+ declare class CompositeSchemaConverter implements SchemaConverter {
35
+ private readonly converters;
36
+ constructor(converters: ConditionalSchemaConverter[]);
37
+ convert(schema: AnySchema | undefined, options: SchemaConvertOptions): Promise<[required: boolean, jsonSchema: JSONSchema]>;
38
+ }
39
+
40
+ interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
41
+ schemaConverters?: ConditionalSchemaConverter[];
42
+ }
43
+ interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Document, 'openapi'>> {
44
+ /**
45
+ * Exclude procedures from the OpenAPI specification.
46
+ *
47
+ * @default () => false
48
+ */
49
+ exclude?: (procedure: AnyProcedure | AnyContractProcedure, path: readonly string[]) => boolean;
50
+ /**
51
+ * Common schemas to be used for $ref resolution.
52
+ */
53
+ commonSchemas?: Record<string, {
54
+ /**
55
+ * Determines which schema definition to use when input and output schemas differ.
56
+ * This is needed because some schemas transform data differently between input and output,
57
+ * making it impossible to use a single $ref for both cases.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // This schema transforms a string input into a number output
62
+ * const Schema = z.string()
63
+ * .transform(v => Number(v))
64
+ * .pipe(z.number())
65
+ *
66
+ * // Input schema: { type: 'string' }
67
+ * // Output schema: { type: 'number' }
68
+ * ```
69
+ *
70
+ * When schemas differ between input and output, you must explicitly choose
71
+ * which version to use for the OpenAPI specification.
72
+ *
73
+ * @default 'input' - Uses the input schema definition by default
74
+ */
75
+ strategy?: SchemaConvertOptions['strategy'];
76
+ schema: AnySchema;
77
+ } | {
78
+ error: 'UndefinedError';
79
+ schema?: never;
80
+ }>;
81
+ }
82
+ /**
83
+ * The generator that converts oRPC routers/contracts to OpenAPI specifications.
84
+ *
85
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
86
+ */
87
+ declare class OpenAPIGenerator {
88
+ #private;
89
+ private readonly serializer;
90
+ private readonly converter;
91
+ constructor(options?: OpenAPIGeneratorOptions);
92
+ /**
93
+ * Generates OpenAPI specifications from oRPC routers/contracts.
94
+ *
95
+ * @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification OpenAPI Specification Docs}
96
+ */
97
+ generate(router: AnyContractRouter | AnyRouter, options?: OpenAPIGeneratorGenerateOptions): Promise<OpenAPI.Document>;
98
+ }
99
+
100
+ export { OpenAPIGenerator as b, CompositeSchemaConverter as e };
101
+ export type { ConditionalSchemaConverter as C, OpenAPIGeneratorOptions as O, SchemaConverterComponent as S, OpenAPIGeneratorGenerateOptions as a, SchemaConvertOptions as c, SchemaConverter as d };
@@ -1,8 +1,8 @@
1
- import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
1
+ import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
2
  import { Context, Router } from '@orpc/server';
3
3
  import { StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
4
 
5
- interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions {
5
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions {
6
6
  }
7
7
  declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
8
8
  constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
@@ -1,8 +1,8 @@
1
- import { StandardOpenAPIJsonSerializerOptions } from '@orpc/openapi-client/standard';
1
+ import { StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions } from '@orpc/openapi-client/standard';
2
2
  import { Context, Router } from '@orpc/server';
3
3
  import { StandardHandlerOptions, StandardHandler } from '@orpc/server/standard';
4
4
 
5
- interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions {
5
+ interface StandardOpenAPIHandlerOptions<T extends Context> extends StandardHandlerOptions<T>, StandardOpenAPIJsonSerializerOptions, StandardBracketNotationSerializerOptions {
6
6
  }
7
7
  declare class StandardOpenAPIHandler<T extends Context> extends StandardHandler<T> {
8
8
  constructor(router: Router<any, T>, options: NoInfer<StandardOpenAPIHandlerOptions<T>>);
@@ -1,10 +1,10 @@
1
- import { fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
1
+ import { isORPCErrorStatus, fallbackORPCErrorStatus, fallbackORPCErrorMessage } from '@orpc/client';
2
2
  import { toHttpPath } from '@orpc/client/standard';
3
3
  import { fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract';
4
4
  import { standardizeHTTPPath, StandardOpenAPIJsonSerializer, getDynamicParams } from '@orpc/openapi-client/standard';
5
5
  import { isProcedure, resolveContractProcedures } from '@orpc/server';
6
- import { isObject, findDeepMatches, toArray, clone } from '@orpc/shared';
7
- import 'json-schema-typed/draft-2020-12';
6
+ import { isObject, stringifyJSON, findDeepMatches, toArray, clone } from '@orpc/shared';
7
+ import { TypeName } from 'json-schema-typed/draft-2020-12';
8
8
 
9
9
  const OPERATION_EXTENDER_SYMBOL = Symbol("ORPC_OPERATION_EXTENDER");
10
10
  function customOpenAPIOperation(o, extend) {
@@ -184,6 +184,57 @@ function applySchemaOptionality(required, schema) {
184
184
  ]
185
185
  };
186
186
  }
187
+ function expandUnionSchema(schema) {
188
+ if (typeof schema === "object") {
189
+ for (const keyword of ["anyOf", "oneOf"]) {
190
+ if (schema[keyword] && Object.keys(schema).every(
191
+ (k) => k === keyword || !LOGIC_KEYWORDS.includes(k)
192
+ )) {
193
+ return schema[keyword].flatMap((s) => expandUnionSchema(s));
194
+ }
195
+ }
196
+ }
197
+ return [schema];
198
+ }
199
+ function expandArrayableSchema(schema) {
200
+ const schemas = expandUnionSchema(schema);
201
+ if (schemas.length !== 2) {
202
+ return void 0;
203
+ }
204
+ const arraySchema = schemas.find(
205
+ (s) => typeof s === "object" && s.type === "array" && Object.keys(s).filter((k) => LOGIC_KEYWORDS.includes(k)).every((k) => k === "type" || k === "items")
206
+ );
207
+ if (arraySchema === void 0) {
208
+ return void 0;
209
+ }
210
+ const items1 = arraySchema.items;
211
+ const items2 = schemas.find((s) => s !== arraySchema);
212
+ if (stringifyJSON(items1) !== stringifyJSON(items2)) {
213
+ return void 0;
214
+ }
215
+ return [items2, arraySchema];
216
+ }
217
+ const PRIMITIVE_SCHEMA_TYPES = /* @__PURE__ */ new Set([
218
+ TypeName.String,
219
+ TypeName.Number,
220
+ TypeName.Integer,
221
+ TypeName.Boolean,
222
+ TypeName.Null
223
+ ]);
224
+ function isPrimitiveSchema(schema) {
225
+ return expandUnionSchema(schema).every((s) => {
226
+ if (typeof s === "boolean") {
227
+ return false;
228
+ }
229
+ if (typeof s.type === "string" && PRIMITIVE_SCHEMA_TYPES.has(s.type)) {
230
+ return true;
231
+ }
232
+ if (s.const !== void 0) {
233
+ return true;
234
+ }
235
+ return false;
236
+ });
237
+ }
187
238
 
188
239
  function toOpenAPIPath(path) {
189
240
  return standardizeHTTPPath(path).replace(/\/\{\+([^}]+)\}/g, "/{$1}");
@@ -256,13 +307,26 @@ function toOpenAPIParameters(schema, parameterIn) {
256
307
  const parameters = [];
257
308
  for (const key in schema.properties) {
258
309
  const keySchema = schema.properties[key];
310
+ let isDeepObjectStyle = true;
311
+ if (parameterIn !== "query") {
312
+ isDeepObjectStyle = false;
313
+ } else if (isPrimitiveSchema(keySchema)) {
314
+ isDeepObjectStyle = false;
315
+ } else {
316
+ const [item] = expandArrayableSchema(keySchema) ?? [];
317
+ if (item !== void 0 && isPrimitiveSchema(item)) {
318
+ isDeepObjectStyle = false;
319
+ }
320
+ }
259
321
  parameters.push({
260
322
  name: key,
261
323
  in: parameterIn,
262
324
  required: schema.required?.includes(key),
263
- style: parameterIn === "query" ? "deepObject" : void 0,
264
- explode: parameterIn === "query" ? true : void 0,
265
- schema: toOpenAPISchema(keySchema)
325
+ schema: toOpenAPISchema(keySchema),
326
+ style: isDeepObjectStyle ? "deepObject" : void 0,
327
+ explode: isDeepObjectStyle ? true : void 0,
328
+ allowEmptyValue: parameterIn === "query" ? true : void 0,
329
+ allowReserved: parameterIn === "query" ? true : void 0
266
330
  });
267
331
  }
268
332
  return parameters;
@@ -281,6 +345,15 @@ function checkParamsSchema(schema, params) {
281
345
  function toOpenAPISchema(schema) {
282
346
  return schema === true ? {} : schema === false ? { not: {} } : schema;
283
347
  }
348
+ const OPENAPI_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/";
349
+ function resolveOpenAPIJsonSchemaRef(doc, schema) {
350
+ if (typeof schema !== "object" || !schema.$ref?.startsWith(OPENAPI_JSON_SCHEMA_REF_PREFIX)) {
351
+ return schema;
352
+ }
353
+ const name = schema.$ref.slice(OPENAPI_JSON_SCHEMA_REF_PREFIX.length);
354
+ const resolved = doc.components?.schemas?.[name];
355
+ return resolved ?? schema;
356
+ }
284
357
 
285
358
  class CompositeSchemaConverter {
286
359
  converters;
@@ -317,8 +390,10 @@ class OpenAPIGenerator {
317
390
  ...clone(options),
318
391
  info: options.info ?? { title: "API Reference", version: "0.0.0" },
319
392
  openapi: "3.1.1",
320
- exclude: void 0
393
+ exclude: void 0,
394
+ commonSchemas: void 0
321
395
  };
396
+ const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas);
322
397
  const contracts = [];
323
398
  await resolveContractProcedures({ path: [], router }, ({ contract, path }) => {
324
399
  if (!exclude(contract, path)) {
@@ -332,16 +407,21 @@ class OpenAPIGenerator {
332
407
  const def = contract["~orpc"];
333
408
  const method = toOpenAPIMethod(fallbackContractConfig("defaultMethod", def.route.method));
334
409
  const httpPath = toOpenAPIPath(def.route.path ?? toHttpPath(path));
335
- const operationObjectRef = {
336
- operationId,
337
- summary: def.route.summary,
338
- description: def.route.description,
339
- deprecated: def.route.deprecated,
340
- tags: def.route.tags?.map((tag) => tag)
341
- };
342
- await this.#request(operationObjectRef, def);
343
- await this.#successResponse(operationObjectRef, def);
344
- await this.#errorResponse(operationObjectRef, def);
410
+ let operationObjectRef;
411
+ if (def.route.spec !== void 0) {
412
+ operationObjectRef = def.route.spec;
413
+ } else {
414
+ operationObjectRef = {
415
+ operationId,
416
+ summary: def.route.summary,
417
+ description: def.route.description,
418
+ deprecated: def.route.deprecated,
419
+ tags: def.route.tags?.map((tag) => tag)
420
+ };
421
+ await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions);
422
+ await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions);
423
+ await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema);
424
+ }
345
425
  doc.paths ??= {};
346
426
  doc.paths[httpPath] ??= {};
347
427
  doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract);
@@ -364,22 +444,96 @@ ${errors.join("\n\n")}`
364
444
  }
365
445
  return this.serializer.serialize(doc)[0];
366
446
  }
367
- async #request(ref, def) {
447
+ async #resolveCommonSchemas(doc, commonSchemas) {
448
+ let undefinedErrorJsonSchema = {
449
+ type: "object",
450
+ properties: {
451
+ defined: { const: false },
452
+ code: { type: "string" },
453
+ status: { type: "number" },
454
+ message: { type: "string" },
455
+ data: {}
456
+ },
457
+ required: ["defined", "code", "status", "message"]
458
+ };
459
+ const baseSchemaConvertOptions = {};
460
+ if (commonSchemas) {
461
+ baseSchemaConvertOptions.components = [];
462
+ for (const key in commonSchemas) {
463
+ const options = commonSchemas[key];
464
+ if (options.schema === void 0) {
465
+ continue;
466
+ }
467
+ const { schema, strategy = "input" } = options;
468
+ const [required, json] = await this.converter.convert(schema, { strategy });
469
+ const allowedStrategies = [strategy];
470
+ if (strategy === "input") {
471
+ const [outputRequired, outputJson] = await this.converter.convert(schema, { strategy: "output" });
472
+ if (outputRequired === required && stringifyJSON(outputJson) === stringifyJSON(json)) {
473
+ allowedStrategies.push("output");
474
+ }
475
+ } else if (strategy === "output") {
476
+ const [inputRequired, inputJson] = await this.converter.convert(schema, { strategy: "input" });
477
+ if (inputRequired === required && stringifyJSON(inputJson) === stringifyJSON(json)) {
478
+ allowedStrategies.push("input");
479
+ }
480
+ }
481
+ baseSchemaConvertOptions.components.push({
482
+ schema,
483
+ required,
484
+ ref: `#/components/schemas/${key}`,
485
+ allowedStrategies
486
+ });
487
+ }
488
+ doc.components ??= {};
489
+ doc.components.schemas ??= {};
490
+ for (const key in commonSchemas) {
491
+ const options = commonSchemas[key];
492
+ if (options.schema === void 0) {
493
+ if (options.error === "UndefinedError") {
494
+ doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema);
495
+ undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` };
496
+ }
497
+ continue;
498
+ }
499
+ const { schema, strategy = "input" } = options;
500
+ const [, json] = await this.converter.convert(
501
+ schema,
502
+ {
503
+ ...baseSchemaConvertOptions,
504
+ strategy,
505
+ minStructureDepthForRef: 1
506
+ // not allow use $ref for root schemas
507
+ }
508
+ );
509
+ doc.components.schemas[key] = toOpenAPISchema(json);
510
+ }
511
+ }
512
+ return { baseSchemaConvertOptions, undefinedErrorJsonSchema };
513
+ }
514
+ async #request(doc, ref, def, baseSchemaConvertOptions) {
368
515
  const method = fallbackContractConfig("defaultMethod", def.route.method);
369
516
  const details = getEventIteratorSchemaDetails(def.inputSchema);
370
517
  if (details) {
371
518
  ref.requestBody = {
372
519
  required: true,
373
520
  content: toOpenAPIEventIteratorContent(
374
- await this.converter.convert(details.yields, { strategy: "input" }),
375
- await this.converter.convert(details.returns, { strategy: "input" })
521
+ await this.converter.convert(details.yields, { ...baseSchemaConvertOptions, strategy: "input" }),
522
+ await this.converter.convert(details.returns, { ...baseSchemaConvertOptions, strategy: "input" })
376
523
  )
377
524
  };
378
525
  return;
379
526
  }
380
527
  const dynamicParams = getDynamicParams(def.route.path)?.map((v) => v.name);
381
528
  const inputStructure = fallbackContractConfig("defaultInputStructure", def.route.inputStructure);
382
- let [required, schema] = await this.converter.convert(def.inputSchema, { strategy: "input" });
529
+ let [required, schema] = await this.converter.convert(
530
+ def.inputSchema,
531
+ {
532
+ ...baseSchemaConvertOptions,
533
+ strategy: "input",
534
+ minStructureDepthForRef: dynamicParams?.length || inputStructure === "detailed" ? 1 : 0
535
+ }
536
+ );
383
537
  if (isAnySchema(schema) && !dynamicParams?.length) {
384
538
  return;
385
539
  }
@@ -422,7 +576,8 @@ ${errors.join("\n\n")}`
422
576
  if (!isObjectSchema(schema)) {
423
577
  throw error;
424
578
  }
425
- if (dynamicParams?.length && (schema.properties?.params === void 0 || !isObjectSchema(schema.properties.params) || !checkParamsSchema(schema.properties.params, dynamicParams))) {
579
+ const resolvedParamSchema = schema.properties?.params !== void 0 ? resolveOpenAPIJsonSchemaRef(doc, schema.properties.params) : void 0;
580
+ if (dynamicParams?.length && (resolvedParamSchema === void 0 || !isObjectSchema(resolvedParamSchema) || !checkParamsSchema(resolvedParamSchema, dynamicParams))) {
426
581
  throw new OpenAPIGeneratorError(
427
582
  'When input structure is "detailed" and path has dynamic params, the "params" schema must be an object with all dynamic params as required.'
428
583
  );
@@ -430,12 +585,13 @@ ${errors.join("\n\n")}`
430
585
  for (const from of ["params", "query", "headers"]) {
431
586
  const fromSchema = schema.properties?.[from];
432
587
  if (fromSchema !== void 0) {
433
- if (!isObjectSchema(fromSchema)) {
588
+ const resolvedSchema = resolveOpenAPIJsonSchemaRef(doc, fromSchema);
589
+ if (!isObjectSchema(resolvedSchema)) {
434
590
  throw error;
435
591
  }
436
592
  const parameterIn = from === "params" ? "path" : from === "headers" ? "header" : "query";
437
593
  ref.parameters ??= [];
438
- ref.parameters.push(...toOpenAPIParameters(fromSchema, parameterIn));
594
+ ref.parameters.push(...toOpenAPIParameters(resolvedSchema, parameterIn));
439
595
  }
440
596
  }
441
597
  if (schema.properties?.body !== void 0) {
@@ -445,7 +601,7 @@ ${errors.join("\n\n")}`
445
601
  };
446
602
  }
447
603
  }
448
- async #successResponse(ref, def) {
604
+ async #successResponse(doc, ref, def, baseSchemaConvertOptions) {
449
605
  const outputSchema = def.outputSchema;
450
606
  const status = fallbackContractConfig("defaultSuccessStatus", def.route.successStatus);
451
607
  const description = fallbackContractConfig("defaultSuccessDescription", def.route?.successDescription);
@@ -456,46 +612,90 @@ ${errors.join("\n\n")}`
456
612
  ref.responses[status] = {
457
613
  description,
458
614
  content: toOpenAPIEventIteratorContent(
459
- await this.converter.convert(eventIteratorSchemaDetails.yields, { strategy: "output" }),
460
- await this.converter.convert(eventIteratorSchemaDetails.returns, { strategy: "output" })
615
+ await this.converter.convert(eventIteratorSchemaDetails.yields, { ...baseSchemaConvertOptions, strategy: "output" }),
616
+ await this.converter.convert(eventIteratorSchemaDetails.returns, { ...baseSchemaConvertOptions, strategy: "output" })
461
617
  )
462
618
  };
463
619
  return;
464
620
  }
465
- const [required, json] = await this.converter.convert(outputSchema, { strategy: "output" });
466
- ref.responses ??= {};
467
- ref.responses[status] = {
468
- description
469
- };
621
+ const [required, json] = await this.converter.convert(
622
+ outputSchema,
623
+ {
624
+ ...baseSchemaConvertOptions,
625
+ strategy: "output",
626
+ minStructureDepthForRef: outputStructure === "detailed" ? 1 : 0
627
+ }
628
+ );
470
629
  if (outputStructure === "compact") {
630
+ ref.responses ??= {};
631
+ ref.responses[status] = {
632
+ description
633
+ };
471
634
  ref.responses[status].content = toOpenAPIContent(applySchemaOptionality(required, json));
472
635
  return;
473
636
  }
474
- const error = new OpenAPIGeneratorError(
475
- 'When output structure is "detailed", output schema must satisfy: { headers?: Record<string, unknown>, body?: unknown }'
476
- );
477
- if (!isObjectSchema(json)) {
478
- throw error;
479
- }
480
- if (json.properties?.headers !== void 0) {
481
- if (!isObjectSchema(json.properties.headers)) {
637
+ const handledStatuses = /* @__PURE__ */ new Set();
638
+ for (const item of expandUnionSchema(json)) {
639
+ const error = new OpenAPIGeneratorError(`
640
+ When output structure is "detailed", output schema must satisfy:
641
+ {
642
+ status?: number, // must be a literal number and in the range of 200-399
643
+ headers?: Record<string, unknown>,
644
+ body?: unknown
645
+ }
646
+
647
+ But got: ${stringifyJSON(item)}
648
+ `);
649
+ if (!isObjectSchema(item)) {
482
650
  throw error;
483
651
  }
484
- for (const key in json.properties.headers.properties) {
485
- ref.responses[status].headers ??= {};
486
- ref.responses[status].headers[key] = {
487
- schema: toOpenAPISchema(json.properties.headers.properties[key]),
488
- required: json.properties.headers.required?.includes(key)
489
- };
652
+ let schemaStatus;
653
+ let schemaDescription;
654
+ if (item.properties?.status !== void 0) {
655
+ const statusSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.status);
656
+ if (typeof statusSchema !== "object" || statusSchema.const === void 0 || typeof statusSchema.const !== "number" || !Number.isInteger(statusSchema.const) || isORPCErrorStatus(statusSchema.const)) {
657
+ throw error;
658
+ }
659
+ schemaStatus = statusSchema.const;
660
+ schemaDescription = statusSchema.description;
661
+ }
662
+ const itemStatus = schemaStatus ?? status;
663
+ const itemDescription = schemaDescription ?? description;
664
+ if (handledStatuses.has(itemStatus)) {
665
+ throw new OpenAPIGeneratorError(`
666
+ When output structure is "detailed", each success status must be unique.
667
+ But got status: ${itemStatus} used more than once.
668
+ `);
669
+ }
670
+ handledStatuses.add(itemStatus);
671
+ ref.responses ??= {};
672
+ ref.responses[itemStatus] = {
673
+ description: itemDescription
674
+ };
675
+ if (item.properties?.headers !== void 0) {
676
+ const headersSchema = resolveOpenAPIJsonSchemaRef(doc, item.properties.headers);
677
+ if (!isObjectSchema(headersSchema)) {
678
+ throw error;
679
+ }
680
+ for (const key in headersSchema.properties) {
681
+ const headerSchema = headersSchema.properties[key];
682
+ if (headerSchema !== void 0) {
683
+ ref.responses[itemStatus].headers ??= {};
684
+ ref.responses[itemStatus].headers[key] = {
685
+ schema: toOpenAPISchema(headerSchema),
686
+ required: item.required?.includes("headers") && headersSchema.required?.includes(key)
687
+ };
688
+ }
689
+ }
690
+ }
691
+ if (item.properties?.body !== void 0) {
692
+ ref.responses[itemStatus].content = toOpenAPIContent(
693
+ applySchemaOptionality(item.required?.includes("body") ?? false, item.properties.body)
694
+ );
490
695
  }
491
- }
492
- if (json.properties?.body !== void 0) {
493
- ref.responses[status].content = toOpenAPIContent(
494
- applySchemaOptionality(json.required?.includes("body") ?? false, json.properties.body)
495
- );
496
696
  }
497
697
  }
498
- async #errorResponse(ref, def) {
698
+ async #errorResponse(ref, def, baseSchemaConvertOptions, undefinedErrorSchema) {
499
699
  const errorMap = def.errorMap;
500
700
  const errors = {};
501
701
  for (const code in errorMap) {
@@ -505,7 +705,7 @@ ${errors.join("\n\n")}`
505
705
  }
506
706
  const status = fallbackORPCErrorStatus(code, config.status);
507
707
  const message = fallbackORPCErrorMessage(code, config.message);
508
- const [dataRequired, dataSchema] = await this.converter.convert(config.data, { strategy: "output" });
708
+ const [dataRequired, dataSchema] = await this.converter.convert(config.data, { ...baseSchemaConvertOptions, strategy: "output" });
509
709
  errors[status] ??= [];
510
710
  errors[status].push({
511
711
  type: "object",
@@ -527,17 +727,7 @@ ${errors.join("\n\n")}`
527
727
  content: toOpenAPIContent({
528
728
  oneOf: [
529
729
  ...schemas,
530
- {
531
- type: "object",
532
- properties: {
533
- defined: { const: false },
534
- code: { type: "string" },
535
- status: { type: "number" },
536
- message: { type: "string" },
537
- data: {}
538
- },
539
- required: ["defined", "code", "status", "message"]
540
- }
730
+ undefinedErrorSchema
541
731
  ]
542
732
  })
543
733
  };
@@ -545,4 +735,4 @@ ${errors.join("\n\n")}`
545
735
  }
546
736
  }
547
737
 
548
- export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, separateObjectSchema as s, toOpenAPIPath as t };
738
+ export { CompositeSchemaConverter as C, LOGIC_KEYWORDS as L, OpenAPIGenerator as O, applyCustomOpenAPIOperation as a, toOpenAPIMethod as b, customOpenAPIOperation as c, toOpenAPIContent as d, toOpenAPIEventIteratorContent as e, toOpenAPIParameters as f, getCustomOpenAPIOperation as g, checkParamsSchema as h, toOpenAPISchema as i, isFileSchema as j, isObjectSchema as k, isAnySchema as l, filterSchemaBranches as m, applySchemaOptionality as n, expandUnionSchema as o, expandArrayableSchema as p, isPrimitiveSchema as q, resolveOpenAPIJsonSchemaRef as r, separateObjectSchema as s, toOpenAPIPath as t };