tangled
alpha
login
or
join now
mokkenstorm.dev
/
openapi-ts
0
fork
atom
fork of hey-api/openapi-ts because I need some additional things
0
fork
atom
overview
issues
pulls
pipelines
chore: revert context argument
Lubos
1 year ago
4a939d16
cebf3270
+721
-718
3 changed files
expand all
collapse all
unified
split
packages
openapi-ts
src
plugins
@hey-api
types
plugin.ts
@tanstack
query-core
plugin.ts
utils
types.ts
+719
-27
packages/openapi-ts/src/plugins/@hey-api/types/plugin.ts
···
1
1
import ts from 'typescript';
2
2
3
3
+
import type { Property } from '../../../compiler';
3
4
import { compiler } from '../../../compiler';
4
5
import type { IRContext } from '../../../ir/context';
5
6
import type {
···
11
12
} from '../../../ir/ir';
12
13
import { operationResponsesMap } from '../../../ir/operation';
13
14
import { deduplicateSchema } from '../../../ir/schema';
15
15
+
import { ensureValidTypeScriptJavaScriptIdentifier } from '../../../openApi';
16
16
+
import { escapeComment } from '../../../utils/escape';
17
17
+
import { irRef, isRefOpenApiComponent } from '../../../utils/ref';
14
18
import type { PluginHandler } from '../../types';
15
15
-
import { schemaToType, type SchemaToTypeOptions } from '../../utils/types';
16
19
import { operationIrRef } from '../services/plugin';
17
20
import type { Config } from './types';
18
21
19
19
-
export const typesId = 'types';
22
22
+
interface SchemaWithType<T extends Required<IRSchemaObject>['type']>
23
23
+
extends Omit<IRSchemaObject, 'type'> {
24
24
+
type: Extract<Required<IRSchemaObject>['type'], T>;
25
25
+
}
26
26
+
27
27
+
const typesId = 'types';
28
28
+
29
29
+
const digitsRegExp = /^\d+$/;
30
30
+
31
31
+
const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => {
32
32
+
const comments = [
33
33
+
schema.description && escapeComment(schema.description),
34
34
+
schema.deprecated && '@deprecated',
35
35
+
];
36
36
+
return comments;
37
37
+
};
38
38
+
39
39
+
const addJavaScriptEnum = ({
40
40
+
$ref,
41
41
+
context,
42
42
+
schema,
43
43
+
}: {
44
44
+
$ref: string;
45
45
+
context: IRContext;
46
46
+
schema: SchemaWithType<'enum'>;
47
47
+
}) => {
48
48
+
const identifier = context.file({ id: typesId })!.identifier({
49
49
+
$ref,
50
50
+
create: true,
51
51
+
namespace: 'value',
52
52
+
});
53
53
+
54
54
+
// TODO: parser - this is the old parser behavior where we would NOT
55
55
+
// print nested enum identifiers if they already exist. This is a
56
56
+
// blocker for referencing these identifiers within the file as
57
57
+
// we cannot guarantee just because they have a duplicate identifier,
58
58
+
// they have a duplicate value.
59
59
+
if (!identifier.created) {
60
60
+
return;
61
61
+
}
62
62
+
63
63
+
const enumObject = schemaToEnumObject({ schema });
64
64
+
65
65
+
const expression = compiler.objectExpression({
66
66
+
multiLine: true,
67
67
+
obj: enumObject.obj,
68
68
+
});
69
69
+
const node = compiler.constVariable({
70
70
+
assertion: 'const',
71
71
+
comment: parseSchemaJsDoc({ schema }),
72
72
+
exportConst: true,
73
73
+
expression,
74
74
+
name: identifier.name || '',
75
75
+
});
76
76
+
return node;
77
77
+
};
78
78
+
79
79
+
const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => {
80
80
+
const typeofItems: Array<
81
81
+
| 'string'
82
82
+
| 'number'
83
83
+
| 'bigint'
84
84
+
| 'boolean'
85
85
+
| 'symbol'
86
86
+
| 'undefined'
87
87
+
| 'object'
88
88
+
| 'function'
89
89
+
> = [];
90
90
+
91
91
+
const obj = (schema.items ?? []).map((item) => {
92
92
+
const typeOfItemConst = typeof item.const;
93
93
+
94
94
+
if (!typeofItems.includes(typeOfItemConst)) {
95
95
+
typeofItems.push(typeOfItemConst);
96
96
+
}
97
97
+
98
98
+
let key;
99
99
+
if (item.title) {
100
100
+
key = item.title;
101
101
+
} else if (typeOfItemConst === 'number') {
102
102
+
key = `_${item.const}`;
103
103
+
} else if (typeOfItemConst === 'boolean') {
104
104
+
const valid = typeOfItemConst ? 'true' : 'false';
105
105
+
key = valid.toLocaleUpperCase();
106
106
+
} else {
107
107
+
let valid = ensureValidTypeScriptJavaScriptIdentifier(
108
108
+
item.const as string,
109
109
+
);
110
110
+
if (!valid) {
111
111
+
// TODO: parser - abstract empty string handling
112
112
+
valid = 'empty_string';
113
113
+
}
114
114
+
key = valid.toLocaleUpperCase();
115
115
+
}
116
116
+
return {
117
117
+
comments: parseSchemaJsDoc({ schema: item }),
118
118
+
key,
119
119
+
value: item.const,
120
120
+
};
121
121
+
});
122
122
+
123
123
+
return {
124
124
+
obj,
125
125
+
typeofItems,
126
126
+
};
127
127
+
};
128
128
+
129
129
+
const addTypeEnum = ({
130
130
+
$ref,
131
131
+
context,
132
132
+
schema,
133
133
+
}: {
134
134
+
$ref: string;
135
135
+
context: IRContext;
136
136
+
schema: SchemaWithType<'enum'>;
137
137
+
}) => {
138
138
+
const identifier = context.file({ id: typesId })!.identifier({
139
139
+
$ref,
140
140
+
create: true,
141
141
+
namespace: 'type',
142
142
+
});
143
143
+
144
144
+
// TODO: parser - this is the old parser behavior where we would NOT
145
145
+
// print nested enum identifiers if they already exist. This is a
146
146
+
// blocker for referencing these identifiers within the file as
147
147
+
// we cannot guarantee just because they have a duplicate identifier,
148
148
+
// they have a duplicate value.
149
149
+
if (
150
150
+
!identifier.created &&
151
151
+
!isRefOpenApiComponent($ref) &&
152
152
+
context.config.plugins['@hey-api/types']?.enums !== 'typescript+namespace'
153
153
+
) {
154
154
+
return;
155
155
+
}
156
156
+
157
157
+
const node = compiler.typeAliasDeclaration({
158
158
+
comment: parseSchemaJsDoc({ schema }),
159
159
+
exportType: true,
160
160
+
name: identifier.name || '',
161
161
+
type: schemaToType({
162
162
+
context,
163
163
+
schema: {
164
164
+
...schema,
165
165
+
type: undefined,
166
166
+
},
167
167
+
}),
168
168
+
});
169
169
+
return node;
170
170
+
};
171
171
+
172
172
+
const addTypeScriptEnum = ({
173
173
+
$ref,
174
174
+
context,
175
175
+
schema,
176
176
+
}: {
177
177
+
$ref: string;
178
178
+
context: IRContext;
179
179
+
schema: SchemaWithType<'enum'>;
180
180
+
}) => {
181
181
+
const identifier = context.file({ id: typesId })!.identifier({
182
182
+
$ref,
183
183
+
create: true,
184
184
+
namespace: 'value',
185
185
+
});
186
186
+
187
187
+
// TODO: parser - this is the old parser behavior where we would NOT
188
188
+
// print nested enum identifiers if they already exist. This is a
189
189
+
// blocker for referencing these identifiers within the file as
190
190
+
// we cannot guarantee just because they have a duplicate identifier,
191
191
+
// they have a duplicate value.
192
192
+
if (
193
193
+
!identifier.created &&
194
194
+
context.config.plugins['@hey-api/types']?.enums !== 'typescript+namespace'
195
195
+
) {
196
196
+
return;
197
197
+
}
198
198
+
199
199
+
const enumObject = schemaToEnumObject({ schema });
200
200
+
201
201
+
// TypeScript enums support only string and number values so we need to fallback to types
202
202
+
if (
203
203
+
enumObject.typeofItems.filter(
204
204
+
(type) => type !== 'number' && type !== 'string',
205
205
+
).length
206
206
+
) {
207
207
+
const node = addTypeEnum({
208
208
+
$ref,
209
209
+
context,
210
210
+
schema,
211
211
+
});
212
212
+
return node;
213
213
+
}
214
214
+
215
215
+
const node = compiler.enumDeclaration({
216
216
+
leadingComment: parseSchemaJsDoc({ schema }),
217
217
+
name: identifier.name || '',
218
218
+
obj: enumObject.obj,
219
219
+
});
220
220
+
return node;
221
221
+
};
222
222
+
223
223
+
const arrayTypeToIdentifier = ({
224
224
+
context,
225
225
+
namespace,
226
226
+
schema,
227
227
+
}: {
228
228
+
context: IRContext;
229
229
+
namespace: Array<ts.Statement>;
230
230
+
schema: SchemaWithType<'array'>;
231
231
+
}) => {
232
232
+
if (!schema.items) {
233
233
+
return compiler.typeArrayNode(
234
234
+
compiler.keywordTypeNode({
235
235
+
keyword: 'unknown',
236
236
+
}),
237
237
+
);
238
238
+
}
239
239
+
240
240
+
schema = deduplicateSchema({ schema });
241
241
+
242
242
+
// at least one item is guaranteed
243
243
+
const itemTypes = schema.items!.map((item) =>
244
244
+
schemaToType({
245
245
+
context,
246
246
+
namespace,
247
247
+
schema: item,
248
248
+
}),
249
249
+
);
250
250
+
251
251
+
if (itemTypes.length === 1) {
252
252
+
return compiler.typeArrayNode(itemTypes[0]);
253
253
+
}
254
254
+
255
255
+
if (schema.logicalOperator === 'and') {
256
256
+
return compiler.typeArrayNode(
257
257
+
compiler.typeIntersectionNode({ types: itemTypes }),
258
258
+
);
259
259
+
}
260
260
+
261
261
+
return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes }));
262
262
+
};
263
263
+
264
264
+
const booleanTypeToIdentifier = ({
265
265
+
schema,
266
266
+
}: {
267
267
+
context: IRContext;
268
268
+
namespace: Array<ts.Statement>;
269
269
+
schema: SchemaWithType<'boolean'>;
270
270
+
}) => {
271
271
+
if (schema.const !== undefined) {
272
272
+
return compiler.literalTypeNode({
273
273
+
literal: compiler.ots.boolean(schema.const as boolean),
274
274
+
});
275
275
+
}
276
276
+
277
277
+
return compiler.keywordTypeNode({
278
278
+
keyword: 'boolean',
279
279
+
});
280
280
+
};
281
281
+
282
282
+
const enumTypeToIdentifier = ({
283
283
+
$ref,
284
284
+
context,
285
285
+
namespace,
286
286
+
schema,
287
287
+
}: {
288
288
+
$ref?: string;
289
289
+
context: IRContext;
290
290
+
namespace: Array<ts.Statement>;
291
291
+
schema: SchemaWithType<'enum'>;
292
292
+
}): ts.TypeNode => {
293
293
+
// TODO: parser - add option to inline enums
294
294
+
if ($ref) {
295
295
+
const isRefComponent = isRefOpenApiComponent($ref);
296
296
+
297
297
+
// when enums are disabled (default), emit only reusable components
298
298
+
// as types, otherwise the output would be broken if we skipped all enums
299
299
+
if (!context.config.plugins['@hey-api/types']?.enums && isRefComponent) {
300
300
+
const typeNode = addTypeEnum({
301
301
+
$ref,
302
302
+
context,
303
303
+
schema,
304
304
+
});
305
305
+
if (typeNode) {
306
306
+
context.file({ id: typesId })!.add(typeNode);
307
307
+
}
308
308
+
}
309
309
+
310
310
+
if (context.config.plugins['@hey-api/types']?.enums === 'javascript') {
311
311
+
const typeNode = addTypeEnum({
312
312
+
$ref,
313
313
+
context,
314
314
+
schema,
315
315
+
});
316
316
+
if (typeNode) {
317
317
+
context.file({ id: typesId })!.add(typeNode);
318
318
+
}
319
319
+
320
320
+
const objectNode = addJavaScriptEnum({
321
321
+
$ref,
322
322
+
context,
323
323
+
schema,
324
324
+
});
325
325
+
if (objectNode) {
326
326
+
context.file({ id: typesId })!.add(objectNode);
327
327
+
}
328
328
+
}
329
329
+
330
330
+
if (context.config.plugins['@hey-api/types']?.enums === 'typescript') {
331
331
+
const enumNode = addTypeScriptEnum({
332
332
+
$ref,
333
333
+
context,
334
334
+
schema,
335
335
+
});
336
336
+
if (enumNode) {
337
337
+
context.file({ id: typesId })!.add(enumNode);
338
338
+
}
339
339
+
}
340
340
+
341
341
+
if (
342
342
+
context.config.plugins['@hey-api/types']?.enums === 'typescript+namespace'
343
343
+
) {
344
344
+
const enumNode = addTypeScriptEnum({
345
345
+
$ref,
346
346
+
context,
347
347
+
schema,
348
348
+
});
349
349
+
if (enumNode) {
350
350
+
if (isRefComponent) {
351
351
+
context.file({ id: typesId })!.add(enumNode);
352
352
+
} else {
353
353
+
// emit enum inside TypeScript namespace
354
354
+
namespace.push(enumNode);
355
355
+
}
356
356
+
}
357
357
+
}
358
358
+
}
359
359
+
360
360
+
const type = schemaToType({
361
361
+
context,
362
362
+
schema: {
363
363
+
...schema,
364
364
+
type: undefined,
365
365
+
},
366
366
+
});
367
367
+
return type;
368
368
+
};
369
369
+
370
370
+
const numberTypeToIdentifier = ({
371
371
+
schema,
372
372
+
}: {
373
373
+
context: IRContext;
374
374
+
namespace: Array<ts.Statement>;
375
375
+
schema: SchemaWithType<'number'>;
376
376
+
}) => {
377
377
+
if (schema.const !== undefined) {
378
378
+
return compiler.literalTypeNode({
379
379
+
literal: compiler.ots.number(schema.const as number),
380
380
+
});
381
381
+
}
382
382
+
383
383
+
return compiler.keywordTypeNode({
384
384
+
keyword: 'number',
385
385
+
});
386
386
+
};
387
387
+
388
388
+
const objectTypeToIdentifier = ({
389
389
+
context,
390
390
+
namespace,
391
391
+
schema,
392
392
+
}: {
393
393
+
context: IRContext;
394
394
+
namespace: Array<ts.Statement>;
395
395
+
schema: SchemaWithType<'object'>;
396
396
+
}) => {
397
397
+
let indexProperty: Property | undefined;
398
398
+
const schemaProperties: Array<Property> = [];
399
399
+
let indexPropertyItems: Array<IRSchemaObject> = [];
400
400
+
const required = schema.required ?? [];
401
401
+
let hasOptionalProperties = false;
402
402
+
403
403
+
for (const name in schema.properties) {
404
404
+
const property = schema.properties[name];
405
405
+
const isRequired = required.includes(name);
406
406
+
digitsRegExp.lastIndex = 0;
407
407
+
schemaProperties.push({
408
408
+
comment: parseSchemaJsDoc({ schema: property }),
409
409
+
isReadOnly: property.accessScope === 'read',
410
410
+
isRequired,
411
411
+
name: digitsRegExp.test(name)
412
412
+
? ts.factory.createNumericLiteral(name)
413
413
+
: name,
414
414
+
type: schemaToType({
415
415
+
$ref: `${irRef}${name}`,
416
416
+
context,
417
417
+
namespace,
418
418
+
schema: property,
419
419
+
}),
420
420
+
});
421
421
+
indexPropertyItems.push(property);
422
422
+
423
423
+
if (!isRequired) {
424
424
+
hasOptionalProperties = true;
425
425
+
}
426
426
+
}
427
427
+
428
428
+
if (
429
429
+
schema.additionalProperties &&
430
430
+
(schema.additionalProperties.type !== 'never' || !indexPropertyItems.length)
431
431
+
) {
432
432
+
if (schema.additionalProperties.type === 'never') {
433
433
+
indexPropertyItems = [schema.additionalProperties];
434
434
+
} else {
435
435
+
indexPropertyItems.unshift(schema.additionalProperties);
436
436
+
}
437
437
+
438
438
+
if (hasOptionalProperties) {
439
439
+
indexPropertyItems.push({
440
440
+
type: 'undefined',
441
441
+
});
442
442
+
}
443
443
+
444
444
+
indexProperty = {
445
445
+
isRequired: true,
446
446
+
name: 'key',
447
447
+
type: schemaToType({
448
448
+
context,
449
449
+
namespace,
450
450
+
schema:
451
451
+
indexPropertyItems.length === 1
452
452
+
? indexPropertyItems[0]
453
453
+
: {
454
454
+
items: indexPropertyItems,
455
455
+
logicalOperator: 'or',
456
456
+
},
457
457
+
}),
458
458
+
};
459
459
+
}
460
460
+
461
461
+
return compiler.typeInterfaceNode({
462
462
+
indexProperty,
463
463
+
properties: schemaProperties,
464
464
+
useLegacyResolution: false,
465
465
+
});
466
466
+
};
467
467
+
468
468
+
const stringTypeToIdentifier = ({
469
469
+
context,
470
470
+
schema,
471
471
+
}: {
472
472
+
context: IRContext;
473
473
+
namespace: Array<ts.Statement>;
474
474
+
schema: SchemaWithType<'string'>;
475
475
+
}) => {
476
476
+
if (schema.const !== undefined) {
477
477
+
return compiler.literalTypeNode({
478
478
+
literal: compiler.stringLiteral({ text: schema.const as string }),
479
479
+
});
480
480
+
}
481
481
+
482
482
+
if (schema.format) {
483
483
+
if (schema.format === 'binary') {
484
484
+
return compiler.typeUnionNode({
485
485
+
types: [
486
486
+
compiler.typeReferenceNode({
487
487
+
typeName: 'Blob',
488
488
+
}),
489
489
+
compiler.typeReferenceNode({
490
490
+
typeName: 'File',
491
491
+
}),
492
492
+
],
493
493
+
});
494
494
+
}
495
495
+
496
496
+
if (schema.format === 'date-time' || schema.format === 'date') {
497
497
+
// TODO: parser - add ability to skip type transformers
498
498
+
if (context.config.plugins['@hey-api/transformers']?.dates) {
499
499
+
return compiler.typeReferenceNode({ typeName: 'Date' });
500
500
+
}
501
501
+
}
502
502
+
}
503
503
+
504
504
+
return compiler.keywordTypeNode({
505
505
+
keyword: 'string',
506
506
+
});
507
507
+
};
508
508
+
509
509
+
const tupleTypeToIdentifier = ({
510
510
+
context,
511
511
+
namespace,
512
512
+
schema,
513
513
+
}: {
514
514
+
context: IRContext;
515
515
+
namespace: Array<ts.Statement>;
516
516
+
schema: SchemaWithType<'tuple'>;
517
517
+
}) => {
518
518
+
const itemTypes: Array<ts.TypeNode> = [];
519
519
+
520
520
+
for (const item of schema.items ?? []) {
521
521
+
itemTypes.push(
522
522
+
schemaToType({
523
523
+
context,
524
524
+
namespace,
525
525
+
schema: item,
526
526
+
}),
527
527
+
);
528
528
+
}
529
529
+
530
530
+
return compiler.typeTupleNode({
531
531
+
types: itemTypes,
532
532
+
});
533
533
+
};
534
534
+
535
535
+
const schemaTypeToIdentifier = ({
536
536
+
$ref,
537
537
+
context,
538
538
+
namespace,
539
539
+
schema,
540
540
+
}: {
541
541
+
$ref?: string;
542
542
+
context: IRContext;
543
543
+
namespace: Array<ts.Statement>;
544
544
+
schema: IRSchemaObject;
545
545
+
}): ts.TypeNode => {
546
546
+
switch (schema.type as Required<IRSchemaObject>['type']) {
547
547
+
case 'array':
548
548
+
return arrayTypeToIdentifier({
549
549
+
context,
550
550
+
namespace,
551
551
+
schema: schema as SchemaWithType<'array'>,
552
552
+
});
553
553
+
case 'boolean':
554
554
+
return booleanTypeToIdentifier({
555
555
+
context,
556
556
+
namespace,
557
557
+
schema: schema as SchemaWithType<'boolean'>,
558
558
+
});
559
559
+
case 'enum':
560
560
+
return enumTypeToIdentifier({
561
561
+
$ref,
562
562
+
context,
563
563
+
namespace,
564
564
+
schema: schema as SchemaWithType<'enum'>,
565
565
+
});
566
566
+
case 'never':
567
567
+
return compiler.keywordTypeNode({
568
568
+
keyword: 'never',
569
569
+
});
570
570
+
case 'null':
571
571
+
return compiler.literalTypeNode({
572
572
+
literal: compiler.null(),
573
573
+
});
574
574
+
case 'number':
575
575
+
return numberTypeToIdentifier({
576
576
+
context,
577
577
+
namespace,
578
578
+
schema: schema as SchemaWithType<'number'>,
579
579
+
});
580
580
+
case 'object':
581
581
+
return objectTypeToIdentifier({
582
582
+
context,
583
583
+
namespace,
584
584
+
schema: schema as SchemaWithType<'object'>,
585
585
+
});
586
586
+
case 'string':
587
587
+
return stringTypeToIdentifier({
588
588
+
context,
589
589
+
namespace,
590
590
+
schema: schema as SchemaWithType<'string'>,
591
591
+
});
592
592
+
case 'tuple':
593
593
+
return tupleTypeToIdentifier({
594
594
+
context,
595
595
+
namespace,
596
596
+
schema: schema as SchemaWithType<'tuple'>,
597
597
+
});
598
598
+
case 'undefined':
599
599
+
return compiler.keywordTypeNode({
600
600
+
keyword: 'undefined',
601
601
+
});
602
602
+
case 'unknown':
603
603
+
return compiler.keywordTypeNode({
604
604
+
keyword: 'unknown',
605
605
+
});
606
606
+
case 'void':
607
607
+
return compiler.keywordTypeNode({
608
608
+
keyword: 'void',
609
609
+
});
610
610
+
}
611
611
+
};
20
612
21
613
const irParametersToIrSchema = ({
22
614
parameters,
···
56
648
const operationToDataType = ({
57
649
context,
58
650
operation,
59
59
-
options,
60
651
}: {
61
652
context: IRContext;
62
653
operation: IROperationObject;
63
63
-
options: SchemaToTypeOptions;
64
654
}) => {
65
655
const data: IRSchemaObject = {
66
656
type: 'object',
···
143
733
exportType: true,
144
734
name: identifier.name || '',
145
735
type: schemaToType({
146
146
-
options,
736
736
+
context,
147
737
schema: data,
148
738
}),
149
739
});
···
154
744
const operationToType = ({
155
745
context,
156
746
operation,
157
157
-
options,
158
747
}: {
159
748
context: IRContext;
160
749
operation: IROperationObject;
161
161
-
options: SchemaToTypeOptions;
162
750
}) => {
163
751
operationToDataType({
164
752
context,
165
753
operation,
166
166
-
options,
167
754
});
168
755
169
756
const file = context.file({ id: typesId })!;
···
182
769
exportType: true,
183
770
name: identifierErrors.name,
184
771
type: schemaToType({
185
185
-
options,
772
772
+
context,
186
773
schema: errors,
187
774
}),
188
775
});
···
227
814
exportType: true,
228
815
name: identifierResponses.name,
229
816
type: schemaToType({
230
230
-
options,
817
817
+
context,
231
818
schema: responses,
232
819
}),
233
820
});
···
262
849
}
263
850
};
264
851
852
852
+
export const schemaToType = ({
853
853
+
$ref,
854
854
+
context,
855
855
+
namespace = [],
856
856
+
schema,
857
857
+
}: {
858
858
+
$ref?: string;
859
859
+
context: IRContext;
860
860
+
namespace?: Array<ts.Statement>;
861
861
+
schema: IRSchemaObject;
862
862
+
}): ts.TypeNode => {
863
863
+
let type: ts.TypeNode | undefined;
864
864
+
865
865
+
if (schema.$ref) {
866
866
+
const identifier = context.file({ id: typesId })!.identifier({
867
867
+
$ref: schema.$ref,
868
868
+
create: true,
869
869
+
namespace: 'type',
870
870
+
});
871
871
+
type = compiler.typeReferenceNode({
872
872
+
typeName: identifier.name || '',
873
873
+
});
874
874
+
} else if (schema.type) {
875
875
+
type = schemaTypeToIdentifier({
876
876
+
$ref,
877
877
+
context,
878
878
+
namespace,
879
879
+
schema,
880
880
+
});
881
881
+
} else if (schema.items) {
882
882
+
schema = deduplicateSchema({ schema });
883
883
+
if (schema.items) {
884
884
+
const itemTypes = schema.items.map((item) =>
885
885
+
schemaToType({
886
886
+
context,
887
887
+
namespace,
888
888
+
schema: item,
889
889
+
}),
890
890
+
);
891
891
+
type =
892
892
+
schema.logicalOperator === 'and'
893
893
+
? compiler.typeIntersectionNode({ types: itemTypes })
894
894
+
: compiler.typeUnionNode({ types: itemTypes });
895
895
+
} else {
896
896
+
type = schemaToType({
897
897
+
context,
898
898
+
namespace,
899
899
+
schema,
900
900
+
});
901
901
+
}
902
902
+
} else {
903
903
+
// catch-all fallback for failed schemas
904
904
+
type = schemaTypeToIdentifier({
905
905
+
context,
906
906
+
namespace,
907
907
+
schema: {
908
908
+
type: 'unknown',
909
909
+
},
910
910
+
});
911
911
+
}
912
912
+
913
913
+
// emit nodes only if $ref points to a reusable component
914
914
+
if ($ref && isRefOpenApiComponent($ref)) {
915
915
+
// emit namespace if it has any members
916
916
+
if (namespace.length) {
917
917
+
const identifier = context.file({ id: typesId })!.identifier({
918
918
+
$ref,
919
919
+
create: true,
920
920
+
namespace: 'value',
921
921
+
});
922
922
+
const node = compiler.namespaceDeclaration({
923
923
+
name: identifier.name || '',
924
924
+
statements: namespace,
925
925
+
});
926
926
+
context.file({ id: typesId })!.add(node);
927
927
+
}
928
928
+
929
929
+
// enum handler emits its own artifacts
930
930
+
if (schema.type !== 'enum') {
931
931
+
const identifier = context.file({ id: typesId })!.identifier({
932
932
+
$ref,
933
933
+
create: true,
934
934
+
namespace: 'type',
935
935
+
});
936
936
+
const node = compiler.typeAliasDeclaration({
937
937
+
comment: parseSchemaJsDoc({ schema }),
938
938
+
exportType: true,
939
939
+
name: identifier.name || '',
940
940
+
type,
941
941
+
});
942
942
+
context.file({ id: typesId })!.add(node);
943
943
+
}
944
944
+
}
945
945
+
946
946
+
return type;
947
947
+
};
948
948
+
265
949
export const handler: PluginHandler<Config> = ({ context, plugin }) => {
266
266
-
const file = context.createFile({
950
950
+
context.createFile({
267
951
id: typesId,
268
952
path: plugin.output,
269
953
});
270
270
-
const options: SchemaToTypeOptions = {
271
271
-
enums: context.config.plugins['@hey-api/types']?.enums,
272
272
-
file,
273
273
-
useTransformersDate: context.config.plugins['@hey-api/transformers']?.dates,
274
274
-
};
275
954
276
955
if (context.ir.components) {
277
956
for (const name in context.ir.components.schemas) {
278
957
const schema = context.ir.components.schemas[name];
279
958
const $ref = `#/components/schemas/${name}`;
280
959
281
281
-
schemaToType({
282
282
-
$ref,
283
283
-
options,
284
284
-
schema,
285
285
-
});
960
960
+
try {
961
961
+
schemaToType({
962
962
+
$ref,
963
963
+
context,
964
964
+
schema,
965
965
+
});
966
966
+
} catch (error) {
967
967
+
console.error(
968
968
+
`🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(schema, null, 2)}`,
969
969
+
);
970
970
+
throw error;
971
971
+
}
286
972
}
287
973
288
974
for (const name in context.ir.components.parameters) {
289
975
const parameter = context.ir.components.parameters[name];
290
976
const $ref = `#/components/parameters/${name}`;
291
977
292
292
-
schemaToType({
293
293
-
$ref,
294
294
-
options,
295
295
-
schema: parameter.schema,
296
296
-
});
978
978
+
try {
979
979
+
schemaToType({
980
980
+
$ref,
981
981
+
context,
982
982
+
schema: parameter.schema,
983
983
+
});
984
984
+
} catch (error) {
985
985
+
console.error(
986
986
+
`🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(parameter.schema, null, 2)}`,
987
987
+
);
988
988
+
throw error;
989
989
+
}
297
990
}
298
991
}
299
992
···
314
1007
operationToType({
315
1008
context,
316
1009
operation,
317
317
-
options,
318
1010
});
319
1011
}
320
1012
}
+2
-8
packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts
···
29
29
operationOptionsType,
30
30
serviceFunctionIdentifier,
31
31
} from '../../@hey-api/services/plugin-legacy';
32
32
-
import { typesId } from '../../@hey-api/types/plugin';
32
32
+
import { schemaToType } from '../../@hey-api/types/plugin';
33
33
import type { PluginHandler } from '../../types';
34
34
-
import { schemaToType } from '../../utils/types';
35
34
import type { Config as AngularQueryConfig } from '../angular-query-experimental';
36
35
import type { Config as ReactQueryConfig } from '../react-query';
37
36
import type { Config as SolidQueryConfig } from '../solid-query';
···
891
890
// `compiler.returnFunctionCall()` accepts only strings, should be cleaned up
892
891
const typePageParam = `${tsNodeToString({
893
892
node: schemaToType({
894
894
-
options: {
895
895
-
enums: context.config.plugins['@hey-api/types']?.enums,
896
896
-
file: context.file({ id: typesId })!,
897
897
-
useTransformersDate:
898
898
-
context.config.plugins['@hey-api/transformers']?.dates,
899
899
-
},
893
893
+
context,
900
894
schema: pagination.schema,
901
895
}),
902
896
unescape: true,
-683
packages/openapi-ts/src/plugins/utils/types.ts
···
1
1
-
import ts from 'typescript';
2
2
-
3
3
-
import type { Property } from '../../compiler';
4
4
-
import { compiler } from '../../compiler';
5
5
-
import type { TypeScriptFile } from '../../generate/files';
6
6
-
import type { IRSchemaObject } from '../../ir/ir';
7
7
-
import { deduplicateSchema } from '../../ir/schema';
8
8
-
import { ensureValidTypeScriptJavaScriptIdentifier } from '../../openApi';
9
9
-
import { escapeComment } from '../../utils/escape';
10
10
-
import { irRef, isRefOpenApiComponent } from '../../utils/ref';
11
11
-
12
12
-
export type SchemaToTypeOptions = {
13
13
-
enums?: 'javascript' | 'typescript' | 'typescript+namespace' | false;
14
14
-
file: TypeScriptFile;
15
15
-
useTransformersDate?: boolean;
16
16
-
};
17
17
-
18
18
-
interface SchemaWithType<T extends Required<IRSchemaObject>['type']>
19
19
-
extends Omit<IRSchemaObject, 'type'> {
20
20
-
type: Extract<Required<IRSchemaObject>['type'], T>;
21
21
-
}
22
22
-
23
23
-
const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => {
24
24
-
const comments = [
25
25
-
schema.description && escapeComment(schema.description),
26
26
-
schema.deprecated && '@deprecated',
27
27
-
];
28
28
-
return comments;
29
29
-
};
30
30
-
31
31
-
const addJavaScriptEnum = ({
32
32
-
$ref,
33
33
-
schema,
34
34
-
options,
35
35
-
}: {
36
36
-
$ref: string;
37
37
-
options: SchemaToTypeOptions;
38
38
-
schema: SchemaWithType<'enum'>;
39
39
-
}) => {
40
40
-
const identifier = options.file.identifier({
41
41
-
$ref,
42
42
-
create: true,
43
43
-
namespace: 'value',
44
44
-
});
45
45
-
46
46
-
// TODO: parser - this is the old parser behavior where we would NOT
47
47
-
// print nested enum identifiers if they already exist. This is a
48
48
-
// blocker for referencing these identifiers within the file as
49
49
-
// we cannot guarantee just because they have a duplicate identifier,
50
50
-
// they have a duplicate value.
51
51
-
if (!identifier.created) {
52
52
-
return;
53
53
-
}
54
54
-
55
55
-
const enumObject = schemaToEnumObject({ schema });
56
56
-
57
57
-
const expression = compiler.objectExpression({
58
58
-
multiLine: true,
59
59
-
obj: enumObject.obj,
60
60
-
});
61
61
-
const node = compiler.constVariable({
62
62
-
assertion: 'const',
63
63
-
comment: parseSchemaJsDoc({ schema }),
64
64
-
exportConst: true,
65
65
-
expression,
66
66
-
name: identifier.name || '',
67
67
-
});
68
68
-
return node;
69
69
-
};
70
70
-
71
71
-
const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => {
72
72
-
const typeofItems: Array<
73
73
-
| 'string'
74
74
-
| 'number'
75
75
-
| 'bigint'
76
76
-
| 'boolean'
77
77
-
| 'symbol'
78
78
-
| 'undefined'
79
79
-
| 'object'
80
80
-
| 'function'
81
81
-
> = [];
82
82
-
83
83
-
const obj = (schema.items ?? []).map((item) => {
84
84
-
const typeOfItemConst = typeof item.const;
85
85
-
86
86
-
if (!typeofItems.includes(typeOfItemConst)) {
87
87
-
typeofItems.push(typeOfItemConst);
88
88
-
}
89
89
-
90
90
-
let key;
91
91
-
if (item.title) {
92
92
-
key = item.title;
93
93
-
} else if (typeOfItemConst === 'number') {
94
94
-
key = `_${item.const}`;
95
95
-
} else if (typeOfItemConst === 'boolean') {
96
96
-
const valid = typeOfItemConst ? 'true' : 'false';
97
97
-
key = valid.toLocaleUpperCase();
98
98
-
} else {
99
99
-
let valid = ensureValidTypeScriptJavaScriptIdentifier(
100
100
-
item.const as string,
101
101
-
);
102
102
-
if (!valid) {
103
103
-
// TODO: parser - abstract empty string handling
104
104
-
valid = 'empty_string';
105
105
-
}
106
106
-
key = valid.toLocaleUpperCase();
107
107
-
}
108
108
-
return {
109
109
-
comments: parseSchemaJsDoc({ schema: item }),
110
110
-
key,
111
111
-
value: item.const,
112
112
-
};
113
113
-
});
114
114
-
115
115
-
return {
116
116
-
obj,
117
117
-
typeofItems,
118
118
-
};
119
119
-
};
120
120
-
121
121
-
const addTypeEnum = ({
122
122
-
$ref,
123
123
-
schema,
124
124
-
options,
125
125
-
}: {
126
126
-
$ref: string;
127
127
-
options: SchemaToTypeOptions;
128
128
-
schema: SchemaWithType<'enum'>;
129
129
-
}) => {
130
130
-
const identifier = options.file.identifier({
131
131
-
$ref,
132
132
-
create: true,
133
133
-
namespace: 'type',
134
134
-
});
135
135
-
136
136
-
// TODO: parser - this is the old parser behavior where we would NOT
137
137
-
// print nested enum identifiers if they already exist. This is a
138
138
-
// blocker for referencing these identifiers within the file as
139
139
-
// we cannot guarantee just because they have a duplicate identifier,
140
140
-
// they have a duplicate value.
141
141
-
if (
142
142
-
!identifier.created &&
143
143
-
!isRefOpenApiComponent($ref) &&
144
144
-
options.enums !== 'typescript+namespace'
145
145
-
) {
146
146
-
return;
147
147
-
}
148
148
-
149
149
-
const node = compiler.typeAliasDeclaration({
150
150
-
comment: parseSchemaJsDoc({ schema }),
151
151
-
exportType: true,
152
152
-
name: identifier.name || '',
153
153
-
type: schemaToType({
154
154
-
options,
155
155
-
schema: {
156
156
-
...schema,
157
157
-
type: undefined,
158
158
-
},
159
159
-
}),
160
160
-
});
161
161
-
return node;
162
162
-
};
163
163
-
164
164
-
const addTypeScriptEnum = ({
165
165
-
$ref,
166
166
-
schema,
167
167
-
options,
168
168
-
}: {
169
169
-
$ref: string;
170
170
-
options: SchemaToTypeOptions;
171
171
-
schema: SchemaWithType<'enum'>;
172
172
-
}) => {
173
173
-
const identifier = options.file.identifier({
174
174
-
$ref,
175
175
-
create: true,
176
176
-
namespace: 'value',
177
177
-
});
178
178
-
179
179
-
// TODO: parser - this is the old parser behavior where we would NOT
180
180
-
// print nested enum identifiers if they already exist. This is a
181
181
-
// blocker for referencing these identifiers within the file as
182
182
-
// we cannot guarantee just because they have a duplicate identifier,
183
183
-
// they have a duplicate value.
184
184
-
if (!identifier.created && options.enums !== 'typescript+namespace') {
185
185
-
return;
186
186
-
}
187
187
-
188
188
-
const enumObject = schemaToEnumObject({ schema });
189
189
-
190
190
-
// TypeScript enums support only string and number values so we need to fallback to types
191
191
-
if (
192
192
-
enumObject.typeofItems.filter(
193
193
-
(type) => type !== 'number' && type !== 'string',
194
194
-
).length
195
195
-
) {
196
196
-
const node = addTypeEnum({
197
197
-
$ref,
198
198
-
options,
199
199
-
schema,
200
200
-
});
201
201
-
return node;
202
202
-
}
203
203
-
204
204
-
const node = compiler.enumDeclaration({
205
205
-
leadingComment: parseSchemaJsDoc({ schema }),
206
206
-
name: identifier.name || '',
207
207
-
obj: enumObject.obj,
208
208
-
});
209
209
-
return node;
210
210
-
};
211
211
-
212
212
-
const arrayTypeToIdentifier = ({
213
213
-
namespace,
214
214
-
schema,
215
215
-
options,
216
216
-
}: {
217
217
-
namespace: Array<ts.Statement>;
218
218
-
options: SchemaToTypeOptions;
219
219
-
schema: SchemaWithType<'array'>;
220
220
-
}) => {
221
221
-
if (!schema.items) {
222
222
-
return compiler.typeArrayNode(
223
223
-
compiler.keywordTypeNode({
224
224
-
keyword: 'unknown',
225
225
-
}),
226
226
-
);
227
227
-
}
228
228
-
229
229
-
schema = deduplicateSchema({ schema });
230
230
-
231
231
-
// at least one item is guaranteed
232
232
-
const itemTypes = schema.items!.map((item) =>
233
233
-
schemaToType({
234
234
-
namespace,
235
235
-
options,
236
236
-
schema: item,
237
237
-
}),
238
238
-
);
239
239
-
240
240
-
if (itemTypes.length === 1) {
241
241
-
return compiler.typeArrayNode(itemTypes[0]);
242
242
-
}
243
243
-
244
244
-
if (schema.logicalOperator === 'and') {
245
245
-
return compiler.typeArrayNode(
246
246
-
compiler.typeIntersectionNode({ types: itemTypes }),
247
247
-
);
248
248
-
}
249
249
-
250
250
-
return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes }));
251
251
-
};
252
252
-
253
253
-
const booleanTypeToIdentifier = ({
254
254
-
schema,
255
255
-
}: {
256
256
-
schema: SchemaWithType<'boolean'>;
257
257
-
}) => {
258
258
-
if (schema.const !== undefined) {
259
259
-
return compiler.literalTypeNode({
260
260
-
literal: compiler.ots.boolean(schema.const as boolean),
261
261
-
});
262
262
-
}
263
263
-
264
264
-
return compiler.keywordTypeNode({
265
265
-
keyword: 'boolean',
266
266
-
});
267
267
-
};
268
268
-
269
269
-
const enumTypeToIdentifier = ({
270
270
-
$ref,
271
271
-
namespace,
272
272
-
schema,
273
273
-
options,
274
274
-
}: {
275
275
-
$ref?: string;
276
276
-
namespace: Array<ts.Statement>;
277
277
-
options: SchemaToTypeOptions;
278
278
-
schema: SchemaWithType<'enum'>;
279
279
-
}): ts.TypeNode => {
280
280
-
// TODO: parser - add option to inline enums
281
281
-
if ($ref) {
282
282
-
const isRefComponent = isRefOpenApiComponent($ref);
283
283
-
284
284
-
// when enums are disabled (default), emit only reusable components
285
285
-
// as types, otherwise the output would be broken if we skipped all enums
286
286
-
if (!options.enums && isRefComponent) {
287
287
-
const typeNode = addTypeEnum({
288
288
-
$ref,
289
289
-
options,
290
290
-
schema,
291
291
-
});
292
292
-
if (typeNode) {
293
293
-
options.file.add(typeNode);
294
294
-
}
295
295
-
}
296
296
-
297
297
-
if (options.enums === 'javascript') {
298
298
-
const typeNode = addTypeEnum({
299
299
-
$ref,
300
300
-
options,
301
301
-
schema,
302
302
-
});
303
303
-
if (typeNode) {
304
304
-
options.file.add(typeNode);
305
305
-
}
306
306
-
307
307
-
const objectNode = addJavaScriptEnum({
308
308
-
$ref,
309
309
-
options,
310
310
-
schema,
311
311
-
});
312
312
-
if (objectNode) {
313
313
-
options.file.add(objectNode);
314
314
-
}
315
315
-
}
316
316
-
317
317
-
if (options.enums === 'typescript') {
318
318
-
const enumNode = addTypeScriptEnum({
319
319
-
$ref,
320
320
-
options,
321
321
-
schema,
322
322
-
});
323
323
-
if (enumNode) {
324
324
-
options.file.add(enumNode);
325
325
-
}
326
326
-
}
327
327
-
328
328
-
if (options.enums === 'typescript+namespace') {
329
329
-
const enumNode = addTypeScriptEnum({
330
330
-
$ref,
331
331
-
options,
332
332
-
schema,
333
333
-
});
334
334
-
if (enumNode) {
335
335
-
if (isRefComponent) {
336
336
-
options.file.add(enumNode);
337
337
-
} else {
338
338
-
// emit enum inside TypeScript namespace
339
339
-
namespace.push(enumNode);
340
340
-
}
341
341
-
}
342
342
-
}
343
343
-
}
344
344
-
345
345
-
const type = schemaToType({
346
346
-
options,
347
347
-
schema: {
348
348
-
...schema,
349
349
-
type: undefined,
350
350
-
},
351
351
-
});
352
352
-
return type;
353
353
-
};
354
354
-
355
355
-
const numberTypeToIdentifier = ({
356
356
-
schema,
357
357
-
}: {
358
358
-
schema: SchemaWithType<'number'>;
359
359
-
}) => {
360
360
-
if (schema.const !== undefined) {
361
361
-
return compiler.literalTypeNode({
362
362
-
literal: compiler.ots.number(schema.const as number),
363
363
-
});
364
364
-
}
365
365
-
366
366
-
return compiler.keywordTypeNode({
367
367
-
keyword: 'number',
368
368
-
});
369
369
-
};
370
370
-
371
371
-
const digitsRegExp = /^\d+$/;
372
372
-
373
373
-
const objectTypeToIdentifier = ({
374
374
-
namespace,
375
375
-
schema,
376
376
-
options,
377
377
-
}: {
378
378
-
namespace: Array<ts.Statement>;
379
379
-
options: SchemaToTypeOptions;
380
380
-
schema: SchemaWithType<'object'>;
381
381
-
}) => {
382
382
-
let indexProperty: Property | undefined;
383
383
-
const schemaProperties: Array<Property> = [];
384
384
-
let indexPropertyItems: Array<IRSchemaObject> = [];
385
385
-
const required = schema.required ?? [];
386
386
-
let hasOptionalProperties = false;
387
387
-
388
388
-
for (const name in schema.properties) {
389
389
-
const property = schema.properties[name];
390
390
-
const isRequired = required.includes(name);
391
391
-
digitsRegExp.lastIndex = 0;
392
392
-
schemaProperties.push({
393
393
-
comment: parseSchemaJsDoc({ schema: property }),
394
394
-
isReadOnly: property.accessScope === 'read',
395
395
-
isRequired,
396
396
-
name: digitsRegExp.test(name)
397
397
-
? ts.factory.createNumericLiteral(name)
398
398
-
: name,
399
399
-
type: schemaToType({
400
400
-
$ref: `${irRef}${name}`,
401
401
-
namespace,
402
402
-
options,
403
403
-
schema: property,
404
404
-
}),
405
405
-
});
406
406
-
indexPropertyItems.push(property);
407
407
-
408
408
-
if (!isRequired) {
409
409
-
hasOptionalProperties = true;
410
410
-
}
411
411
-
}
412
412
-
413
413
-
if (
414
414
-
schema.additionalProperties &&
415
415
-
(schema.additionalProperties.type !== 'never' || !indexPropertyItems.length)
416
416
-
) {
417
417
-
if (schema.additionalProperties.type === 'never') {
418
418
-
indexPropertyItems = [schema.additionalProperties];
419
419
-
} else {
420
420
-
indexPropertyItems.unshift(schema.additionalProperties);
421
421
-
}
422
422
-
423
423
-
if (hasOptionalProperties) {
424
424
-
indexPropertyItems.push({
425
425
-
type: 'undefined',
426
426
-
});
427
427
-
}
428
428
-
429
429
-
indexProperty = {
430
430
-
isRequired: true,
431
431
-
name: 'key',
432
432
-
type: schemaToType({
433
433
-
namespace,
434
434
-
options,
435
435
-
schema:
436
436
-
indexPropertyItems.length === 1
437
437
-
? indexPropertyItems[0]
438
438
-
: {
439
439
-
items: indexPropertyItems,
440
440
-
logicalOperator: 'or',
441
441
-
},
442
442
-
}),
443
443
-
};
444
444
-
}
445
445
-
446
446
-
return compiler.typeInterfaceNode({
447
447
-
indexProperty,
448
448
-
properties: schemaProperties,
449
449
-
useLegacyResolution: false,
450
450
-
});
451
451
-
};
452
452
-
453
453
-
const stringTypeToIdentifier = ({
454
454
-
schema,
455
455
-
options,
456
456
-
}: {
457
457
-
options: SchemaToTypeOptions;
458
458
-
schema: SchemaWithType<'string'>;
459
459
-
}) => {
460
460
-
if (schema.const !== undefined) {
461
461
-
return compiler.literalTypeNode({
462
462
-
literal: compiler.stringLiteral({ text: schema.const as string }),
463
463
-
});
464
464
-
}
465
465
-
466
466
-
if (schema.format) {
467
467
-
if (schema.format === 'binary') {
468
468
-
return compiler.typeUnionNode({
469
469
-
types: [
470
470
-
compiler.typeReferenceNode({
471
471
-
typeName: 'Blob',
472
472
-
}),
473
473
-
compiler.typeReferenceNode({
474
474
-
typeName: 'File',
475
475
-
}),
476
476
-
],
477
477
-
});
478
478
-
}
479
479
-
480
480
-
if (schema.format === 'date-time' || schema.format === 'date') {
481
481
-
// TODO: parser - add ability to skip type transformers
482
482
-
if (options.useTransformersDate) {
483
483
-
return compiler.typeReferenceNode({ typeName: 'Date' });
484
484
-
}
485
485
-
}
486
486
-
}
487
487
-
488
488
-
return compiler.keywordTypeNode({
489
489
-
keyword: 'string',
490
490
-
});
491
491
-
};
492
492
-
493
493
-
const tupleTypeToIdentifier = ({
494
494
-
namespace,
495
495
-
schema,
496
496
-
options,
497
497
-
}: {
498
498
-
namespace: Array<ts.Statement>;
499
499
-
options: SchemaToTypeOptions;
500
500
-
schema: SchemaWithType<'tuple'>;
501
501
-
}) => {
502
502
-
const itemTypes: Array<ts.TypeNode> = [];
503
503
-
504
504
-
for (const item of schema.items ?? []) {
505
505
-
itemTypes.push(
506
506
-
schemaToType({
507
507
-
namespace,
508
508
-
options,
509
509
-
schema: item,
510
510
-
}),
511
511
-
);
512
512
-
}
513
513
-
514
514
-
return compiler.typeTupleNode({
515
515
-
types: itemTypes,
516
516
-
});
517
517
-
};
518
518
-
519
519
-
const schemaTypeToIdentifier = ({
520
520
-
$ref,
521
521
-
namespace,
522
522
-
schema,
523
523
-
options,
524
524
-
}: {
525
525
-
$ref?: string;
526
526
-
namespace: Array<ts.Statement>;
527
527
-
options: SchemaToTypeOptions;
528
528
-
schema: IRSchemaObject;
529
529
-
}): ts.TypeNode => {
530
530
-
switch (schema.type as Required<IRSchemaObject>['type']) {
531
531
-
case 'array':
532
532
-
return arrayTypeToIdentifier({
533
533
-
namespace,
534
534
-
options,
535
535
-
schema: schema as SchemaWithType<'array'>,
536
536
-
});
537
537
-
case 'boolean':
538
538
-
return booleanTypeToIdentifier({
539
539
-
schema: schema as SchemaWithType<'boolean'>,
540
540
-
});
541
541
-
case 'enum':
542
542
-
return enumTypeToIdentifier({
543
543
-
$ref,
544
544
-
namespace,
545
545
-
options,
546
546
-
schema: schema as SchemaWithType<'enum'>,
547
547
-
});
548
548
-
case 'never':
549
549
-
return compiler.keywordTypeNode({ keyword: 'never' });
550
550
-
case 'null':
551
551
-
return compiler.literalTypeNode({
552
552
-
literal: compiler.null(),
553
553
-
});
554
554
-
case 'number':
555
555
-
return numberTypeToIdentifier({
556
556
-
schema: schema as SchemaWithType<'number'>,
557
557
-
});
558
558
-
case 'object':
559
559
-
return objectTypeToIdentifier({
560
560
-
namespace,
561
561
-
options,
562
562
-
schema: schema as SchemaWithType<'object'>,
563
563
-
});
564
564
-
case 'string':
565
565
-
return stringTypeToIdentifier({
566
566
-
options,
567
567
-
schema: schema as SchemaWithType<'string'>,
568
568
-
});
569
569
-
case 'tuple':
570
570
-
return tupleTypeToIdentifier({
571
571
-
namespace,
572
572
-
options,
573
573
-
schema: schema as SchemaWithType<'tuple'>,
574
574
-
});
575
575
-
case 'undefined':
576
576
-
return compiler.keywordTypeNode({ keyword: 'undefined' });
577
577
-
case 'unknown':
578
578
-
return compiler.keywordTypeNode({
579
579
-
keyword: 'unknown',
580
580
-
});
581
581
-
case 'void':
582
582
-
return compiler.keywordTypeNode({
583
583
-
keyword: 'void',
584
584
-
});
585
585
-
}
586
586
-
};
587
587
-
588
588
-
export const schemaToType = ({
589
589
-
$ref,
590
590
-
namespace = [],
591
591
-
schema,
592
592
-
options,
593
593
-
}: {
594
594
-
$ref?: string;
595
595
-
namespace?: Array<ts.Statement>;
596
596
-
options: SchemaToTypeOptions;
597
597
-
schema: IRSchemaObject;
598
598
-
}): ts.TypeNode => {
599
599
-
let type: ts.TypeNode | undefined;
600
600
-
601
601
-
if (schema.$ref) {
602
602
-
const identifier = options.file.identifier({
603
603
-
$ref: schema.$ref,
604
604
-
create: true,
605
605
-
namespace: 'type',
606
606
-
});
607
607
-
type = compiler.typeReferenceNode({
608
608
-
typeName: identifier.name || '',
609
609
-
});
610
610
-
} else if (schema.type) {
611
611
-
type = schemaTypeToIdentifier({
612
612
-
$ref,
613
613
-
namespace,
614
614
-
options,
615
615
-
schema,
616
616
-
});
617
617
-
} else if (schema.items) {
618
618
-
schema = deduplicateSchema({ schema });
619
619
-
if (schema.items) {
620
620
-
const itemTypes = schema.items.map((item) =>
621
621
-
schemaToType({
622
622
-
namespace,
623
623
-
options,
624
624
-
schema: item,
625
625
-
}),
626
626
-
);
627
627
-
type =
628
628
-
schema.logicalOperator === 'and'
629
629
-
? compiler.typeIntersectionNode({ types: itemTypes })
630
630
-
: compiler.typeUnionNode({ types: itemTypes });
631
631
-
} else {
632
632
-
type = schemaToType({
633
633
-
namespace,
634
634
-
options,
635
635
-
schema,
636
636
-
});
637
637
-
}
638
638
-
} else {
639
639
-
// catch-all fallback for failed schemas
640
640
-
type = schemaTypeToIdentifier({
641
641
-
namespace,
642
642
-
options,
643
643
-
schema: {
644
644
-
type: 'unknown',
645
645
-
},
646
646
-
});
647
647
-
}
648
648
-
649
649
-
// emit nodes only if $ref points to a reusable component
650
650
-
if ($ref && isRefOpenApiComponent($ref)) {
651
651
-
// emit namespace if it has any members
652
652
-
if (namespace.length) {
653
653
-
const identifier = options.file.identifier({
654
654
-
$ref,
655
655
-
create: true,
656
656
-
namespace: 'value',
657
657
-
});
658
658
-
const node = compiler.namespaceDeclaration({
659
659
-
name: identifier.name || '',
660
660
-
statements: namespace,
661
661
-
});
662
662
-
options.file.add(node);
663
663
-
}
664
664
-
665
665
-
// enum handler emits its own artifacts
666
666
-
if (schema.type !== 'enum') {
667
667
-
const identifier = options.file.identifier({
668
668
-
$ref,
669
669
-
create: true,
670
670
-
namespace: 'type',
671
671
-
});
672
672
-
const node = compiler.typeAliasDeclaration({
673
673
-
comment: parseSchemaJsDoc({ schema }),
674
674
-
exportType: true,
675
675
-
name: identifier.name || '',
676
676
-
type,
677
677
-
});
678
678
-
options.file.add(node);
679
679
-
}
680
680
-
}
681
681
-
682
682
-
return type;
683
683
-
};