@kubb/plugin-oas 0.0.0-canary-20240509211223

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/OperationGenerator-DvnXUUp4.d.ts +56 -0
  4. package/dist/OperationGenerator-X6CTMfhG.d.cts +56 -0
  5. package/dist/Schema-BWPWyiQO.d.cts +39 -0
  6. package/dist/Schema-DFZBfjF2.d.ts +39 -0
  7. package/dist/SchemaGenerator-hK5SHxTI.d.ts +316 -0
  8. package/dist/SchemaGenerator-z_7YrAAB.d.cts +316 -0
  9. package/dist/chunk-2MJ2CQMI.js +61 -0
  10. package/dist/chunk-2MJ2CQMI.js.map +1 -0
  11. package/dist/chunk-3RCQ2LNT.js +81 -0
  12. package/dist/chunk-3RCQ2LNT.js.map +1 -0
  13. package/dist/chunk-55NUFNT6.cjs +68 -0
  14. package/dist/chunk-55NUFNT6.cjs.map +1 -0
  15. package/dist/chunk-ECXSQTMV.cjs +81 -0
  16. package/dist/chunk-ECXSQTMV.cjs.map +1 -0
  17. package/dist/chunk-EPTOYYAP.js +3312 -0
  18. package/dist/chunk-EPTOYYAP.js.map +1 -0
  19. package/dist/chunk-H52M2RUX.cjs +3312 -0
  20. package/dist/chunk-H52M2RUX.cjs.map +1 -0
  21. package/dist/chunk-LQ6IAWRX.js +68 -0
  22. package/dist/chunk-LQ6IAWRX.js.map +1 -0
  23. package/dist/chunk-VNFSHGSN.cjs +61 -0
  24. package/dist/chunk-VNFSHGSN.cjs.map +1 -0
  25. package/dist/components.cjs +18 -0
  26. package/dist/components.cjs.map +1 -0
  27. package/dist/components.d.cts +41 -0
  28. package/dist/components.d.ts +41 -0
  29. package/dist/components.js +18 -0
  30. package/dist/components.js.map +1 -0
  31. package/dist/hooks.cjs +152 -0
  32. package/dist/hooks.cjs.map +1 -0
  33. package/dist/hooks.d.cts +76 -0
  34. package/dist/hooks.d.ts +76 -0
  35. package/dist/hooks.js +152 -0
  36. package/dist/hooks.js.map +1 -0
  37. package/dist/index.cjs +985 -0
  38. package/dist/index.cjs.map +1 -0
  39. package/dist/index.d.cts +17 -0
  40. package/dist/index.d.ts +17 -0
  41. package/dist/index.js +985 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types-n5zV4Q3s.d.cts +146 -0
  44. package/dist/types-n5zV4Q3s.d.ts +146 -0
  45. package/dist/utils.cjs +126 -0
  46. package/dist/utils.cjs.map +1 -0
  47. package/dist/utils.d.cts +80 -0
  48. package/dist/utils.d.ts +80 -0
  49. package/dist/utils.js +126 -0
  50. package/dist/utils.js.map +1 -0
  51. package/package.json +107 -0
  52. package/src/OperationGenerator.ts +328 -0
  53. package/src/SchemaGenerator.ts +827 -0
  54. package/src/SchemaMapper.ts +145 -0
  55. package/src/components/Oas.tsx +31 -0
  56. package/src/components/Operation.tsx +21 -0
  57. package/src/components/Schema.tsx +156 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/hooks/index.ts +5 -0
  60. package/src/hooks/useOas.ts +15 -0
  61. package/src/hooks/useOperation.ts +18 -0
  62. package/src/hooks/useOperationManager.ts +142 -0
  63. package/src/hooks/useOperations.ts +40 -0
  64. package/src/hooks/useSchema.ts +23 -0
  65. package/src/index.ts +30 -0
  66. package/src/plugin.ts +133 -0
  67. package/src/types.ts +147 -0
  68. package/src/utils/getComments.ts +15 -0
  69. package/src/utils/getGroupedByTagFiles.ts +81 -0
  70. package/src/utils/getParams.ts +57 -0
  71. package/src/utils/getSchemaFactory.ts +33 -0
  72. package/src/utils/getSchemas.ts +45 -0
  73. package/src/utils/index.ts +8 -0
  74. package/src/utils/parseFromConfig.ts +36 -0
  75. package/src/utils/refSorter.ts +13 -0
@@ -0,0 +1,827 @@
1
+ import { Generator } from '@kubb/core'
2
+ import transformers, { pascalCase } from '@kubb/core/transformers'
3
+ import { getUniqueName } from '@kubb/core/utils'
4
+
5
+ import { isReference } from '@kubb/oas'
6
+ import { isDeepEqual, isNumber, uniqueWith } from 'remeda'
7
+ import { isKeyword, schemaKeywords } from './SchemaMapper.ts'
8
+ import { getSchemaFactory } from './utils/getSchemaFactory.ts'
9
+ import { getSchemas } from './utils/getSchemas.ts'
10
+
11
+ import type { KubbFile, Plugin, PluginFactoryOptions, PluginManager, ResolveNameParams } from '@kubb/core'
12
+ import type { Oas, OpenAPIV3, SchemaObject, contentType } from '@kubb/oas'
13
+ import type { Schema, SchemaKeywordMapper } from './SchemaMapper.ts'
14
+ import type { OperationSchema, Override, Refs } from './types.ts'
15
+
16
+ export type SchemaMethodResult<TFileMeta extends KubbFile.FileMetaBase> = Promise<KubbFile.File<TFileMeta> | Array<KubbFile.File<TFileMeta>> | null>
17
+
18
+ type Context<TOptions, TPluginOptions extends PluginFactoryOptions> = {
19
+ oas: Oas
20
+ pluginManager: PluginManager
21
+ /**
22
+ * Current plugin
23
+ */
24
+ plugin: Plugin<TPluginOptions>
25
+ mode: KubbFile.Mode
26
+ include?: Array<'schemas' | 'responses' | 'requestBodies'>
27
+ override: Array<Override<TOptions>> | undefined
28
+ contentType?: contentType
29
+ output?: string
30
+ }
31
+
32
+ export type SchemaGeneratorOptions = {
33
+ dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
34
+ unknownType: 'any' | 'unknown'
35
+ enumType?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal'
36
+ enumSuffix?: string
37
+ usedEnumNames?: Record<string, number>
38
+ mapper?: Record<string, string>
39
+ typed?: boolean
40
+ transformers: {
41
+ /**
42
+ * Customize the names based on the type that is provided by the plugin.
43
+ */
44
+ name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
45
+ /**
46
+ * Receive schema and name(propertName) and return FakerMeta array
47
+ * TODO TODO add docs
48
+ * @beta
49
+ */
50
+ schema?: (schemaProps: SchemaProps, defaultSchemas: Schema[]) => Schema[] | undefined
51
+ }
52
+ }
53
+
54
+ export type SchemaGeneratorBuildOptions = Omit<OperationSchema, 'name' | 'schema'>
55
+
56
+ type SchemaProps = {
57
+ schema?: SchemaObject
58
+ name?: string
59
+ parentName?: string
60
+ }
61
+
62
+ export abstract class SchemaGenerator<
63
+ TOptions extends SchemaGeneratorOptions = SchemaGeneratorOptions,
64
+ TPluginOptions extends PluginFactoryOptions = PluginFactoryOptions,
65
+ TFileMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase,
66
+ > extends Generator<TOptions, Context<TOptions, TPluginOptions>> {
67
+ // Collect the types of all referenced schemas, so we can export them later
68
+ refs: Refs = {}
69
+
70
+ // Keep track of already used type aliases
71
+ #usedAliasNames: Record<string, number> = {}
72
+
73
+ /**
74
+ * Creates a type node from a given schema.
75
+ * Delegates to getBaseTypeFromSchema internally and
76
+ * optionally adds a union with null.
77
+ */
78
+ buildSchemas(props: SchemaProps): Schema[] {
79
+ const options = this.#getOptions(props)
80
+
81
+ const defaultSchemas = this.#parseSchemaObject(props)
82
+ const schemas = options.transformers?.schema?.(props, defaultSchemas) || defaultSchemas || []
83
+
84
+ return uniqueWith<Schema>(schemas, isDeepEqual)
85
+ }
86
+
87
+ deepSearch<T extends keyof SchemaKeywordMapper>(schemas: Schema[] | undefined, keyword: T): SchemaKeywordMapper[T][] {
88
+ return SchemaGenerator.deepSearch<T>(schemas, keyword)
89
+ }
90
+
91
+ find<T extends keyof SchemaKeywordMapper>(schemas: Schema[] | undefined, keyword: T): SchemaKeywordMapper[T] | undefined {
92
+ return SchemaGenerator.find<T>(schemas, keyword)
93
+ }
94
+
95
+ static deepSearch<T extends keyof SchemaKeywordMapper>(schemas: Schema[] | undefined, keyword: T): SchemaKeywordMapper[T][] {
96
+ const foundItems: SchemaKeywordMapper[T][] = []
97
+
98
+ schemas?.forEach((schema) => {
99
+ if (schema.keyword === keyword) {
100
+ foundItems.push(schema as SchemaKeywordMapper[T])
101
+ }
102
+
103
+ if (schema.keyword === schemaKeywords.object) {
104
+ const subItem = schema as SchemaKeywordMapper['object']
105
+
106
+ Object.values(subItem.args?.properties || {}).forEach((entrySchema) => {
107
+ foundItems.push(...SchemaGenerator.deepSearch<T>(entrySchema, keyword))
108
+ })
109
+
110
+ Object.values(subItem.args?.additionalProperties || {}).forEach((entrySchema) => {
111
+ foundItems.push(...SchemaGenerator.deepSearch<T>([entrySchema], keyword))
112
+ })
113
+ }
114
+
115
+ if (schema.keyword === schemaKeywords.array) {
116
+ const subItem = schema as SchemaKeywordMapper['array']
117
+
118
+ subItem.args.items.forEach((entrySchema) => {
119
+ foundItems.push(...SchemaGenerator.deepSearch<T>([entrySchema], keyword))
120
+ })
121
+ }
122
+
123
+ if (schema.keyword === schemaKeywords.and) {
124
+ const subItem = schema as SchemaKeywordMapper['and']
125
+
126
+ subItem.args.forEach((entrySchema) => {
127
+ foundItems.push(...SchemaGenerator.deepSearch<T>([entrySchema], keyword))
128
+ })
129
+ }
130
+
131
+ if (schema.keyword === schemaKeywords.tuple) {
132
+ const subItem = schema as SchemaKeywordMapper['tuple']
133
+
134
+ subItem.args.forEach((entrySchema) => {
135
+ foundItems.push(...SchemaGenerator.deepSearch<T>([entrySchema], keyword))
136
+ })
137
+ }
138
+
139
+ if (schema.keyword === schemaKeywords.union) {
140
+ const subItem = schema as SchemaKeywordMapper['union']
141
+
142
+ subItem.args.forEach((entrySchema) => {
143
+ foundItems.push(...SchemaGenerator.deepSearch<T>([entrySchema], keyword))
144
+ })
145
+ }
146
+ })
147
+
148
+ return foundItems
149
+ }
150
+
151
+ static findInObject<T extends keyof SchemaKeywordMapper>(schemas: Schema[] | undefined, keyword: T): SchemaKeywordMapper[T] | undefined {
152
+ let foundItem: SchemaKeywordMapper[T] | undefined = undefined
153
+
154
+ schemas?.forEach((schema) => {
155
+ if (!foundItem && schema.keyword === keyword) {
156
+ foundItem = schema as SchemaKeywordMapper[T]
157
+ }
158
+
159
+ if (schema.keyword === schemaKeywords.object) {
160
+ const subItem = schema as SchemaKeywordMapper['object']
161
+
162
+ Object.values(subItem.args?.properties || {}).forEach((entrySchema) => {
163
+ if (!foundItem) {
164
+ foundItem = SchemaGenerator.find<T>(entrySchema, keyword)
165
+ }
166
+ })
167
+
168
+ Object.values(subItem.args?.additionalProperties || {}).forEach((entrySchema) => {
169
+ if (!foundItem) {
170
+ foundItem = SchemaGenerator.find<T>([entrySchema], keyword)
171
+ }
172
+ })
173
+ }
174
+ })
175
+
176
+ return foundItem
177
+ }
178
+
179
+ static find<T extends keyof SchemaKeywordMapper>(schemas: Schema[] | undefined, keyword: T): SchemaKeywordMapper[T] | undefined {
180
+ let foundItem: SchemaKeywordMapper[T] | undefined = undefined
181
+
182
+ schemas?.forEach((schema) => {
183
+ if (!foundItem && schema.keyword === keyword) {
184
+ foundItem = schema as SchemaKeywordMapper[T]
185
+ }
186
+
187
+ if (schema.keyword === schemaKeywords.array) {
188
+ const subItem = schema as SchemaKeywordMapper['array']
189
+
190
+ subItem.args.items.forEach((entrySchema) => {
191
+ if (!foundItem) {
192
+ foundItem = SchemaGenerator.find<T>([entrySchema], keyword)
193
+ }
194
+ })
195
+ }
196
+
197
+ if (schema.keyword === schemaKeywords.and) {
198
+ const subItem = schema as SchemaKeywordMapper['and']
199
+
200
+ subItem.args.forEach((entrySchema) => {
201
+ if (!foundItem) {
202
+ foundItem = SchemaGenerator.find<T>([entrySchema], keyword)
203
+ }
204
+ })
205
+ }
206
+
207
+ if (schema.keyword === schemaKeywords.tuple) {
208
+ const subItem = schema as SchemaKeywordMapper['tuple']
209
+
210
+ subItem.args.forEach((entrySchema) => {
211
+ if (!foundItem) {
212
+ foundItem = SchemaGenerator.find<T>([entrySchema], keyword)
213
+ }
214
+ })
215
+ }
216
+
217
+ if (schema.keyword === schemaKeywords.union) {
218
+ const subItem = schema as SchemaKeywordMapper['union']
219
+
220
+ subItem.args.forEach((entrySchema) => {
221
+ if (!foundItem) {
222
+ foundItem = SchemaGenerator.find<T>([entrySchema], keyword)
223
+ }
224
+ })
225
+ }
226
+ })
227
+
228
+ return foundItem
229
+ }
230
+
231
+ #getUsedEnumNames(props: SchemaProps) {
232
+ const options = this.#getOptions(props)
233
+
234
+ return options.usedEnumNames || {}
235
+ }
236
+
237
+ #getOptions({ name }: SchemaProps): Partial<TOptions> {
238
+ const { override = [] } = this.context
239
+
240
+ return {
241
+ ...this.options,
242
+ ...(override.find(({ pattern, type }) => {
243
+ if (name && type === 'schemaName') {
244
+ return !!name.match(pattern)
245
+ }
246
+
247
+ return false
248
+ })?.options || {}),
249
+ }
250
+ }
251
+
252
+ #getUnknownReturn(props: SchemaProps) {
253
+ const options = this.#getOptions(props)
254
+
255
+ if (options.unknownType === 'any') {
256
+ return schemaKeywords.any
257
+ }
258
+
259
+ return schemaKeywords.unknown
260
+ }
261
+
262
+ /**
263
+ * Recursively creates a type literal with the given props.
264
+ */
265
+ #parseProperties({ schema, name }: SchemaProps): Schema[] {
266
+ const properties = schema?.properties || {}
267
+ const additionalProperties = schema?.additionalProperties
268
+ const required = schema?.required
269
+
270
+ const propertiesSchemas = Object.keys(properties)
271
+ .map((propertyName) => {
272
+ const validationFunctions: Schema[] = []
273
+ const propertySchema = properties[propertyName] as SchemaObject
274
+
275
+ const isRequired = Array.isArray(required) ? required?.includes(propertyName) : !!required
276
+ const nullable = propertySchema.nullable ?? propertySchema['x-nullable'] ?? false
277
+
278
+ validationFunctions.push(...this.buildSchemas({ schema: propertySchema, name: propertyName, parentName: name }))
279
+
280
+ validationFunctions.push({
281
+ keyword: schemaKeywords.name,
282
+ args: propertyName,
283
+ })
284
+
285
+ if (!isRequired && nullable) {
286
+ validationFunctions.push({ keyword: schemaKeywords.nullish })
287
+ } else if (!isRequired) {
288
+ validationFunctions.push({ keyword: schemaKeywords.optional })
289
+ }
290
+
291
+ return {
292
+ [propertyName]: validationFunctions,
293
+ }
294
+ })
295
+ .reduce((acc, curr) => ({ ...acc, ...curr }), {})
296
+ let additionalPropertiesSchemas: Schema[] = []
297
+
298
+ if (additionalProperties) {
299
+ additionalPropertiesSchemas =
300
+ additionalProperties === true
301
+ ? [{ keyword: this.#getUnknownReturn({ schema, name }) }]
302
+ : this.buildSchemas({ schema: additionalProperties as SchemaObject, parentName: name })
303
+ }
304
+
305
+ return [
306
+ {
307
+ keyword: schemaKeywords.object,
308
+ args: {
309
+ properties: propertiesSchemas,
310
+ additionalProperties: additionalPropertiesSchemas,
311
+ },
312
+ },
313
+ ]
314
+ }
315
+
316
+ /**
317
+ * Create a type alias for the schema referenced by the given ReferenceObject
318
+ */
319
+ #getRefAlias(obj: OpenAPIV3.ReferenceObject): Schema[] {
320
+ const { $ref } = obj
321
+ let ref = this.refs[$ref]
322
+
323
+ const originalName = getUniqueName($ref.replace(/.+\//, ''), this.#usedAliasNames)
324
+ const propertyName = this.context.pluginManager.resolveName({
325
+ name: originalName,
326
+ pluginKey: this.context.plugin.key,
327
+ type: 'function',
328
+ })
329
+
330
+ if (ref) {
331
+ return [
332
+ {
333
+ keyword: schemaKeywords.ref,
334
+ args: { name: ref.propertyName, path: ref.path },
335
+ },
336
+ ]
337
+ }
338
+
339
+ const fileName = this.context.pluginManager.resolveName({
340
+ name: originalName,
341
+ pluginKey: this.context.plugin.key,
342
+ type: 'file',
343
+ })
344
+ const file = this.context.pluginManager.getFile({
345
+ name: fileName,
346
+ pluginKey: this.context.plugin.key,
347
+ extName: '.ts',
348
+ })
349
+
350
+ ref = this.refs[$ref] = {
351
+ propertyName,
352
+ originalName,
353
+ path: file.path,
354
+ }
355
+
356
+ return [
357
+ {
358
+ keyword: schemaKeywords.ref,
359
+ args: { name: ref.propertyName, path: ref?.path, isTypeOnly: false },
360
+ },
361
+ ]
362
+ }
363
+
364
+ #getParsedSchemaObject(schema?: SchemaObject) {
365
+ const parsedSchema = getSchemaFactory(this.context.oas)(schema)
366
+ return parsedSchema
367
+ }
368
+
369
+ /**
370
+ * This is the very core of the OpenAPI to TS conversion - it takes a
371
+ * schema and returns the appropriate type.
372
+ */
373
+ #parseSchemaObject({ schema: _schema, name, parentName }: SchemaProps): Schema[] {
374
+ const options = this.#getOptions({ schema: _schema, name })
375
+ const unknownReturn = this.#getUnknownReturn({ schema: _schema, name })
376
+ const { schema, version } = this.#getParsedSchemaObject(_schema)
377
+ const resolvedName = this.context.pluginManager.resolveName({
378
+ name: `${parentName || ''} ${name}`,
379
+ pluginKey: this.context.plugin.key,
380
+ type: 'type',
381
+ })
382
+
383
+ if (!schema) {
384
+ return [{ keyword: unknownReturn }]
385
+ }
386
+
387
+ const baseItems: Schema[] = [
388
+ {
389
+ keyword: schemaKeywords.schema,
390
+ args: {
391
+ type: schema.type as any,
392
+ format: schema.format,
393
+ },
394
+ },
395
+ ]
396
+ const min = schema.minimum ?? schema.minLength ?? schema.minItems ?? undefined
397
+ const max = schema.maximum ?? schema.maxLength ?? schema.maxItems ?? undefined
398
+ const nullable = schema.nullable ?? schema['x-nullable'] ?? false
399
+
400
+ if (schema.default !== undefined && !Array.isArray(schema.default)) {
401
+ if (typeof schema.default === 'string') {
402
+ baseItems.push({
403
+ keyword: schemaKeywords.default,
404
+ args: transformers.stringify(schema.default),
405
+ })
406
+ }
407
+ if (typeof schema.default === 'boolean') {
408
+ baseItems.push({
409
+ keyword: schemaKeywords.default,
410
+ args: schema.default ?? false,
411
+ })
412
+ }
413
+ }
414
+
415
+ if (schema.description) {
416
+ baseItems.push({
417
+ keyword: schemaKeywords.describe,
418
+ args: schema.description,
419
+ })
420
+ }
421
+
422
+ if (schema.pattern) {
423
+ baseItems.unshift({
424
+ keyword: schemaKeywords.matches,
425
+ args: schema.pattern,
426
+ })
427
+ }
428
+
429
+ if (max !== undefined) {
430
+ baseItems.unshift({ keyword: schemaKeywords.max, args: max })
431
+ }
432
+
433
+ if (min !== undefined) {
434
+ baseItems.unshift({ keyword: schemaKeywords.min, args: min })
435
+ }
436
+
437
+ if (nullable) {
438
+ baseItems.push({ keyword: schemaKeywords.nullable })
439
+ }
440
+
441
+ if (schema.type && Array.isArray(schema.type)) {
442
+ const [_schema, nullable] = schema.type
443
+
444
+ if (nullable === 'null') {
445
+ baseItems.push({ keyword: schemaKeywords.nullable })
446
+ }
447
+ }
448
+
449
+ if (schema.readOnly) {
450
+ baseItems.push({ keyword: schemaKeywords.readOnly })
451
+ }
452
+
453
+ if (isReference(schema)) {
454
+ return [...this.#getRefAlias(schema), ...baseItems]
455
+ }
456
+
457
+ if (schema.oneOf) {
458
+ // union
459
+ const schemaWithoutOneOf = { ...schema, oneOf: undefined }
460
+
461
+ const union: Schema = {
462
+ keyword: schemaKeywords.union,
463
+ args: schema.oneOf
464
+ .map((item) => {
465
+ return item && this.buildSchemas({ schema: item as SchemaObject, name, parentName })[0]
466
+ })
467
+ .filter(Boolean)
468
+ .filter((item) => {
469
+ return item && item.keyword !== unknownReturn
470
+ }),
471
+ }
472
+ if (schemaWithoutOneOf.properties) {
473
+ return [...this.buildSchemas({ schema: schemaWithoutOneOf, name, parentName }), union, ...baseItems]
474
+ }
475
+
476
+ return [union, ...baseItems]
477
+ }
478
+
479
+ if (schema.anyOf) {
480
+ // union
481
+ const schemaWithoutAnyOf = { ...schema, anyOf: undefined }
482
+
483
+ const union: Schema = {
484
+ keyword: schemaKeywords.union,
485
+ args: schema.anyOf
486
+ .map((item) => {
487
+ return item && this.buildSchemas({ schema: item as SchemaObject, name, parentName })[0]
488
+ })
489
+ .filter(Boolean)
490
+ .filter((item) => {
491
+ return item && item.keyword !== unknownReturn
492
+ })
493
+ .map((item) => {
494
+ if (isKeyword(item, schemaKeywords.object)) {
495
+ return {
496
+ ...item,
497
+ args: {
498
+ ...item.args,
499
+ strict: true,
500
+ },
501
+ }
502
+ }
503
+ return item
504
+ }),
505
+ }
506
+ if (schemaWithoutAnyOf.properties) {
507
+ return [...this.buildSchemas({ schema: schemaWithoutAnyOf, name, parentName }), union, ...baseItems]
508
+ }
509
+
510
+ return [union, ...baseItems]
511
+ }
512
+ if (schema.allOf) {
513
+ // intersection/add
514
+ const schemaWithoutAllOf = { ...schema, allOf: undefined }
515
+
516
+ const and: Schema = {
517
+ keyword: schemaKeywords.and,
518
+ args: schema.allOf
519
+ .map((item) => {
520
+ return item && this.buildSchemas({ schema: item as SchemaObject, name, parentName })[0]
521
+ })
522
+ .filter(Boolean)
523
+ .filter((item) => {
524
+ return item && item.keyword !== unknownReturn
525
+ }),
526
+ }
527
+
528
+ if (schemaWithoutAllOf.properties) {
529
+ return [
530
+ {
531
+ ...and,
532
+ args: [...(and.args || []), ...this.buildSchemas({ schema: schemaWithoutAllOf, name, parentName })],
533
+ },
534
+ ...baseItems,
535
+ ]
536
+ }
537
+
538
+ return [and, ...baseItems]
539
+ }
540
+
541
+ if (schema.enum) {
542
+ const enumName = getUniqueName(pascalCase([parentName, name, options.enumSuffix].join(' ')), this.#getUsedEnumNames({ schema, name }))
543
+ const typeName = this.context.pluginManager.resolveName({
544
+ name: enumName,
545
+ pluginKey: this.context.plugin.key,
546
+ type: 'type',
547
+ })
548
+
549
+ // x-enumNames has priority
550
+ const extensionEnums = ['x-enumNames', 'x-enum-varnames']
551
+ .filter((extensionKey) => extensionKey in schema)
552
+ .map((extensionKey) => {
553
+ return [
554
+ {
555
+ keyword: schemaKeywords.enum,
556
+ args: {
557
+ name,
558
+ typeName,
559
+ asConst: false,
560
+ items: [...new Set(schema[extensionKey as keyof typeof schema] as string[])].map((name: string | number, index) => ({
561
+ name: transformers.stringify(name),
562
+ value: schema.enum?.[index] as string | number,
563
+ format: isNumber(schema.enum?.[index]) ? 'number' : 'string',
564
+ })),
565
+ },
566
+ },
567
+ ...baseItems.filter(
568
+ (item) => item.keyword !== schemaKeywords.min && item.keyword !== schemaKeywords.max && item.keyword !== schemaKeywords.matches,
569
+ ),
570
+ ]
571
+ })
572
+
573
+ if (schema.type === 'number' || schema.type === 'integer') {
574
+ // we cannot use z.enum when enum type is number/integer
575
+ const enumNames = extensionEnums[0]?.find((item) => isKeyword(item, schemaKeywords.enum)) as SchemaKeywordMapper['enum']
576
+ return [
577
+ {
578
+ keyword: schemaKeywords.enum,
579
+ args: {
580
+ name: enumName,
581
+ typeName,
582
+ asConst: true,
583
+ items: enumNames?.args?.items
584
+ ? [...new Set(enumNames.args.items)].map(({ name, value }) => ({
585
+ name,
586
+ value,
587
+ format: 'number',
588
+ }))
589
+ : [...new Set(schema.enum)].map((value: string) => {
590
+ return {
591
+ name: value,
592
+ value,
593
+ format: 'number',
594
+ }
595
+ }),
596
+ },
597
+ },
598
+ ...baseItems.filter((item) => item.keyword !== schemaKeywords.min && item.keyword !== schemaKeywords.max && item.keyword !== schemaKeywords.matches),
599
+ ]
600
+ }
601
+
602
+ if (extensionEnums.length > 0 && extensionEnums[0]) {
603
+ return extensionEnums[0]
604
+ }
605
+
606
+ return [
607
+ {
608
+ keyword: schemaKeywords.enum,
609
+ args: {
610
+ name: enumName,
611
+ typeName,
612
+ asConst: false,
613
+ items: [...new Set(schema.enum)].map((value: string) => ({
614
+ name: transformers.stringify(value),
615
+ value,
616
+ format: isNumber(value) ? 'number' : 'string',
617
+ })),
618
+ },
619
+ },
620
+ ...baseItems.filter((item) => item.keyword !== schemaKeywords.min && item.keyword !== schemaKeywords.max && item.keyword !== schemaKeywords.matches),
621
+ ]
622
+ }
623
+
624
+ if ('prefixItems' in schema) {
625
+ const prefixItems = schema.prefixItems as SchemaObject[]
626
+
627
+ return [
628
+ {
629
+ keyword: schemaKeywords.tuple,
630
+ args: prefixItems
631
+ .map((item) => {
632
+ return this.buildSchemas({ schema: item, name, parentName })[0]
633
+ })
634
+ .filter(Boolean),
635
+ },
636
+ ...baseItems,
637
+ ]
638
+ }
639
+
640
+ if (version === '3.1' && 'const' in schema) {
641
+ // const keyword takes precendence over the actual type.
642
+ if (schema['const']) {
643
+ return [
644
+ {
645
+ keyword: schemaKeywords.const,
646
+ args: {
647
+ name: schema['const'],
648
+ format: typeof schema['const'] === 'number' ? 'number' : 'string',
649
+ value: schema['const'],
650
+ },
651
+ },
652
+ ...baseItems,
653
+ ]
654
+ }
655
+ return [{ keyword: schemaKeywords.null }]
656
+ }
657
+
658
+ /**
659
+ * > Structural validation alone may be insufficient to allow an application to correctly utilize certain values. The "format"
660
+ * > annotation keyword is defined to allow schema authors to convey semantic information for a fixed subset of values which are
661
+ * > accurately described by authoritative resources, be they RFCs or other external specifications.
662
+ *
663
+ * In other words: format is more specific than type alone, hence it should override the type value, if possible.
664
+ *
665
+ * see also https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
666
+ */
667
+ if (schema.format) {
668
+ switch (schema.format) {
669
+ case 'binary':
670
+ baseItems.push({ keyword: schemaKeywords.blob })
671
+ return baseItems
672
+ case 'date-time':
673
+ if (options.dateType) {
674
+ if (options.dateType === 'date') {
675
+ baseItems.unshift({ keyword: schemaKeywords.date, args: { type: 'date' } })
676
+
677
+ return baseItems
678
+ }
679
+
680
+ if (options.dateType === 'stringOffset') {
681
+ baseItems.unshift({ keyword: schemaKeywords.datetime, args: { offset: true } })
682
+ return baseItems
683
+ }
684
+
685
+ if (options.dateType === 'stringLocal') {
686
+ baseItems.unshift({ keyword: schemaKeywords.datetime, args: { local: true } })
687
+ return baseItems
688
+ }
689
+
690
+ baseItems.unshift({ keyword: schemaKeywords.datetime, args: { offset: false } })
691
+
692
+ return baseItems
693
+ }
694
+ break
695
+ case 'date':
696
+ if (options.dateType) {
697
+ if (options.dateType === 'date') {
698
+ baseItems.unshift({ keyword: schemaKeywords.date, args: { type: 'date' } })
699
+
700
+ return baseItems
701
+ }
702
+
703
+ baseItems.unshift({ keyword: schemaKeywords.date, args: { type: 'string' } })
704
+
705
+ return baseItems
706
+ }
707
+ break
708
+ case 'time':
709
+ if (options.dateType) {
710
+ if (options.dateType === 'date') {
711
+ baseItems.unshift({ keyword: schemaKeywords.time, args: { type: 'date' } })
712
+
713
+ return baseItems
714
+ }
715
+
716
+ baseItems.unshift({ keyword: schemaKeywords.time, args: { type: 'string' } })
717
+
718
+ return baseItems
719
+ }
720
+ break
721
+ case 'uuid':
722
+ baseItems.unshift({ keyword: schemaKeywords.uuid })
723
+ break
724
+ case 'email':
725
+ case 'idn-email':
726
+ baseItems.unshift({ keyword: schemaKeywords.email })
727
+ break
728
+ case 'uri':
729
+ case 'ipv4':
730
+ case 'ipv6':
731
+ case 'uri-reference':
732
+ case 'hostname':
733
+ case 'idn-hostname':
734
+ baseItems.unshift({ keyword: schemaKeywords.url })
735
+ break
736
+ // case 'duration':
737
+ // case 'json-pointer':
738
+ // case 'relative-json-pointer':
739
+ default:
740
+ // formats not yet implemented: ignore.
741
+ break
742
+ }
743
+ }
744
+
745
+ // type based logic
746
+ if ('items' in schema || schema.type === ('array' as 'string')) {
747
+ const min = schema.minimum ?? schema.minLength ?? schema.minItems ?? undefined
748
+ const max = schema.maximum ?? schema.maxLength ?? schema.maxItems ?? undefined
749
+ const items = this.buildSchemas({ schema: 'items' in schema ? (schema.items as SchemaObject) : [], name, parentName })
750
+
751
+ return [
752
+ {
753
+ keyword: schemaKeywords.array,
754
+ args: {
755
+ items,
756
+ min,
757
+ max,
758
+ },
759
+ },
760
+ ...baseItems.filter((item) => item.keyword !== schemaKeywords.min && item.keyword !== schemaKeywords.max),
761
+ ]
762
+ }
763
+
764
+ if (schema.properties || schema.additionalProperties) {
765
+ return [...this.#parseProperties({ schema, name }), ...baseItems]
766
+ }
767
+
768
+ if (schema.type) {
769
+ if (Array.isArray(schema.type)) {
770
+ // OPENAPI v3.1.0: https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0
771
+ const [type] = schema.type as Array<OpenAPIV3.NonArraySchemaObjectType>
772
+
773
+ return [
774
+ ...this.buildSchemas({
775
+ schema: {
776
+ ...schema,
777
+ type,
778
+ },
779
+ name,
780
+ parentName,
781
+ }),
782
+ ...baseItems,
783
+ ].filter(Boolean)
784
+ }
785
+
786
+ // 'string' | 'number' | 'integer' | 'boolean'
787
+ return [{ keyword: schema.type }, ...baseItems]
788
+ }
789
+
790
+ return [{ keyword: unknownReturn }]
791
+ }
792
+
793
+ async build(): Promise<Array<KubbFile.File<TFileMeta>>> {
794
+ const { oas, contentType, include } = this.context
795
+
796
+ const schemas = getSchemas({ oas, contentType, includes: include })
797
+
798
+ const promises = Object.entries(schemas).reduce((acc, [name, schema]) => {
799
+ const promiseOperation = this.schema.call(this, name, schema)
800
+
801
+ if (promiseOperation) {
802
+ acc.push(promiseOperation)
803
+ }
804
+
805
+ return acc
806
+ }, [] as SchemaMethodResult<TFileMeta>[])
807
+
808
+ const files = await Promise.all(promises)
809
+
810
+ // using .flat because schemaGenerator[method] can return a array of files or just one file
811
+ return files.flat().filter(Boolean)
812
+ }
813
+
814
+ /**
815
+ * Schema
816
+ */
817
+ abstract schema(name: string, object: SchemaObject): SchemaMethodResult<TFileMeta>
818
+ /**
819
+ * Returns the source, in the future it will return a React component
820
+ */
821
+ abstract getSource<TOptions extends SchemaGeneratorBuildOptions = SchemaGeneratorBuildOptions>(name: string, schemas: Schema[], options?: TOptions): string[]
822
+
823
+ /**
824
+ * @deprecated only used for testing
825
+ */
826
+ abstract buildSource(name: string, object: SchemaObject | undefined, options?: SchemaGeneratorBuildOptions): string[]
827
+ }