@orpc/openapi 0.10.0 → 0.11.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.
@@ -1,655 +0,0 @@
1
- import {
2
- getCustomJSONSchema,
3
- getCustomZodFileMimeType,
4
- getCustomZodType,
5
- } from '@orpc/zod'
6
- import escapeStringRegexp from 'escape-string-regexp'
7
- import {
8
- Format,
9
- type JSONSchema,
10
- type keywords,
11
- } from 'json-schema-typed/draft-2020-12'
12
- import {
13
- type EnumLike,
14
- type KeySchema,
15
- type ZodAny,
16
- type ZodArray,
17
- type ZodBranded,
18
- type ZodCatch,
19
- type ZodDefault,
20
- type ZodDiscriminatedUnion,
21
- type ZodEffects,
22
- type ZodEnum,
23
- ZodFirstPartyTypeKind,
24
- type ZodIntersection,
25
- type ZodLazy,
26
- type ZodLiteral,
27
- type ZodMap,
28
- type ZodNativeEnum,
29
- type ZodNullable,
30
- type ZodNumber,
31
- type ZodObject,
32
- type ZodOptional,
33
- type ZodPipeline,
34
- type ZodRawShape,
35
- type ZodReadonly,
36
- type ZodRecord,
37
- type ZodSet,
38
- type ZodString,
39
- type ZodTuple,
40
- type ZodTypeAny,
41
- type ZodUnion,
42
- type ZodUnionOptions,
43
- } from 'zod'
44
-
45
- export const NON_LOGIC_KEYWORDS = [
46
- // Core Documentation Keywords
47
- '$anchor',
48
- '$comment',
49
- '$defs',
50
- '$id',
51
- 'title',
52
- 'description',
53
-
54
- // Value Keywords
55
- 'default',
56
- 'deprecated',
57
- 'examples',
58
-
59
- // Metadata Keywords
60
- '$schema',
61
- 'definitions', // Legacy, but still used
62
- 'readOnly',
63
- 'writeOnly',
64
-
65
- // Display and UI Hints
66
- 'contentMediaType',
67
- 'contentEncoding',
68
- 'format',
69
-
70
- // Custom Extensions
71
- '$vocabulary',
72
- '$dynamicAnchor',
73
- '$dynamicRef',
74
- ] satisfies (typeof keywords)[number][]
75
-
76
- export const UNSUPPORTED_JSON_SCHEMA = { not: {} }
77
- export const UNDEFINED_JSON_SCHEMA = { const: 'undefined' }
78
-
79
- export interface ZodToJsonSchemaOptions {
80
- /**
81
- * Max depth of lazy type, if it exceeds.
82
- *
83
- * Used `{}` when reach max depth
84
- *
85
- * @default 5
86
- */
87
- maxLazyDepth?: number
88
-
89
- /**
90
- * The length used to track the depth of lazy type
91
- *
92
- * @internal
93
- */
94
- lazyDepth?: number
95
-
96
- /**
97
- * The expected json schema for input or output zod schema
98
- *
99
- * @default input
100
- */
101
- mode?: 'input' | 'output'
102
-
103
- /**
104
- * Track if current level schema is handled custom json schema to prevent recursive
105
- *
106
- * @internal
107
- */
108
- isHandledCustomJSONSchema?: boolean
109
- }
110
-
111
- export function zodToJsonSchema(
112
- schema: ZodTypeAny,
113
- options?: ZodToJsonSchemaOptions,
114
- ): Exclude<JSONSchema, boolean> {
115
- if (!options?.isHandledCustomJSONSchema) {
116
- const customJSONSchema = getCustomJSONSchema(schema._def, options)
117
-
118
- if (customJSONSchema) {
119
- const json = zodToJsonSchema(schema, {
120
- ...options,
121
- isHandledCustomJSONSchema: true,
122
- })
123
-
124
- return {
125
- ...json,
126
- ...customJSONSchema,
127
- }
128
- }
129
- }
130
-
131
- const childOptions = { ...options, isHandledCustomJSONSchema: false }
132
-
133
- const customType = getCustomZodType(schema._def)
134
-
135
- switch (customType) {
136
- case 'Blob': {
137
- return { type: 'string', contentMediaType: '*/*' }
138
- }
139
-
140
- case 'File': {
141
- const mimeType = getCustomZodFileMimeType(schema._def) ?? '*/*'
142
-
143
- return { type: 'string', contentMediaType: mimeType }
144
- }
145
-
146
- case 'Invalid Date': {
147
- return { const: 'Invalid Date' }
148
- }
149
-
150
- case 'RegExp': {
151
- return {
152
- type: 'string',
153
- pattern: '^\\/(.*)\\/([a-z]*)$',
154
- }
155
- }
156
-
157
- case 'URL': {
158
- return { type: 'string', format: Format.URI }
159
- }
160
- }
161
-
162
- const _expectedCustomType: undefined = customType
163
-
164
- const typeName = schema._def.typeName as ZodFirstPartyTypeKind | undefined
165
-
166
- switch (typeName) {
167
- case ZodFirstPartyTypeKind.ZodString: {
168
- const schema_ = schema as ZodString
169
-
170
- const json: JSONSchema = { type: 'string' }
171
-
172
- for (const check of schema_._def.checks) {
173
- switch (check.kind) {
174
- case 'base64':
175
- json.contentEncoding = 'base64'
176
- break
177
- case 'cuid':
178
- json.pattern = '^[0-9A-HJKMNP-TV-Z]{26}$'
179
- break
180
- case 'email':
181
- json.format = Format.Email
182
- break
183
- case 'url':
184
- json.format = Format.URI
185
- break
186
- case 'uuid':
187
- json.format = Format.UUID
188
- break
189
- case 'regex':
190
- json.pattern = check.regex.source
191
- break
192
- case 'min':
193
- json.minLength = check.value
194
- break
195
- case 'max':
196
- json.maxLength = check.value
197
- break
198
- case 'length':
199
- json.minLength = check.value
200
- json.maxLength = check.value
201
- break
202
- case 'includes':
203
- json.pattern = escapeStringRegexp(check.value)
204
- break
205
- case 'startsWith':
206
- json.pattern = `^${escapeStringRegexp(check.value)}`
207
- break
208
- case 'endsWith':
209
- json.pattern = `${escapeStringRegexp(check.value)}$`
210
- break
211
- case 'emoji':
212
- json.pattern
213
- = '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'
214
- break
215
- case 'nanoid':
216
- json.pattern = '^[a-zA-Z0-9_-]{21}$'
217
- break
218
- case 'cuid2':
219
- json.pattern = '^[0-9a-z]+$'
220
- break
221
- case 'ulid':
222
- json.pattern = '^[0-9A-HJKMNP-TV-Z]{26}$'
223
- break
224
- case 'datetime':
225
- json.format = Format.DateTime
226
- break
227
- case 'date':
228
- json.format = Format.Date
229
- break
230
- case 'time':
231
- json.format = Format.Time
232
- break
233
- case 'duration':
234
- json.format = Format.Duration
235
- break
236
- case 'ip':
237
- json.format = Format.IPv4
238
- break
239
- default: {
240
- const _expect: 'toLowerCase' | 'toUpperCase' | 'trim' = check.kind
241
- }
242
- }
243
- }
244
-
245
- return json
246
- }
247
-
248
- case ZodFirstPartyTypeKind.ZodNumber: {
249
- const schema_ = schema as ZodNumber
250
-
251
- const json: JSONSchema = { type: 'number' }
252
-
253
- for (const check of schema_._def.checks) {
254
- switch (check.kind) {
255
- case 'int':
256
- json.type = 'integer'
257
- break
258
- case 'min':
259
- json.minimum = check.value
260
- break
261
- case 'max':
262
- json.maximum = check.value
263
- break
264
- case 'multipleOf':
265
- json.multipleOf = check.value
266
- break
267
- default: {
268
- const _expect: 'finite' = check.kind
269
- }
270
- }
271
- }
272
-
273
- return json
274
- }
275
-
276
- case ZodFirstPartyTypeKind.ZodNaN: {
277
- return { const: 'NaN' }
278
- }
279
-
280
- case ZodFirstPartyTypeKind.ZodBigInt: {
281
- const json: JSONSchema = { type: 'string', pattern: '^-?[0-9]+$' }
282
-
283
- // WARN: ignore checks
284
-
285
- return json
286
- }
287
-
288
- case ZodFirstPartyTypeKind.ZodBoolean: {
289
- return { type: 'boolean' }
290
- }
291
-
292
- case ZodFirstPartyTypeKind.ZodDate: {
293
- const jsonSchema: JSONSchema = { type: 'string', format: Format.Date }
294
-
295
- // WARN: ignore checks
296
-
297
- return jsonSchema
298
- }
299
-
300
- case ZodFirstPartyTypeKind.ZodNull: {
301
- return { type: 'null' }
302
- }
303
-
304
- case ZodFirstPartyTypeKind.ZodVoid:
305
- case ZodFirstPartyTypeKind.ZodUndefined: {
306
- return UNDEFINED_JSON_SCHEMA
307
- }
308
-
309
- case ZodFirstPartyTypeKind.ZodLiteral: {
310
- const schema_ = schema as ZodLiteral<unknown>
311
- return { const: schema_._def.value }
312
- }
313
-
314
- case ZodFirstPartyTypeKind.ZodEnum: {
315
- const schema_ = schema as ZodEnum<[string, ...string[]]>
316
-
317
- return {
318
- enum: schema_._def.values,
319
- }
320
- }
321
-
322
- case ZodFirstPartyTypeKind.ZodNativeEnum: {
323
- const schema_ = schema as ZodNativeEnum<EnumLike>
324
-
325
- return {
326
- enum: Object.values(schema_._def.values),
327
- }
328
- }
329
-
330
- case ZodFirstPartyTypeKind.ZodArray: {
331
- const schema_ = schema as ZodArray<ZodTypeAny>
332
- const def = schema_._def
333
-
334
- const json: JSONSchema = { type: 'array' }
335
-
336
- if (def.exactLength) {
337
- json.maxItems = def.exactLength.value
338
- json.minItems = def.exactLength.value
339
- }
340
-
341
- if (def.minLength) {
342
- json.minItems = def.minLength.value
343
- }
344
-
345
- if (def.maxLength) {
346
- json.maxItems = def.maxLength.value
347
- }
348
-
349
- return json
350
- }
351
-
352
- case ZodFirstPartyTypeKind.ZodTuple: {
353
- const schema_ = schema as ZodTuple<
354
- [ZodTypeAny, ...ZodTypeAny[]],
355
- ZodTypeAny | null
356
- >
357
-
358
- const prefixItems: JSONSchema[] = []
359
- const json: JSONSchema = { type: 'array' }
360
-
361
- for (const item of schema_._def.items) {
362
- prefixItems.push(zodToJsonSchema(item, childOptions))
363
- }
364
-
365
- if (prefixItems?.length) {
366
- json.prefixItems = prefixItems
367
- }
368
-
369
- if (schema_._def.rest) {
370
- const items = zodToJsonSchema(schema_._def.rest, childOptions)
371
- if (items) {
372
- json.items = items
373
- }
374
- }
375
-
376
- return json
377
- }
378
-
379
- case ZodFirstPartyTypeKind.ZodObject: {
380
- const schema_ = schema as ZodObject<ZodRawShape>
381
-
382
- const json: JSONSchema = { type: 'object' }
383
- const properties: Record<string, JSONSchema> = {}
384
- const required: string[] = []
385
-
386
- for (const [key, value] of Object.entries(schema_.shape)) {
387
- const { schema, matches } = extractJSONSchema(
388
- zodToJsonSchema(value, childOptions),
389
- schema => schema === UNDEFINED_JSON_SCHEMA,
390
- )
391
-
392
- if (schema) {
393
- properties[key] = schema
394
- }
395
-
396
- if (matches.length === 0) {
397
- required.push(key)
398
- }
399
- }
400
-
401
- if (Object.keys(properties).length) {
402
- json.properties = properties
403
- }
404
-
405
- if (required.length) {
406
- json.required = required
407
- }
408
-
409
- const additionalProperties = zodToJsonSchema(
410
- schema_._def.catchall,
411
- childOptions,
412
- )
413
- if (schema_._def.unknownKeys === 'strict') {
414
- json.additionalProperties
415
- = additionalProperties === UNSUPPORTED_JSON_SCHEMA
416
- ? false
417
- : additionalProperties
418
- }
419
- else {
420
- if (
421
- additionalProperties
422
- && additionalProperties !== UNSUPPORTED_JSON_SCHEMA
423
- ) {
424
- json.additionalProperties = additionalProperties
425
- }
426
- }
427
-
428
- return json
429
- }
430
-
431
- case ZodFirstPartyTypeKind.ZodRecord: {
432
- const schema_ = schema as ZodRecord<KeySchema, ZodAny>
433
-
434
- const json: JSONSchema = { type: 'object' }
435
-
436
- json.additionalProperties = zodToJsonSchema(
437
- schema_._def.valueType,
438
- childOptions,
439
- )
440
-
441
- return json
442
- }
443
-
444
- case ZodFirstPartyTypeKind.ZodSet: {
445
- const schema_ = schema as ZodSet
446
-
447
- return {
448
- type: 'array',
449
- items: zodToJsonSchema(schema_._def.valueType, childOptions),
450
- }
451
- }
452
-
453
- case ZodFirstPartyTypeKind.ZodMap: {
454
- const schema_ = schema as ZodMap
455
-
456
- return {
457
- type: 'array',
458
- items: {
459
- type: 'array',
460
- prefixItems: [
461
- zodToJsonSchema(schema_._def.keyType, childOptions),
462
- zodToJsonSchema(schema_._def.valueType, childOptions),
463
- ],
464
- maxItems: 2,
465
- minItems: 2,
466
- },
467
- }
468
- }
469
-
470
- case ZodFirstPartyTypeKind.ZodUnion:
471
- case ZodFirstPartyTypeKind.ZodDiscriminatedUnion: {
472
- const schema_ = schema as
473
- | ZodUnion<ZodUnionOptions>
474
- | ZodDiscriminatedUnion<string, [ZodObject<any>, ...ZodObject<any>[]]>
475
-
476
- const anyOf: JSONSchema[] = []
477
-
478
- for (const s of schema_._def.options) {
479
- anyOf.push(zodToJsonSchema(s, childOptions))
480
- }
481
-
482
- return { anyOf }
483
- }
484
-
485
- case ZodFirstPartyTypeKind.ZodIntersection: {
486
- const schema_ = schema as ZodIntersection<ZodTypeAny, ZodTypeAny>
487
-
488
- const allOf: JSONSchema[] = []
489
-
490
- for (const s of [schema_._def.left, schema_._def.right]) {
491
- allOf.push(zodToJsonSchema(s, childOptions))
492
- }
493
-
494
- return { allOf }
495
- }
496
-
497
- case ZodFirstPartyTypeKind.ZodLazy: {
498
- const schema_ = schema as ZodLazy<ZodTypeAny>
499
-
500
- const maxLazyDepth = childOptions?.maxLazyDepth ?? 5
501
- const lazyDepth = childOptions?.lazyDepth ?? 0
502
-
503
- if (lazyDepth > maxLazyDepth) {
504
- return {}
505
- }
506
-
507
- return zodToJsonSchema(schema_._def.getter(), {
508
- ...childOptions,
509
- lazyDepth: lazyDepth + 1,
510
- })
511
- }
512
-
513
- case ZodFirstPartyTypeKind.ZodUnknown:
514
- case ZodFirstPartyTypeKind.ZodAny:
515
- case undefined: {
516
- return {}
517
- }
518
-
519
- case ZodFirstPartyTypeKind.ZodOptional: {
520
- const schema_ = schema as ZodOptional<ZodTypeAny>
521
-
522
- const inner = zodToJsonSchema(schema_._def.innerType, childOptions)
523
-
524
- return {
525
- anyOf: [UNDEFINED_JSON_SCHEMA, inner],
526
- }
527
- }
528
-
529
- case ZodFirstPartyTypeKind.ZodReadonly: {
530
- const schema_ = schema as ZodReadonly<ZodTypeAny>
531
- return zodToJsonSchema(schema_._def.innerType, childOptions)
532
- }
533
-
534
- case ZodFirstPartyTypeKind.ZodDefault: {
535
- const schema_ = schema as ZodDefault<ZodTypeAny>
536
- return zodToJsonSchema(schema_._def.innerType, childOptions)
537
- }
538
-
539
- case ZodFirstPartyTypeKind.ZodEffects: {
540
- const schema_ = schema as ZodEffects<ZodTypeAny>
541
-
542
- if (
543
- schema_._def.effect.type === 'transform'
544
- && childOptions?.mode === 'output'
545
- ) {
546
- return {}
547
- }
548
-
549
- return zodToJsonSchema(schema_._def.schema, childOptions)
550
- }
551
-
552
- case ZodFirstPartyTypeKind.ZodCatch: {
553
- const schema_ = schema as ZodCatch<ZodTypeAny>
554
- return zodToJsonSchema(schema_._def.innerType, childOptions)
555
- }
556
-
557
- case ZodFirstPartyTypeKind.ZodBranded: {
558
- const schema_ = schema as ZodBranded<ZodTypeAny, string | number | symbol>
559
- return zodToJsonSchema(schema_._def.type, childOptions)
560
- }
561
-
562
- case ZodFirstPartyTypeKind.ZodPipeline: {
563
- const schema_ = schema as ZodPipeline<ZodTypeAny, ZodTypeAny>
564
- return zodToJsonSchema(
565
- childOptions?.mode === 'output' ? schema_._def.out : schema_._def.in,
566
- childOptions,
567
- )
568
- }
569
-
570
- case ZodFirstPartyTypeKind.ZodNullable: {
571
- const schema_ = schema as ZodNullable<ZodTypeAny>
572
-
573
- const inner = zodToJsonSchema(schema_._def.innerType, childOptions)
574
-
575
- return {
576
- anyOf: [{ type: 'null' }, inner],
577
- }
578
- }
579
- }
580
-
581
- const _expected:
582
- | ZodFirstPartyTypeKind.ZodPromise
583
- | ZodFirstPartyTypeKind.ZodSymbol
584
- | ZodFirstPartyTypeKind.ZodFunction
585
- | ZodFirstPartyTypeKind.ZodNever = typeName
586
-
587
- return UNSUPPORTED_JSON_SCHEMA
588
- }
589
-
590
- export function extractJSONSchema(
591
- schema: JSONSchema,
592
- check: (schema: JSONSchema) => boolean,
593
- matches: JSONSchema[] = [],
594
- ): { schema: JSONSchema | undefined, matches: JSONSchema[] } {
595
- if (check(schema)) {
596
- matches.push(schema)
597
- return { schema: undefined, matches }
598
- }
599
-
600
- if (typeof schema === 'boolean') {
601
- return { schema, matches }
602
- }
603
-
604
- // TODO: $ref
605
-
606
- if (
607
- schema.anyOf
608
- && Object.keys(schema).every(
609
- k => k === 'anyOf' || NON_LOGIC_KEYWORDS.includes(k as any),
610
- )
611
- ) {
612
- const anyOf = schema.anyOf
613
- .map(s => extractJSONSchema(s, check, matches).schema)
614
- .filter(v => !!v)
615
-
616
- if (anyOf.length === 1 && typeof anyOf[0] === 'object') {
617
- return { schema: { ...schema, anyOf: undefined, ...anyOf[0] }, matches }
618
- }
619
-
620
- return {
621
- schema: {
622
- ...schema,
623
- anyOf,
624
- },
625
- matches,
626
- }
627
- }
628
-
629
- // TODO: $ref
630
-
631
- if (
632
- schema.oneOf
633
- && Object.keys(schema).every(
634
- k => k === 'oneOf' || NON_LOGIC_KEYWORDS.includes(k as any),
635
- )
636
- ) {
637
- const oneOf = schema.oneOf
638
- .map(s => extractJSONSchema(s, check, matches).schema)
639
- .filter(v => !!v)
640
-
641
- if (oneOf.length === 1 && typeof oneOf[0] === 'object') {
642
- return { schema: { ...schema, oneOf: undefined, ...oneOf[0] }, matches }
643
- }
644
-
645
- return {
646
- schema: {
647
- ...schema,
648
- oneOf,
649
- },
650
- matches,
651
- }
652
- }
653
-
654
- return { schema, matches }
655
- }