@kubb/swagger-ts 2.10.0 → 2.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,415 +0,0 @@
1
- import { Generator } from '@kubb/core'
2
- import transformers, { pascalCase } from '@kubb/core/transformers'
3
- import { getUniqueName } from '@kubb/core/utils'
4
- import * as factory from '@kubb/parser/factory'
5
- import { keywordTypeNodes } from '@kubb/parser/factory'
6
- import { getSchemaFactory, isReference } from '@kubb/swagger/utils'
7
-
8
- import { pluginKey } from './plugin.ts'
9
-
10
- import type { PluginManager } from '@kubb/core'
11
- import type { ts } from '@kubb/parser'
12
- import type { ImportMeta, Refs } from '@kubb/swagger'
13
- import type { Oas, OpenAPIV3, OpenAPIV3_1, SchemaObject } from '@kubb/swagger/oas'
14
- import type { PluginOptions } from './types.ts'
15
-
16
- // based on https://github.com/cellular/oazapfts/blob/7ba226ebb15374e8483cc53e7532f1663179a22c/src/codegen/generate.ts#L398
17
-
18
- type Context = {
19
- oas: Oas
20
- pluginManager: PluginManager
21
- }
22
-
23
- export class TypeGenerator extends Generator<PluginOptions['resolvedOptions'], Context> {
24
- refs: Refs = {}
25
- imports: ImportMeta[] = []
26
-
27
- extraNodes: ts.Node[] = []
28
-
29
- aliases: ts.TypeAliasDeclaration[] = []
30
-
31
- // Keep track of already used type aliases
32
- #usedAliasNames: Record<string, number> = {}
33
-
34
- build({
35
- schema,
36
- optional,
37
- baseName,
38
- description,
39
- keysToOmit,
40
- }: {
41
- schema: SchemaObject
42
- baseName: string
43
- description?: string
44
- optional?: boolean
45
- keysToOmit?: string[]
46
- }): ts.Node[] {
47
- const nodes: ts.Node[] = []
48
- let type = this.getTypeFromSchema(schema, baseName)
49
-
50
- if (!type) {
51
- return this.extraNodes
52
- }
53
-
54
- if (optional) {
55
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] }) as ts.TypeNode
56
- }
57
-
58
- const node = factory.createTypeAliasDeclaration({
59
- modifiers: [factory.modifiers.export],
60
- name: this.context.pluginManager.resolveName({ name: baseName, pluginKey, type: 'type' }),
61
- type: keysToOmit?.length ? factory.createOmitDeclaration({ keys: keysToOmit, type, nonNullable: true }) : type,
62
- })
63
-
64
- if (description) {
65
- nodes.push(
66
- factory.appendJSDocToNode({
67
- node,
68
- comments: [`@description ${transformers.trim(description)}`],
69
- }),
70
- )
71
- } else {
72
- nodes.push(node)
73
- }
74
-
75
- // filter out if the export name is the same as one that we already defined in extraNodes(see enum)
76
- const filterdNodes = nodes.filter(
77
- (node: ts.Node) =>
78
- !this.extraNodes.some(
79
- (extraNode: ts.Node) => (extraNode as ts.TypeAliasDeclaration)?.name?.escapedText === (node as ts.TypeAliasDeclaration)?.name?.escapedText,
80
- ),
81
- )
82
-
83
- return [...this.extraNodes, ...filterdNodes]
84
- }
85
-
86
- /**
87
- * Creates a type node from a given schema.
88
- * Delegates to getBaseTypeFromSchema internally and
89
- * optionally adds a union with null.
90
- */
91
- getTypeFromSchema(schema?: SchemaObject, name?: string): ts.TypeNode | null {
92
- const type = this.#getBaseTypeFromSchema(schema, name)
93
-
94
- if (!type) {
95
- return null
96
- }
97
-
98
- const nullable = (schema?.nullable ?? schema?.['x-nullable']) ?? false
99
-
100
- if (nullable) {
101
- return factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] })
102
- }
103
-
104
- return type
105
- }
106
-
107
- /**
108
- * Recursively creates a type literal with the given props.
109
- */
110
- #getTypeFromProperties(baseSchema?: SchemaObject, baseName?: string): ts.TypeNode | null {
111
- const { optionalType } = this.options
112
- const properties = baseSchema?.properties || {}
113
- const required = baseSchema?.required
114
- const additionalProperties = baseSchema?.additionalProperties
115
-
116
- const members: Array<ts.TypeElement | null> = Object.keys(properties).map((name) => {
117
- const schema = properties[name] as SchemaObject
118
-
119
- const isRequired = Array.isArray(required) ? required.includes(name) : !!required
120
- let type = this.getTypeFromSchema(schema, this.context.pluginManager.resolveName({ name: `${baseName || ''} ${name}`, pluginKey, type: 'type' }))
121
-
122
- if (!type) {
123
- return null
124
- }
125
-
126
- if (!isRequired && ['undefined', 'questionTokenAndUndefined'].includes(optionalType as string)) {
127
- type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] })
128
- }
129
- const propertySignature = factory.createPropertySignature({
130
- questionToken: ['questionToken', 'questionTokenAndUndefined'].includes(optionalType as string) && !isRequired,
131
- name,
132
- type: type as ts.TypeNode,
133
- readOnly: schema.readOnly,
134
- })
135
-
136
- return factory.appendJSDocToNode({
137
- node: propertySignature,
138
- comments: [
139
- schema.description ? `@description ${schema.description}` : undefined,
140
- schema.type ? `@type ${schema.type?.toString()}${isRequired ? '' : ' | undefined'} ${schema.format || ''}` : undefined,
141
- schema.example ? `@example ${schema.example as string}` : undefined,
142
- schema.deprecated ? `@deprecated` : undefined,
143
- schema.default !== undefined && typeof schema.default === 'string' ? `@default '${schema.default}'` : undefined,
144
- schema.default !== undefined && typeof schema.default !== 'string' ? `@default ${schema.default as string}` : undefined,
145
- ].filter(Boolean),
146
- })
147
- })
148
- if (additionalProperties) {
149
- const type = additionalProperties === true ? this.#unknownReturn : this.getTypeFromSchema(additionalProperties as SchemaObject)
150
-
151
- if (type) {
152
- members.push(factory.createIndexSignature(type))
153
- }
154
- }
155
- return factory.createTypeLiteralNode(members.filter(Boolean))
156
- }
157
-
158
- /**
159
- * Create a type alias for the schema referenced by the given ReferenceObject
160
- */
161
- #getRefAlias(obj: OpenAPIV3.ReferenceObject, _baseName?: string) {
162
- const { $ref } = obj
163
- let ref = this.refs[$ref]
164
-
165
- if (ref) {
166
- return factory.createTypeReferenceNode(ref.propertyName, undefined)
167
- }
168
-
169
- const originalName = getUniqueName($ref.replace(/.+\//, ''), this.#usedAliasNames)
170
- const propertyName = this.context.pluginManager.resolveName({ name: originalName, pluginKey, type: 'type' })
171
-
172
- ref = this.refs[$ref] = {
173
- propertyName,
174
- originalName,
175
- }
176
-
177
- const fileName = this.context.pluginManager.resolveName({ name: originalName, pluginKey, type: 'file' })
178
- const path = this.context.pluginManager.resolvePath({ baseName: fileName, pluginKey })
179
-
180
- this.imports.push({
181
- ref,
182
- path: path || '',
183
- isTypeOnly: true,
184
- })
185
-
186
- return factory.createTypeReferenceNode(ref.propertyName, undefined)
187
- }
188
-
189
- #getParsedSchema(schema?: SchemaObject) {
190
- const parsedSchema = getSchemaFactory(this.context.oas)(schema)
191
- return parsedSchema
192
- }
193
-
194
- /**
195
- * This is the very core of the OpenAPI to TS conversion - it takes a
196
- * schema and returns the appropriate type.
197
- */
198
- #getBaseTypeFromSchema(
199
- _schema: SchemaObject | undefined,
200
- baseName?: string,
201
- ): ts.TypeNode | null {
202
- const { schema, version } = this.#getParsedSchema(_schema)
203
-
204
- if (!schema) {
205
- return this.#unknownReturn
206
- }
207
-
208
- if (isReference(schema)) {
209
- return this.#getRefAlias(schema, baseName)
210
- }
211
-
212
- if (schema.oneOf) {
213
- // union
214
- const schemaWithoutOneOf = { ...schema, oneOf: undefined } as SchemaObject
215
-
216
- const union = factory.createUnionDeclaration({
217
- withParentheses: true,
218
- nodes: schema.oneOf
219
- .map((item) => {
220
- return item && this.getTypeFromSchema(item as SchemaObject)
221
- })
222
- .filter((item) => {
223
- return item && item !== this.#unknownReturn
224
- }) as Array<ts.TypeNode>,
225
- })
226
-
227
- if (schemaWithoutOneOf.properties) {
228
- return factory.createIntersectionDeclaration({
229
- nodes: [this.getTypeFromSchema(schemaWithoutOneOf, baseName), union].filter(Boolean),
230
- })
231
- }
232
-
233
- return union
234
- }
235
-
236
- if (schema.anyOf) {
237
- const schemaWithoutAnyOf = { ...schema, anyOf: undefined } as SchemaObject
238
-
239
- const union = factory.createUnionDeclaration({
240
- withParentheses: true,
241
- nodes: schema.anyOf
242
- .map((item) => {
243
- return item && this.getTypeFromSchema(item as SchemaObject)
244
- })
245
- .filter((item) => {
246
- return item && item !== this.#unknownReturn
247
- }) as Array<ts.TypeNode>,
248
- })
249
-
250
- if (schemaWithoutAnyOf.properties) {
251
- return factory.createIntersectionDeclaration({
252
- nodes: [this.getTypeFromSchema(schemaWithoutAnyOf, baseName), union].filter(Boolean),
253
- })
254
- }
255
-
256
- return union
257
- }
258
- if (schema.allOf) {
259
- // intersection/add
260
- const schemaWithoutAllOf = { ...schema, allOf: undefined } as SchemaObject
261
-
262
- const and = factory.createIntersectionDeclaration({
263
- withParentheses: true,
264
- nodes: schema.allOf
265
- .map((item) => {
266
- return item && this.getTypeFromSchema(item as SchemaObject)
267
- })
268
- .filter((item) => {
269
- return item && item !== this.#unknownReturn
270
- }) as Array<ts.TypeNode>,
271
- })
272
-
273
- if (schemaWithoutAllOf.properties) {
274
- return factory.createIntersectionDeclaration({
275
- nodes: [this.getTypeFromSchema(schemaWithoutAllOf, baseName), and].filter(Boolean),
276
- })
277
- }
278
-
279
- return and
280
- }
281
-
282
- /**
283
- * Enum will be defined outside the baseType(hints the baseName check)
284
- */
285
- if (schema.enum && baseName) {
286
- const enumName = getUniqueName(pascalCase([baseName, this.options.enumSuffix].join(' ')), this.options.usedEnumNames)
287
-
288
- let enums: [key: string, value: string | number][] = [...new Set(schema.enum)].map((key) => [key, key])
289
-
290
- const extensionEnums: Array<typeof enums> = ['x-enumNames', 'x-enum-varnames']
291
- .filter(extensionKey => extensionKey in schema)
292
- .map((extensionKey) =>
293
- [...new Set(schema[extensionKey as keyof typeof schema] as string[])].map((key, index) => [key, schema.enum?.[index] as string] as const)
294
- )
295
-
296
- if (extensionEnums.length > 0 && extensionEnums[0]) {
297
- enums = extensionEnums[0]
298
- }
299
-
300
- this.extraNodes.push(
301
- ...factory.createEnumDeclaration({
302
- name: transformers.camelCase(enumName),
303
- typeName: this.context.pluginManager.resolveName({ name: enumName, pluginKey, type: 'type' }),
304
- enums,
305
- type: this.options.enumType,
306
- }),
307
- )
308
- return factory.createTypeReferenceNode(this.context.pluginManager.resolveName({ name: enumName, pluginKey, type: 'type' }), undefined)
309
- }
310
-
311
- if (schema.enum) {
312
- return factory.createUnionDeclaration({
313
- nodes: schema.enum.map((name) => {
314
- return factory.createLiteralTypeNode(typeof name === 'number' ? factory.createNumericLiteral(name) : factory.createStringLiteral(`${name}`))
315
- }) as unknown as Array<ts.TypeNode>,
316
- })
317
- }
318
-
319
- if ('items' in schema) {
320
- // items -> array
321
- const node = this.getTypeFromSchema(schema.items as SchemaObject, baseName)
322
- if (node) {
323
- return factory.createArrayTypeNode(node)
324
- }
325
- }
326
-
327
- /**
328
- * OpenAPI 3.1
329
- * @link https://json-schema.org/understanding-json-schema/reference/array.html#tuple-validation
330
- */
331
-
332
- if ('prefixItems' in schema) {
333
- const prefixItems = schema.prefixItems as SchemaObject[]
334
-
335
- return factory.createTupleDeclaration({
336
- nodes: prefixItems.map((item) => {
337
- // no baseType so we can fall back on an union when using enum
338
- return this.getTypeFromSchema(item, undefined)
339
- }) as Array<ts.TypeNode>,
340
- })
341
- }
342
-
343
- if (schema.properties || schema.additionalProperties) {
344
- // properties -> literal type
345
- return this.#getTypeFromProperties(schema as SchemaObject, baseName)
346
- }
347
-
348
- /**
349
- * validate "const" property as defined in JSON-Schema-Validation
350
- *
351
- * https://json-schema.org/draft/2020-12/json-schema-validation#name-const
352
- *
353
- * > 6.1.3. const
354
- * > The value of this keyword MAY be of any type, including null.
355
- * > Use of this keyword is functionally equivalent to an "enum" (Section 6.1.2) with a single value.
356
- * > An instance validates successfully against this keyword if its value is equal to the value of the keyword.
357
- */
358
- if (version === '3.1' && 'const' in schema) {
359
- // const keyword takes precendence over the actual type.
360
- if (schema['const']) {
361
- if (typeof schema['const'] === 'string') {
362
- return factory.createLiteralTypeNode(factory.createStringLiteral(schema['const']))
363
- } else if (typeof schema['const'] === 'number') {
364
- return factory.createLiteralTypeNode(factory.createNumericLiteral(schema['const']))
365
- }
366
- } else {
367
- return keywordTypeNodes.null
368
- }
369
- }
370
-
371
- if (schema.type) {
372
- if (Array.isArray(schema.type)) {
373
- // TODO remove hardcoded first type, second nullable
374
- // OPENAPI v3.1.0: https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0
375
- const [type, nullable] = schema.type as Array<OpenAPIV3_1.NonArraySchemaObjectType>
376
-
377
- return factory.createUnionDeclaration({
378
- nodes: [
379
- this.getTypeFromSchema(
380
- {
381
- ...schema,
382
- type,
383
- } as SchemaObject,
384
- baseName,
385
- ),
386
- nullable ? factory.createLiteralTypeNode(factory.createNull()) : undefined,
387
- ].filter(Boolean),
388
- })
389
- }
390
-
391
- if (this.options.dateType === 'date' && ['date', 'date-time'].some((item) => item === schema.format)) {
392
- return factory.createTypeReferenceNode(factory.createIdentifier('Date'))
393
- }
394
-
395
- // string, boolean, null, number
396
- if (schema.type in factory.keywordTypeNodes) {
397
- return factory.keywordTypeNodes[schema.type as keyof typeof factory.keywordTypeNodes]
398
- }
399
- }
400
-
401
- if (schema.format === 'binary') {
402
- return factory.createTypeReferenceNode('Blob', [])
403
- }
404
-
405
- return this.#unknownReturn
406
- }
407
-
408
- get #unknownReturn() {
409
- if (this.options.unknownType === 'any') {
410
- return factory.keywordTypeNodes.any
411
- }
412
-
413
- return factory.keywordTypeNodes.unknown
414
- }
415
- }
@@ -1,142 +0,0 @@
1
- import transformers from '@kubb/core/transformers'
2
- import { print } from '@kubb/parser'
3
- import * as factory from '@kubb/parser/factory'
4
- import { Editor, File, usePlugin, usePluginManager } from '@kubb/react'
5
- import { useOas, useOperation, useOperationFile, useOperationName, useSchemas } from '@kubb/swagger/hooks'
6
-
7
- import { TypeBuilder } from '../TypeBuilder.ts'
8
-
9
- import type { KubbFile } from '@kubb/core'
10
- import type { ts } from '@kubb/parser'
11
- import type { OperationSchemas } from '@kubb/swagger'
12
- import type { Operation } from '@kubb/swagger/oas'
13
- import type { ReactNode } from 'react'
14
- import type { FileMeta, PluginOptions } from '../types.ts'
15
-
16
- type Props = {
17
- builder: TypeBuilder
18
- }
19
-
20
- function printCombinedSchema(name: string, operation: Operation, schemas: OperationSchemas): string {
21
- const properties: Record<string, ts.TypeNode> = {
22
- 'response': factory.createTypeReferenceNode(
23
- factory.createIdentifier(schemas.response.name),
24
- undefined,
25
- ),
26
- }
27
-
28
- if (schemas.request) {
29
- properties['request'] = factory.createTypeReferenceNode(
30
- factory.createIdentifier(schemas.request.name),
31
- undefined,
32
- )
33
- }
34
-
35
- if (schemas.pathParams) {
36
- properties['pathParams'] = factory.createTypeReferenceNode(
37
- factory.createIdentifier(schemas.pathParams.name),
38
- undefined,
39
- )
40
- }
41
-
42
- if (schemas.queryParams) {
43
- properties['queryParams'] = factory.createTypeReferenceNode(
44
- factory.createIdentifier(schemas.queryParams.name),
45
- undefined,
46
- )
47
- }
48
-
49
- if (schemas.headerParams) {
50
- properties['headerParams'] = factory.createTypeReferenceNode(
51
- factory.createIdentifier(schemas.headerParams.name),
52
- undefined,
53
- )
54
- }
55
-
56
- if (schemas.errors) {
57
- properties['errors'] = factory.createUnionDeclaration({
58
- nodes: schemas.errors.map(error => {
59
- return factory.createTypeReferenceNode(
60
- factory.createIdentifier(error.name),
61
- undefined,
62
- )
63
- }),
64
- })!
65
- }
66
- const namespaceNode = factory.createTypeAliasDeclaration({
67
- name: operation.method === 'get' ? `${name}Query` : `${name}Mutation`,
68
- type: factory.createTypeLiteralNode(
69
- Object.keys(properties).map(key => {
70
- const type = properties[key]
71
- if (!type) {
72
- return undefined
73
- }
74
-
75
- return factory.createPropertySignature(
76
- {
77
- name: transformers.pascalCase(key),
78
- type,
79
- },
80
- )
81
- }).filter(Boolean),
82
- ),
83
- modifiers: [factory.modifiers.export],
84
- })
85
-
86
- return print(namespaceNode)
87
- }
88
-
89
- export function Mutation({
90
- builder,
91
- }: Props): ReactNode {
92
- const { source } = builder.build()
93
-
94
- return (
95
- <>
96
- {source}
97
- </>
98
- )
99
- }
100
-
101
- type FileProps = {
102
- mode: KubbFile.Mode
103
- }
104
-
105
- Mutation.File = function({ mode }: FileProps): ReactNode {
106
- const plugin = usePlugin<PluginOptions>()
107
-
108
- const schemas = useSchemas()
109
- const pluginManager = usePluginManager()
110
- const oas = useOas()
111
- const file = useOperationFile()
112
- const factoryName = useOperationName({ type: 'type' })
113
- const operation = useOperation()
114
-
115
- const builder = new TypeBuilder(plugin.options, { plugin, oas, pluginManager })
116
- .add(schemas.pathParams)
117
- .add(schemas.queryParams)
118
- .add(schemas.headerParams)
119
- .add(schemas.response)
120
- .add(schemas.request)
121
- .add(schemas.statusCodes)
122
-
123
- const { source, imports } = builder.build()
124
-
125
- return (
126
- <Editor language="typescript">
127
- <File<FileMeta>
128
- baseName={file.baseName}
129
- path={file.path}
130
- meta={file.meta}
131
- >
132
- {mode === 'directory' && imports.map((item, index) => {
133
- return <File.Import key={index} root={file.path} {...item} />
134
- })}
135
- <File.Source>
136
- {source}
137
- {printCombinedSchema(factoryName, operation, schemas)}
138
- </File.Source>
139
- </File>
140
- </Editor>
141
- )
142
- }