fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1180 from hey-api/feat/parser-plugin-schemas

feat: rewrite schemas to new parser

authored by

Lubos and committed by
GitHub
55d57151 2acbc809

+1856 -1625
+6 -3
packages/openapi-ts/src/generate/output.ts
··· 2 2 3 3 import type { IRContext } from '../ir/context'; 4 4 import type { OpenApi } from '../openApi'; 5 + import { generateSchemas } from '../plugins/@hey-api/schemas/plugin'; 6 + import { generateServices } from '../plugins/@hey-api/services/plugin'; 7 + import { generateTypes } from '../plugins/@hey-api/types/plugin'; 5 8 import type { Client } from '../types/client'; 6 9 import type { Files } from '../types/utils'; 7 10 import { getConfig, isLegacyClient } from '../utils/config'; ··· 12 15 import { generateIndexFile } from './indexFile'; 13 16 import { generateLegacyPlugins } from './plugins'; 14 17 import { generateLegacySchemas } from './schemas'; 15 - import { generateLegacyServices, generateServices } from './services'; 18 + import { generateLegacyServices } from './services'; 16 19 import { generateLegacyTransformers } from './transformers'; 17 - import { generateLegacyTypes, generateTypes } from './types'; 20 + import { generateLegacyTypes } from './types'; 18 21 19 22 /** 20 23 * Write our OpenAPI client, using the given templates at the given output ··· 125 128 generateTypes({ context }); 126 129 127 130 // schemas.gen.ts 128 - // await generateLegacySchemas({ files, openApi }); 131 + generateSchemas({ context }); 129 132 130 133 // transformers 131 134 if (
+1 -468
packages/openapi-ts/src/generate/services.ts
··· 1 - import type ts from 'typescript'; 2 - 3 1 import type { 4 2 ClassElement, 5 3 Comments, ··· 8 6 } from '../compiler'; 9 7 import { compiler } from '../compiler'; 10 8 import type { FunctionTypeParameter, ObjectValue } from '../compiler/types'; 11 - import type { IRContext } from '../ir/context'; 12 - import type { 13 - IROperationObject, 14 - IRPathItemObject, 15 - IRPathsObject, 16 - } from '../ir/ir'; 17 - import { hasOperationDataRequired } from '../ir/operation'; 9 + import type { IROperationObject } from '../ir/ir'; 18 10 import { isOperationParameterRequired } from '../openApi'; 19 11 import type { 20 12 Client, ··· 28 20 import { camelCase } from '../utils/camelCase'; 29 21 import { getConfig, isLegacyClient } from '../utils/config'; 30 22 import { escapeComment, escapeName } from '../utils/escape'; 31 - import { getServiceName } from '../utils/postprocess'; 32 23 import { reservedWordsRegExp } from '../utils/regexp'; 33 24 import { transformServiceName } from '../utils/transform'; 34 25 import { setUniqueTypeName } from '../utils/type'; 35 26 import { unique } from '../utils/unique'; 36 27 import { clientModulePath, clientOptionsTypeName } from './client'; 37 28 import { TypeScriptFile } from './files'; 38 - import { irRef } from './types'; 39 29 40 30 type OnNode = (node: Node) => void; 41 31 type OnImport = (name: string) => void; 42 - 43 - const servicesId = 'services'; 44 32 45 33 export const generateImport = ({ 46 34 meta, ··· 73 61 export const modelResponseTransformerTypeName = (name: string) => 74 62 `${name}ModelResponseTransformer`; 75 63 76 - interface OperationIRRef { 77 - /** 78 - * Operation ID 79 - */ 80 - id: string; 81 - } 82 - 83 - const operationIrRef = ({ 84 - id, 85 - type, 86 - }: OperationIRRef & { 87 - type: 'data' | 'error' | 'response'; 88 - }): string => { 89 - let affix = ''; 90 - switch (type) { 91 - case 'data': 92 - affix = 'Data'; 93 - break; 94 - case 'error': 95 - affix = 'Error'; 96 - break; 97 - case 'response': 98 - affix = 'Response'; 99 - break; 100 - } 101 - return `${irRef}${camelCase({ 102 - input: id, 103 - pascalCase: true, 104 - })}${affix}`; 105 - }; 106 - 107 - export const operationDataRef = ({ id }: OperationIRRef): string => 108 - operationIrRef({ id, type: 'data' }); 109 - 110 64 export const operationDataTypeName = (name: string) => 111 65 `${camelCase({ 112 66 input: name, 113 67 pascalCase: true, 114 68 })}Data`; 115 69 116 - export const operationErrorRef = ({ id }: OperationIRRef): string => 117 - operationIrRef({ id, type: 'error' }); 118 - 119 70 export const operationErrorTypeName = (name: string) => 120 71 `${camelCase({ 121 72 input: name, ··· 125 76 // operation response type ends with "Response", it's enough to append "Transformer" 126 77 export const operationResponseTransformerTypeName = (name: string) => 127 78 `${name}Transformer`; 128 - 129 - export const operationResponseRef = ({ id }: OperationIRRef): string => 130 - operationIrRef({ id, type: 'response' }); 131 79 132 80 export const operationResponseTypeName = (name: string) => 133 81 `${camelCase({ ··· 822 770 } 823 771 }; 824 772 825 - const checkPrerequisites = ({ context }: { context: IRContext }) => { 826 - if (!context.config.client.name) { 827 - throw new Error( 828 - '🚫 client needs to be set to generate services - which HTTP client do you want to use?', 829 - ); 830 - } 831 - 832 - if (!context.file({ id: 'types' })) { 833 - throw new Error( 834 - '🚫 types need to be exported to generate services - enable type generation', 835 - ); 836 - } 837 - }; 838 - 839 773 export const generateLegacyServices = async ({ 840 774 client, 841 775 files, ··· 970 904 }); 971 905 } 972 906 }; 973 - 974 - const requestOptions = ({ 975 - context, 976 - operation, 977 - path, 978 - }: { 979 - context: IRContext; 980 - operation: IROperationObject; 981 - path: string; 982 - }) => { 983 - const file = context.file({ id: servicesId })!; 984 - const servicesOutput = file.nameWithoutExtension(); 985 - // const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}` 986 - 987 - // TODO: parser - add response transformers 988 - // const operationName = operationResponseTypeName(operation.name); 989 - // const { name: responseTransformerName } = setUniqueTypeName({ 990 - // client, 991 - // meta: { 992 - // $ref: `transformers/${operationName}`, 993 - // name: operationName, 994 - // }, 995 - // nameTransformer: operationResponseTransformerTypeName, 996 - // }); 997 - 998 - // if (responseTransformerName) { 999 - // file.import({ 1000 - // // this detection could be done safer, but it shouldn't cause any issues 1001 - // asType: !responseTransformerName.endsWith('Transformer'), 1002 - // module: typesModule, 1003 - // name: responseTransformerName, 1004 - // }); 1005 - // } 1006 - 1007 - const obj: ObjectValue[] = [{ spread: 'options' }]; 1008 - 1009 - if (operation.body) { 1010 - switch (operation.body.type) { 1011 - case 'form-data': 1012 - obj.push({ spread: 'formDataBodySerializer' }); 1013 - file.import({ 1014 - module: clientModulePath({ 1015 - config: context.config, 1016 - sourceOutput: servicesOutput, 1017 - }), 1018 - name: 'formDataBodySerializer', 1019 - }); 1020 - break; 1021 - case 'json': 1022 - break; 1023 - case 'url-search-params': 1024 - obj.push({ spread: 'urlSearchParamsBodySerializer' }); 1025 - file.import({ 1026 - module: clientModulePath({ 1027 - config: context.config, 1028 - sourceOutput: servicesOutput, 1029 - }), 1030 - name: 'urlSearchParamsBodySerializer', 1031 - }); 1032 - break; 1033 - } 1034 - 1035 - obj.push({ 1036 - key: 'headers', 1037 - value: [ 1038 - { 1039 - key: 'Content-Type', 1040 - // form-data does not need Content-Type header, browser will set it automatically 1041 - value: 1042 - operation.body.type === 'form-data' 1043 - ? null 1044 - : operation.body.mediaType, 1045 - }, 1046 - { 1047 - spread: 'options?.headers', 1048 - }, 1049 - ], 1050 - }); 1051 - } 1052 - 1053 - // TODO: parser - set parseAs to skip inference if every response has the same 1054 - // content type. currently impossible because successes do not contain 1055 - // header information 1056 - 1057 - obj.push({ 1058 - key: 'url', 1059 - value: path, 1060 - }); 1061 - 1062 - // TODO: parser - add response transformers 1063 - // if (responseTransformerName) { 1064 - // obj = [ 1065 - // ...obj, 1066 - // { 1067 - // key: 'responseTransformer', 1068 - // value: responseTransformerName, 1069 - // }, 1070 - // ]; 1071 - // } 1072 - 1073 - return compiler.objectExpression({ 1074 - identifiers: ['responseTransformer'], 1075 - obj, 1076 - }); 1077 - }; 1078 - 1079 - const generateClassServices = ({ context }: { context: IRContext }) => { 1080 - const file = context.file({ id: servicesId })!; 1081 - const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}`; 1082 - 1083 - const services = new Map<string, Array<ts.MethodDeclaration>>(); 1084 - 1085 - for (const path in context.ir.paths) { 1086 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 1087 - 1088 - for (const _method in pathItem) { 1089 - const method = _method as keyof IRPathItemObject; 1090 - const operation = pathItem[method]!; 1091 - 1092 - const identifierData = context.file({ id: 'types' })!.identifier({ 1093 - $ref: operationDataRef({ id: operation.id }), 1094 - namespace: 'type', 1095 - }); 1096 - if (identifierData.name) { 1097 - file.import({ 1098 - // this detection could be done safer, but it shouldn't cause any issues 1099 - asType: !identifierData.name.endsWith('Transformer'), 1100 - module: typesModule, 1101 - name: identifierData.name, 1102 - }); 1103 - } 1104 - 1105 - const identifierError = context.file({ id: 'types' })!.identifier({ 1106 - $ref: operationErrorRef({ id: operation.id }), 1107 - namespace: 'type', 1108 - }); 1109 - if (identifierError.name) { 1110 - file.import({ 1111 - // this detection could be done safer, but it shouldn't cause any issues 1112 - asType: !identifierError.name.endsWith('Transformer'), 1113 - module: typesModule, 1114 - name: identifierError.name, 1115 - }); 1116 - } 1117 - 1118 - const identifierResponse = context.file({ id: 'types' })!.identifier({ 1119 - $ref: operationResponseRef({ id: operation.id }), 1120 - namespace: 'type', 1121 - }); 1122 - if (identifierResponse.name) { 1123 - file.import({ 1124 - // this detection could be done safer, but it shouldn't cause any issues 1125 - asType: !identifierResponse.name.endsWith('Transformer'), 1126 - module: typesModule, 1127 - name: identifierResponse.name, 1128 - }); 1129 - } 1130 - 1131 - const node = compiler.methodDeclaration({ 1132 - accessLevel: 'public', 1133 - comment: [ 1134 - operation.deprecated && '@deprecated', 1135 - operation.summary && escapeComment(operation.summary), 1136 - operation.description && escapeComment(operation.description), 1137 - ], 1138 - isStatic: true, 1139 - name: serviceFunctionIdentifier({ 1140 - config: context.config, 1141 - handleIllegal: false, 1142 - id: operation.id, 1143 - operation, 1144 - }), 1145 - parameters: [ 1146 - { 1147 - isRequired: hasOperationDataRequired(operation), 1148 - name: 'options', 1149 - type: operationOptionsType({ 1150 - importedType: identifierData.name, 1151 - throwOnError: 'ThrowOnError', 1152 - }), 1153 - }, 1154 - ], 1155 - returnType: undefined, 1156 - statements: [ 1157 - compiler.returnFunctionCall({ 1158 - args: [ 1159 - requestOptions({ 1160 - context, 1161 - operation, 1162 - path, 1163 - }), 1164 - ], 1165 - name: `(options?.client ?? client).${method}`, 1166 - types: [ 1167 - identifierResponse.name || 'unknown', 1168 - identifierError.name || 'unknown', 1169 - 'ThrowOnError', 1170 - ], 1171 - }), 1172 - ], 1173 - types: [ 1174 - { 1175 - default: false, 1176 - extends: 'boolean', 1177 - name: 'ThrowOnError', 1178 - }, 1179 - ], 1180 - }); 1181 - 1182 - const uniqueTags = Array.from(new Set(operation.tags)); 1183 - if (!uniqueTags.length) { 1184 - uniqueTags.push('default'); 1185 - } 1186 - 1187 - for (const tag of uniqueTags) { 1188 - const serviceName = getServiceName(tag); 1189 - const nodes = services.get(serviceName) ?? []; 1190 - nodes.push(node); 1191 - services.set(serviceName, nodes); 1192 - } 1193 - } 1194 - } 1195 - 1196 - for (const [serviceName, nodes] of services) { 1197 - const node = compiler.classDeclaration({ 1198 - decorator: undefined, 1199 - members: nodes, 1200 - name: transformServiceName({ 1201 - config: context.config, 1202 - name: serviceName, 1203 - }), 1204 - }); 1205 - file.add(node); 1206 - } 1207 - }; 1208 - 1209 - const generateFlatServices = ({ context }: { context: IRContext }) => { 1210 - const file = context.file({ id: servicesId })!; 1211 - const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}`; 1212 - 1213 - for (const path in context.ir.paths) { 1214 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 1215 - 1216 - for (const _method in pathItem) { 1217 - const method = _method as keyof IRPathItemObject; 1218 - const operation = pathItem[method]!; 1219 - 1220 - const identifierData = context.file({ id: 'types' })!.identifier({ 1221 - $ref: operationDataRef({ id: operation.id }), 1222 - namespace: 'type', 1223 - }); 1224 - if (identifierData.name) { 1225 - file.import({ 1226 - // this detection could be done safer, but it shouldn't cause any issues 1227 - asType: !identifierData.name.endsWith('Transformer'), 1228 - module: typesModule, 1229 - name: identifierData.name, 1230 - }); 1231 - } 1232 - 1233 - const identifierError = context.file({ id: 'types' })!.identifier({ 1234 - $ref: operationErrorRef({ id: operation.id }), 1235 - namespace: 'type', 1236 - }); 1237 - if (identifierError.name) { 1238 - file.import({ 1239 - // this detection could be done safer, but it shouldn't cause any issues 1240 - asType: !identifierError.name.endsWith('Transformer'), 1241 - module: typesModule, 1242 - name: identifierError.name, 1243 - }); 1244 - } 1245 - 1246 - const identifierResponse = context.file({ id: 'types' })!.identifier({ 1247 - $ref: operationResponseRef({ id: operation.id }), 1248 - namespace: 'type', 1249 - }); 1250 - if (identifierResponse.name) { 1251 - file.import({ 1252 - // this detection could be done safer, but it shouldn't cause any issues 1253 - asType: !identifierResponse.name.endsWith('Transformer'), 1254 - module: typesModule, 1255 - name: identifierResponse.name, 1256 - }); 1257 - } 1258 - 1259 - const node = compiler.constVariable({ 1260 - comment: [ 1261 - operation.deprecated && '@deprecated', 1262 - operation.summary && escapeComment(operation.summary), 1263 - operation.description && escapeComment(operation.description), 1264 - ], 1265 - exportConst: true, 1266 - expression: compiler.arrowFunction({ 1267 - parameters: [ 1268 - { 1269 - isRequired: hasOperationDataRequired(operation), 1270 - name: 'options', 1271 - type: operationOptionsType({ 1272 - importedType: identifierData.name, 1273 - throwOnError: 'ThrowOnError', 1274 - }), 1275 - }, 1276 - ], 1277 - returnType: undefined, 1278 - statements: [ 1279 - compiler.returnFunctionCall({ 1280 - args: [ 1281 - requestOptions({ 1282 - context, 1283 - operation, 1284 - path, 1285 - }), 1286 - ], 1287 - name: `(options?.client ?? client).${method}`, 1288 - types: [ 1289 - identifierResponse.name || 'unknown', 1290 - identifierError.name || 'unknown', 1291 - 'ThrowOnError', 1292 - ], 1293 - }), 1294 - ], 1295 - types: [ 1296 - { 1297 - default: false, 1298 - extends: 'boolean', 1299 - name: 'ThrowOnError', 1300 - }, 1301 - ], 1302 - }), 1303 - name: serviceFunctionIdentifier({ 1304 - config: context.config, 1305 - handleIllegal: true, 1306 - id: operation.id, 1307 - operation, 1308 - }), 1309 - }); 1310 - file.add(node); 1311 - } 1312 - } 1313 - }; 1314 - 1315 - export const generateServices = ({ context }: { context: IRContext }) => { 1316 - // TODO: parser - once services are a plugin, this logic can be simplified 1317 - if (!context.config.services.export) { 1318 - return; 1319 - } 1320 - 1321 - checkPrerequisites({ context }); 1322 - 1323 - const file = context.createFile({ 1324 - id: servicesId, 1325 - path: 'services', 1326 - }); 1327 - const servicesOutput = file.nameWithoutExtension(); 1328 - 1329 - // import required packages and core files 1330 - file.import({ 1331 - module: clientModulePath({ 1332 - config: context.config, 1333 - sourceOutput: servicesOutput, 1334 - }), 1335 - name: 'createClient', 1336 - }); 1337 - file.import({ 1338 - module: clientModulePath({ 1339 - config: context.config, 1340 - sourceOutput: servicesOutput, 1341 - }), 1342 - name: 'createConfig', 1343 - }); 1344 - file.import({ 1345 - asType: true, 1346 - module: clientModulePath({ 1347 - config: context.config, 1348 - sourceOutput: servicesOutput, 1349 - }), 1350 - name: clientOptionsTypeName(), 1351 - }); 1352 - 1353 - // define client first 1354 - const statement = compiler.constVariable({ 1355 - exportConst: true, 1356 - expression: compiler.callExpression({ 1357 - functionName: 'createClient', 1358 - parameters: [ 1359 - compiler.callExpression({ 1360 - functionName: 'createConfig', 1361 - }), 1362 - ], 1363 - }), 1364 - name: 'client', 1365 - }); 1366 - file.add(statement); 1367 - 1368 - if (context.config.services.asClass) { 1369 - generateClassServices({ context }); 1370 - } else { 1371 - generateFlatServices({ context }); 1372 - } 1373 - };
+1 -1138
packages/openapi-ts/src/generate/types.ts
··· 1 1 import type { EnumDeclaration } from 'typescript'; 2 - import type ts from 'typescript'; 3 2 4 - import type { Property } from '../compiler'; 5 3 import { type Comments, compiler, type Node } from '../compiler'; 6 - import type { IRContext } from '../ir/context'; 7 - import type { 8 - IROperationObject, 9 - IRParameterObject, 10 - IRPathsObject, 11 - IRResponseObject, 12 - IRSchemaObject, 13 - } from '../ir/ir'; 14 - import { addItemsToSchema } from '../ir/utils'; 15 - import { 16 - ensureValidTypeScriptJavaScriptIdentifier, 17 - isOperationParameterRequired, 18 - } from '../openApi'; 4 + import { isOperationParameterRequired } from '../openApi'; 19 5 import type { 20 6 Client, 21 7 Method, ··· 26 12 import { getConfig, isLegacyClient } from '../utils/config'; 27 13 import { enumEntry, enumUnionType } from '../utils/enum'; 28 14 import { escapeComment } from '../utils/escape'; 29 - import { isRefOpenApiComponent } from '../utils/ref'; 30 15 import { sortByName, sorterByName } from '../utils/sort'; 31 16 import { 32 17 setUniqueTypeName, ··· 35 20 } from '../utils/type'; 36 21 import { TypeScriptFile } from './files'; 37 22 import { 38 - operationDataRef, 39 23 operationDataTypeName, 40 - operationErrorRef, 41 24 operationErrorTypeName, 42 - operationResponseRef, 43 25 operationResponseTypeName, 44 26 } from './services'; 45 27 ··· 50 32 onRemoveNode?: VoidFunction; 51 33 } 52 34 53 - interface SchemaWithType<T extends Required<IRSchemaObject>['type']> 54 - extends Omit<IRSchemaObject, 'type'> { 55 - type: Extract<Required<IRSchemaObject>['type'], T>; 56 - } 57 - 58 35 const treeName = '$OpenApiTs'; 59 - 60 - export const irRef = '#/ir/'; 61 - const typesId = 'types'; 62 36 63 37 export const emptyModel: Model = { 64 38 $refs: [], ··· 626 600 } 627 601 }; 628 602 629 - const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => { 630 - const comments = [ 631 - schema.description && escapeComment(schema.description), 632 - schema.deprecated && '@deprecated', 633 - ]; 634 - return comments; 635 - }; 636 - 637 - const addJavaScriptEnum = ({ 638 - $ref, 639 - context, 640 - schema, 641 - }: { 642 - $ref: string; 643 - context: IRContext; 644 - schema: SchemaWithType<'enum'>; 645 - }) => { 646 - const identifier = context.file({ id: typesId })!.identifier({ 647 - $ref, 648 - create: true, 649 - namespace: 'value', 650 - }); 651 - 652 - // TODO: parser - this is the old parser behavior where we would NOT 653 - // print nested enum identifiers if they already exist. This is a 654 - // blocker for referencing these identifiers within the file as 655 - // we cannot guarantee just because they have a duplicate identifier, 656 - // they have a duplicate value. 657 - if (!identifier.created) { 658 - return; 659 - } 660 - 661 - const enumObject = schemaToEnumObject({ schema }); 662 - 663 - const expression = compiler.objectExpression({ 664 - multiLine: true, 665 - obj: enumObject.obj, 666 - }); 667 - const node = compiler.constVariable({ 668 - assertion: 'const', 669 - comment: parseSchemaJsDoc({ schema }), 670 - exportConst: true, 671 - expression, 672 - name: identifier.name, 673 - }); 674 - return node; 675 - }; 676 - 677 - const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => { 678 - const typeofItems: Array< 679 - | 'string' 680 - | 'number' 681 - | 'bigint' 682 - | 'boolean' 683 - | 'symbol' 684 - | 'undefined' 685 - | 'object' 686 - | 'function' 687 - > = []; 688 - 689 - const obj = (schema.items ?? []).map((item) => { 690 - const typeOfItemConst = typeof item.const; 691 - 692 - if (!typeofItems.includes(typeOfItemConst)) { 693 - typeofItems.push(typeOfItemConst); 694 - } 695 - 696 - let key; 697 - if (item.title) { 698 - key = item.title; 699 - } else if (typeOfItemConst === 'number') { 700 - key = `_${item.const}`; 701 - } else if (typeOfItemConst === 'boolean') { 702 - const valid = typeOfItemConst ? 'true' : 'false'; 703 - key = valid.toLocaleUpperCase(); 704 - } else { 705 - let valid = ensureValidTypeScriptJavaScriptIdentifier( 706 - item.const as string, 707 - ); 708 - if (!valid) { 709 - // TODO: parser - abstract empty string handling 710 - valid = 'empty_string'; 711 - } 712 - key = valid.toLocaleUpperCase(); 713 - } 714 - return { 715 - comments: parseSchemaJsDoc({ schema: item }), 716 - key, 717 - value: item.const, 718 - }; 719 - }); 720 - 721 - return { 722 - obj, 723 - typeofItems, 724 - }; 725 - }; 726 - 727 - const addTypeEnum = ({ 728 - $ref, 729 - context, 730 - schema, 731 - }: { 732 - $ref: string; 733 - context: IRContext; 734 - schema: SchemaWithType<'enum'>; 735 - }) => { 736 - const identifier = context.file({ id: typesId })!.identifier({ 737 - $ref, 738 - create: true, 739 - namespace: 'type', 740 - }); 741 - 742 - // TODO: parser - this is the old parser behavior where we would NOT 743 - // print nested enum identifiers if they already exist. This is a 744 - // blocker for referencing these identifiers within the file as 745 - // we cannot guarantee just because they have a duplicate identifier, 746 - // they have a duplicate value. 747 - if ( 748 - !identifier.created && 749 - context.config.types.enums !== 'typescript+namespace' 750 - ) { 751 - return; 752 - } 753 - 754 - const node = compiler.typeAliasDeclaration({ 755 - comment: parseSchemaJsDoc({ schema }), 756 - exportType: true, 757 - name: identifier.name, 758 - type: schemaToType({ 759 - context, 760 - schema: { 761 - ...schema, 762 - type: undefined, 763 - }, 764 - }), 765 - }); 766 - return node; 767 - }; 768 - 769 - const addTypeScriptEnum = ({ 770 - $ref, 771 - context, 772 - schema, 773 - }: { 774 - $ref: string; 775 - context: IRContext; 776 - schema: SchemaWithType<'enum'>; 777 - }) => { 778 - const identifier = context.file({ id: typesId })!.identifier({ 779 - $ref, 780 - create: true, 781 - namespace: 'value', 782 - }); 783 - 784 - // TODO: parser - this is the old parser behavior where we would NOT 785 - // print nested enum identifiers if they already exist. This is a 786 - // blocker for referencing these identifiers within the file as 787 - // we cannot guarantee just because they have a duplicate identifier, 788 - // they have a duplicate value. 789 - if ( 790 - !identifier.created && 791 - context.config.types.enums !== 'typescript+namespace' 792 - ) { 793 - return; 794 - } 795 - 796 - const enumObject = schemaToEnumObject({ schema }); 797 - 798 - // TypeScript enums support only string and number values so we need to fallback to types 799 - if ( 800 - enumObject.typeofItems.filter( 801 - (type) => type !== 'number' && type !== 'string', 802 - ).length 803 - ) { 804 - const node = addTypeEnum({ 805 - $ref, 806 - context, 807 - schema, 808 - }); 809 - return node; 810 - } 811 - 812 - const node = compiler.enumDeclaration({ 813 - leadingComment: parseSchemaJsDoc({ schema }), 814 - name: identifier.name, 815 - obj: enumObject.obj, 816 - }); 817 - return node; 818 - }; 819 - 820 - const arrayTypeToIdentifier = ({ 821 - context, 822 - namespace, 823 - schema, 824 - }: { 825 - context: IRContext; 826 - namespace: Array<ts.Statement>; 827 - schema: SchemaWithType<'array'>; 828 - }) => { 829 - if (!schema.items) { 830 - return compiler.typeArrayNode( 831 - compiler.keywordTypeNode({ 832 - keyword: 'unknown', 833 - }), 834 - ); 835 - } 836 - 837 - schema = deduplicateSchema({ schema }); 838 - 839 - // at least one item is guaranteed 840 - const itemTypes = schema.items!.map((item) => 841 - schemaToType({ 842 - context, 843 - namespace, 844 - schema: item, 845 - }), 846 - ); 847 - 848 - if (itemTypes.length === 1) { 849 - return compiler.typeArrayNode(itemTypes[0]); 850 - } 851 - 852 - if (schema.logicalOperator === 'and') { 853 - return compiler.typeArrayNode( 854 - compiler.typeIntersectionNode({ types: itemTypes }), 855 - ); 856 - } 857 - 858 - return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes })); 859 - }; 860 - 861 - const booleanTypeToIdentifier = ({ 862 - schema, 863 - }: { 864 - context: IRContext; 865 - namespace: Array<ts.Statement>; 866 - schema: SchemaWithType<'boolean'>; 867 - }) => { 868 - if (schema.const !== undefined) { 869 - return compiler.literalTypeNode({ 870 - literal: compiler.ots.boolean(schema.const as boolean), 871 - }); 872 - } 873 - 874 - return compiler.keywordTypeNode({ 875 - keyword: 'boolean', 876 - }); 877 - }; 878 - 879 - const enumTypeToIdentifier = ({ 880 - $ref, 881 - context, 882 - namespace, 883 - schema, 884 - }: { 885 - $ref?: string; 886 - context: IRContext; 887 - namespace: Array<ts.Statement>; 888 - schema: SchemaWithType<'enum'>; 889 - }): ts.TypeNode => { 890 - // TODO: parser - add option to inline enums 891 - if ($ref) { 892 - const isRefComponent = isRefOpenApiComponent($ref); 893 - 894 - // when enums are disabled (default), emit only reusable components 895 - // as types, otherwise the output would be broken if we skipped all enums 896 - if (!context.config.types.enums && isRefComponent) { 897 - const typeNode = addTypeEnum({ 898 - $ref, 899 - context, 900 - schema, 901 - }); 902 - if (typeNode) { 903 - context.file({ id: typesId })!.add(typeNode); 904 - } 905 - } 906 - 907 - if (context.config.types.enums === 'javascript') { 908 - const typeNode = addTypeEnum({ 909 - $ref, 910 - context, 911 - schema, 912 - }); 913 - if (typeNode) { 914 - context.file({ id: typesId })!.add(typeNode); 915 - } 916 - 917 - const objectNode = addJavaScriptEnum({ 918 - $ref, 919 - context, 920 - schema, 921 - }); 922 - if (objectNode) { 923 - context.file({ id: typesId })!.add(objectNode); 924 - } 925 - } 926 - 927 - if (context.config.types.enums === 'typescript') { 928 - const enumNode = addTypeScriptEnum({ 929 - $ref, 930 - context, 931 - schema, 932 - }); 933 - if (enumNode) { 934 - context.file({ id: typesId })!.add(enumNode); 935 - } 936 - } 937 - 938 - if (context.config.types.enums === 'typescript+namespace') { 939 - const enumNode = addTypeScriptEnum({ 940 - $ref, 941 - context, 942 - schema, 943 - }); 944 - if (enumNode) { 945 - if (isRefComponent) { 946 - context.file({ id: typesId })!.add(enumNode); 947 - } else { 948 - // emit enum inside TypeScript namespace 949 - namespace.push(enumNode); 950 - } 951 - } 952 - } 953 - } 954 - 955 - const type = schemaToType({ 956 - context, 957 - schema: { 958 - ...schema, 959 - type: undefined, 960 - }, 961 - }); 962 - return type; 963 - }; 964 - 965 - const numberTypeToIdentifier = ({ 966 - schema, 967 - }: { 968 - context: IRContext; 969 - namespace: Array<ts.Statement>; 970 - schema: SchemaWithType<'number'>; 971 - }) => { 972 - if (schema.const !== undefined) { 973 - return compiler.literalTypeNode({ 974 - literal: compiler.ots.number(schema.const as number), 975 - }); 976 - } 977 - 978 - return compiler.keywordTypeNode({ 979 - keyword: 'number', 980 - }); 981 - }; 982 - 983 - const objectTypeToIdentifier = ({ 984 - context, 985 - namespace, 986 - schema, 987 - }: { 988 - context: IRContext; 989 - namespace: Array<ts.Statement>; 990 - schema: SchemaWithType<'object'>; 991 - }) => { 992 - let indexProperty: Property | undefined; 993 - const schemaProperties: Array<Property> = []; 994 - const indexPropertyItems: Array<IRSchemaObject> = []; 995 - const required = schema.required ?? []; 996 - let hasOptionalProperties = false; 997 - 998 - for (const name in schema.properties) { 999 - const property = schema.properties[name]; 1000 - const isRequired = required.includes(name); 1001 - schemaProperties.push({ 1002 - comment: parseSchemaJsDoc({ schema: property }), 1003 - isReadOnly: property.accessScope === 'read', 1004 - isRequired, 1005 - name, 1006 - type: schemaToType({ 1007 - $ref: `${irRef}${name}`, 1008 - context, 1009 - namespace, 1010 - schema: property, 1011 - }), 1012 - }); 1013 - indexPropertyItems.push(property); 1014 - 1015 - if (!isRequired) { 1016 - hasOptionalProperties = true; 1017 - } 1018 - } 1019 - 1020 - if (schema.additionalProperties) { 1021 - indexPropertyItems.unshift(schema.additionalProperties); 1022 - 1023 - if (hasOptionalProperties) { 1024 - indexPropertyItems.push({ 1025 - type: 'void', 1026 - }); 1027 - } 1028 - 1029 - indexProperty = { 1030 - isRequired: true, 1031 - name: 'key', 1032 - type: schemaToType({ 1033 - context, 1034 - namespace, 1035 - schema: 1036 - indexPropertyItems.length === 1 1037 - ? indexPropertyItems[0] 1038 - : { 1039 - items: indexPropertyItems, 1040 - logicalOperator: 'or', 1041 - }, 1042 - }), 1043 - }; 1044 - } 1045 - 1046 - return compiler.typeInterfaceNode({ 1047 - indexProperty, 1048 - properties: schemaProperties, 1049 - useLegacyResolution: false, 1050 - }); 1051 - }; 1052 - 1053 - const stringTypeToIdentifier = ({ 1054 - schema, 1055 - }: { 1056 - context: IRContext; 1057 - namespace: Array<ts.Statement>; 1058 - schema: SchemaWithType<'string'>; 1059 - }) => { 1060 - if (schema.const !== undefined) { 1061 - return compiler.literalTypeNode({ 1062 - literal: compiler.stringLiteral({ text: schema.const as string }), 1063 - }); 1064 - } 1065 - 1066 - if (schema.format) { 1067 - if (schema.format === 'binary') { 1068 - return compiler.typeUnionNode({ 1069 - types: [ 1070 - compiler.typeReferenceNode({ 1071 - typeName: 'Blob', 1072 - }), 1073 - compiler.typeReferenceNode({ 1074 - typeName: 'File', 1075 - }), 1076 - ], 1077 - }); 1078 - } 1079 - } 1080 - 1081 - return compiler.keywordTypeNode({ 1082 - keyword: 'string', 1083 - }); 1084 - }; 1085 - 1086 - const tupleTypeToIdentifier = ({ 1087 - context, 1088 - namespace, 1089 - schema, 1090 - }: { 1091 - context: IRContext; 1092 - namespace: Array<ts.Statement>; 1093 - schema: SchemaWithType<'tuple'>; 1094 - }) => { 1095 - const itemTypes: Array<ts.TypeNode> = []; 1096 - 1097 - for (const item of schema.items ?? []) { 1098 - itemTypes.push( 1099 - schemaToType({ 1100 - context, 1101 - namespace, 1102 - schema: item, 1103 - }), 1104 - ); 1105 - } 1106 - 1107 - return compiler.typeTupleNode({ 1108 - types: itemTypes, 1109 - }); 1110 - }; 1111 - 1112 - const schemaTypeToIdentifier = ({ 1113 - $ref, 1114 - context, 1115 - namespace, 1116 - schema, 1117 - }: { 1118 - $ref?: string; 1119 - context: IRContext; 1120 - namespace: Array<ts.Statement>; 1121 - schema: IRSchemaObject; 1122 - }): ts.TypeNode => { 1123 - switch (schema.type as Required<IRSchemaObject>['type']) { 1124 - case 'array': 1125 - return arrayTypeToIdentifier({ 1126 - context, 1127 - namespace, 1128 - schema: schema as SchemaWithType<'array'>, 1129 - }); 1130 - case 'boolean': 1131 - return booleanTypeToIdentifier({ 1132 - context, 1133 - namespace, 1134 - schema: schema as SchemaWithType<'boolean'>, 1135 - }); 1136 - case 'enum': 1137 - return enumTypeToIdentifier({ 1138 - $ref, 1139 - context, 1140 - namespace, 1141 - schema: schema as SchemaWithType<'enum'>, 1142 - }); 1143 - case 'null': 1144 - return compiler.literalTypeNode({ 1145 - literal: compiler.null(), 1146 - }); 1147 - case 'number': 1148 - return numberTypeToIdentifier({ 1149 - context, 1150 - namespace, 1151 - schema: schema as SchemaWithType<'number'>, 1152 - }); 1153 - case 'object': 1154 - return objectTypeToIdentifier({ 1155 - context, 1156 - namespace, 1157 - schema: schema as SchemaWithType<'object'>, 1158 - }); 1159 - case 'string': 1160 - return stringTypeToIdentifier({ 1161 - context, 1162 - namespace, 1163 - schema: schema as SchemaWithType<'string'>, 1164 - }); 1165 - case 'tuple': 1166 - return tupleTypeToIdentifier({ 1167 - context, 1168 - namespace, 1169 - schema: schema as SchemaWithType<'tuple'>, 1170 - }); 1171 - case 'unknown': 1172 - return compiler.keywordTypeNode({ 1173 - keyword: 'unknown', 1174 - }); 1175 - case 'void': 1176 - return compiler.keywordTypeNode({ 1177 - keyword: 'undefined', 1178 - }); 1179 - } 1180 - }; 1181 - 1182 - /** 1183 - * Ensure we don't produce redundant types, e.g. string | string. 1184 - */ 1185 - const deduplicateSchema = <T extends IRSchemaObject>({ 1186 - schema, 1187 - }: { 1188 - schema: T; 1189 - }): T => { 1190 - if (!schema.items) { 1191 - return schema; 1192 - } 1193 - 1194 - const uniqueItems: Array<IRSchemaObject> = []; 1195 - const typeIds: Array<string> = []; 1196 - 1197 - for (const item of schema.items) { 1198 - // skip nested schemas for now, handle if necessary 1199 - if ( 1200 - !item.type || 1201 - item.type === 'boolean' || 1202 - item.type === 'null' || 1203 - item.type === 'number' || 1204 - item.type === 'string' || 1205 - item.type === 'unknown' || 1206 - item.type === 'void' 1207 - ) { 1208 - // const needs namespace to handle empty string values, otherwise 1209 - // fallback would equal an actual value and we would skip an item 1210 - const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`; 1211 - if (!typeIds.includes(typeId)) { 1212 - typeIds.push(typeId); 1213 - uniqueItems.push(item); 1214 - } 1215 - continue; 1216 - } 1217 - 1218 - uniqueItems.push(item); 1219 - } 1220 - 1221 - schema.items = uniqueItems; 1222 - 1223 - if ( 1224 - schema.items.length <= 1 && 1225 - schema.type !== 'array' && 1226 - schema.type !== 'enum' && 1227 - schema.type !== 'tuple' 1228 - ) { 1229 - // bring the only item up to clean up the schema 1230 - const liftedSchema = schema.items[0]; 1231 - delete schema.logicalOperator; 1232 - delete schema.items; 1233 - schema = { 1234 - ...schema, 1235 - ...liftedSchema, 1236 - }; 1237 - } 1238 - 1239 - // exclude unknown if it's the only type left 1240 - if (schema.type === 'unknown') { 1241 - return {} as T; 1242 - } 1243 - 1244 - return schema; 1245 - }; 1246 - 1247 - const irParametersToIrSchema = ({ 1248 - parameters, 1249 - }: { 1250 - parameters: Record<string, IRParameterObject>; 1251 - }): IRSchemaObject => { 1252 - const irSchema: IRSchemaObject = { 1253 - type: 'object', 1254 - }; 1255 - 1256 - if (parameters) { 1257 - const properties: Record<string, IRSchemaObject> = {}; 1258 - const required: Array<string> = []; 1259 - 1260 - for (const name in parameters) { 1261 - const parameter = parameters[name]; 1262 - 1263 - properties[name] = deduplicateSchema({ 1264 - schema: parameter.schema, 1265 - }); 1266 - 1267 - if (parameter.required) { 1268 - required.push(name); 1269 - } 1270 - } 1271 - 1272 - irSchema.properties = properties; 1273 - 1274 - if (required.length) { 1275 - irSchema.required = required; 1276 - } 1277 - } 1278 - 1279 - return irSchema; 1280 - }; 1281 - 1282 - const operationToDataType = ({ 1283 - context, 1284 - operation, 1285 - }: { 1286 - context: IRContext; 1287 - operation: IROperationObject; 1288 - }) => { 1289 - const data: IRSchemaObject = { 1290 - type: 'object', 1291 - }; 1292 - const dataRequired: Array<string> = []; 1293 - 1294 - if (operation.body) { 1295 - if (!data.properties) { 1296 - data.properties = {}; 1297 - } 1298 - 1299 - data.properties.body = operation.body.schema; 1300 - 1301 - if (operation.body.required) { 1302 - dataRequired.push('body'); 1303 - } 1304 - } 1305 - 1306 - if (operation.parameters) { 1307 - if (!data.properties) { 1308 - data.properties = {}; 1309 - } 1310 - 1311 - // TODO: parser - handle cookie parameters 1312 - 1313 - if (operation.parameters.header) { 1314 - data.properties.headers = irParametersToIrSchema({ 1315 - parameters: operation.parameters.header, 1316 - }); 1317 - 1318 - if (data.properties.headers.required) { 1319 - dataRequired.push('headers'); 1320 - } 1321 - } 1322 - 1323 - if (operation.parameters.path) { 1324 - data.properties.path = irParametersToIrSchema({ 1325 - parameters: operation.parameters.path, 1326 - }); 1327 - 1328 - if (data.properties.path.required) { 1329 - dataRequired.push('path'); 1330 - } 1331 - } 1332 - 1333 - if (operation.parameters.query) { 1334 - data.properties.query = irParametersToIrSchema({ 1335 - parameters: operation.parameters.query, 1336 - }); 1337 - 1338 - if (data.properties.query.required) { 1339 - dataRequired.push('query'); 1340 - } 1341 - } 1342 - } 1343 - 1344 - data.required = dataRequired; 1345 - 1346 - if (data.properties) { 1347 - const identifier = context.file({ id: typesId })!.identifier({ 1348 - $ref: operationDataRef({ id: operation.id }), 1349 - create: true, 1350 - namespace: 'type', 1351 - }); 1352 - const node = compiler.typeAliasDeclaration({ 1353 - exportType: true, 1354 - name: identifier.name, 1355 - type: schemaToType({ 1356 - context, 1357 - schema: data, 1358 - }), 1359 - }); 1360 - context.file({ id: typesId })!.add(node); 1361 - } 1362 - }; 1363 - 1364 - type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default'; 1365 - 1366 - const statusCodeToGroup = ({ 1367 - statusCode, 1368 - }: { 1369 - statusCode: string; 1370 - }): StatusGroup => { 1371 - switch (statusCode) { 1372 - case '1XX': 1373 - return '1XX'; 1374 - case '2XX': 1375 - return '2XX'; 1376 - case '3XX': 1377 - return '3XX'; 1378 - case '4XX': 1379 - return '4XX'; 1380 - case '5XX': 1381 - return '5XX'; 1382 - case 'default': 1383 - return 'default'; 1384 - default: 1385 - return `${statusCode[0]}XX` as StatusGroup; 1386 - } 1387 - }; 1388 - 1389 - const operationToResponseTypes = ({ 1390 - context, 1391 - operation, 1392 - }: { 1393 - context: IRContext; 1394 - operation: IROperationObject; 1395 - }) => { 1396 - if (!operation.responses) { 1397 - return; 1398 - } 1399 - 1400 - let errors: IRSchemaObject = {}; 1401 - const errorsItems: Array<IRSchemaObject> = []; 1402 - 1403 - let responses: IRSchemaObject = {}; 1404 - const responsesItems: Array<IRSchemaObject> = []; 1405 - 1406 - let defaultResponse: IRResponseObject | undefined; 1407 - 1408 - for (const name in operation.responses) { 1409 - const response = operation.responses[name]!; 1410 - 1411 - switch (statusCodeToGroup({ statusCode: name })) { 1412 - case '1XX': 1413 - case '3XX': 1414 - // TODO: parser - handle informational and redirection status codes 1415 - break; 1416 - case '2XX': 1417 - responsesItems.push(response.schema); 1418 - break; 1419 - case '4XX': 1420 - case '5XX': 1421 - errorsItems.push(response.schema); 1422 - break; 1423 - case 'default': 1424 - // store default response to be evaluated last 1425 - defaultResponse = response; 1426 - break; 1427 - } 1428 - } 1429 - 1430 - // infer default response type 1431 - if (defaultResponse) { 1432 - let inferred = false; 1433 - 1434 - // assume default is intended for success if none exists yet 1435 - if (!responsesItems.length) { 1436 - responsesItems.push(defaultResponse.schema); 1437 - inferred = true; 1438 - } 1439 - 1440 - const description = ( 1441 - defaultResponse.schema.description ?? '' 1442 - ).toLocaleLowerCase(); 1443 - const $ref = (defaultResponse.schema.$ref ?? '').toLocaleLowerCase(); 1444 - 1445 - // TODO: parser - this could be rewritten using regular expressions 1446 - const successKeywords = ['success']; 1447 - if ( 1448 - successKeywords.some( 1449 - (keyword) => description.includes(keyword) || $ref.includes(keyword), 1450 - ) 1451 - ) { 1452 - responsesItems.push(defaultResponse.schema); 1453 - inferred = true; 1454 - } 1455 - 1456 - // TODO: parser - this could be rewritten using regular expressions 1457 - const errorKeywords = ['error', 'problem']; 1458 - if ( 1459 - errorKeywords.some( 1460 - (keyword) => description.includes(keyword) || $ref.includes(keyword), 1461 - ) 1462 - ) { 1463 - errorsItems.push(defaultResponse.schema); 1464 - inferred = true; 1465 - } 1466 - 1467 - // if no keyword match, assume default schema is intended for error 1468 - if (!inferred) { 1469 - errorsItems.push(defaultResponse.schema); 1470 - } 1471 - } 1472 - 1473 - if (errorsItems.length) { 1474 - errors = addItemsToSchema({ 1475 - items: errorsItems, 1476 - mutateSchemaOneItem: true, 1477 - schema: errors, 1478 - }); 1479 - errors = deduplicateSchema({ schema: errors }); 1480 - if (Object.keys(errors).length) { 1481 - const identifier = context.file({ id: typesId })!.identifier({ 1482 - $ref: operationErrorRef({ id: operation.id }), 1483 - create: true, 1484 - namespace: 'type', 1485 - }); 1486 - const node = compiler.typeAliasDeclaration({ 1487 - exportType: true, 1488 - name: identifier.name, 1489 - type: schemaToType({ 1490 - context, 1491 - schema: errors, 1492 - }), 1493 - }); 1494 - context.file({ id: typesId })!.add(node); 1495 - } 1496 - } 1497 - 1498 - if (responsesItems.length) { 1499 - responses = addItemsToSchema({ 1500 - items: responsesItems, 1501 - mutateSchemaOneItem: true, 1502 - schema: responses, 1503 - }); 1504 - responses = deduplicateSchema({ schema: responses }); 1505 - if (Object.keys(responses).length) { 1506 - const identifier = context.file({ id: typesId })!.identifier({ 1507 - $ref: operationResponseRef({ id: operation.id }), 1508 - create: true, 1509 - namespace: 'type', 1510 - }); 1511 - const node = compiler.typeAliasDeclaration({ 1512 - exportType: true, 1513 - name: identifier.name, 1514 - type: schemaToType({ 1515 - context, 1516 - schema: responses, 1517 - }), 1518 - }); 1519 - context.file({ id: typesId })!.add(node); 1520 - } 1521 - } 1522 - }; 1523 - 1524 - const operationToType = ({ 1525 - context, 1526 - operation, 1527 - }: { 1528 - context: IRContext; 1529 - operation: IROperationObject; 1530 - }) => { 1531 - operationToDataType({ 1532 - context, 1533 - operation, 1534 - }); 1535 - 1536 - operationToResponseTypes({ 1537 - context, 1538 - operation, 1539 - }); 1540 - }; 1541 - 1542 - export const schemaToType = ({ 1543 - $ref, 1544 - context, 1545 - namespace = [], 1546 - schema, 1547 - }: { 1548 - $ref?: string; 1549 - context: IRContext; 1550 - namespace?: Array<ts.Statement>; 1551 - schema: IRSchemaObject; 1552 - }): ts.TypeNode => { 1553 - let type: ts.TypeNode | undefined; 1554 - 1555 - if (schema.$ref) { 1556 - const identifier = context.file({ id: typesId })!.identifier({ 1557 - $ref: schema.$ref, 1558 - create: true, 1559 - namespace: 'type', 1560 - }); 1561 - type = compiler.typeReferenceNode({ 1562 - typeName: identifier.name, 1563 - }); 1564 - } else if (schema.type) { 1565 - type = schemaTypeToIdentifier({ 1566 - $ref, 1567 - context, 1568 - namespace, 1569 - schema, 1570 - }); 1571 - } else if (schema.items) { 1572 - schema = deduplicateSchema({ schema }); 1573 - if (schema.items) { 1574 - const itemTypes = schema.items.map((item) => 1575 - schemaToType({ 1576 - context, 1577 - namespace, 1578 - schema: item, 1579 - }), 1580 - ); 1581 - type = 1582 - schema.logicalOperator === 'and' 1583 - ? compiler.typeIntersectionNode({ types: itemTypes }) 1584 - : compiler.typeUnionNode({ types: itemTypes }); 1585 - } else { 1586 - type = schemaToType({ 1587 - context, 1588 - namespace, 1589 - schema, 1590 - }); 1591 - } 1592 - } else { 1593 - // catch-all fallback for failed schemas 1594 - type = schemaTypeToIdentifier({ 1595 - context, 1596 - namespace, 1597 - schema: { 1598 - type: 'unknown', 1599 - }, 1600 - }); 1601 - } 1602 - 1603 - // emit nodes only if $ref points to a reusable component 1604 - if ($ref && isRefOpenApiComponent($ref)) { 1605 - // emit namespace if it has any members 1606 - if (namespace.length) { 1607 - const identifier = context.file({ id: typesId })!.identifier({ 1608 - $ref, 1609 - create: true, 1610 - namespace: 'value', 1611 - }); 1612 - const node = compiler.namespaceDeclaration({ 1613 - name: identifier.name, 1614 - statements: namespace, 1615 - }); 1616 - context.file({ id: typesId })!.add(node); 1617 - } 1618 - 1619 - // enum handler emits its own artifacts 1620 - if (schema.type !== 'enum') { 1621 - const identifier = context.file({ id: typesId })!.identifier({ 1622 - $ref, 1623 - create: true, 1624 - namespace: 'type', 1625 - }); 1626 - const node = compiler.typeAliasDeclaration({ 1627 - comment: parseSchemaJsDoc({ schema }), 1628 - exportType: true, 1629 - name: identifier.name, 1630 - type, 1631 - }); 1632 - context.file({ id: typesId })!.add(node); 1633 - } 1634 - } 1635 - 1636 - return type; 1637 - }; 1638 - 1639 603 export const generateLegacyTypes = async ({ 1640 604 client, 1641 605 files, ··· 1662 626 1663 627 processServiceTypes({ client, onNode }); 1664 628 }; 1665 - 1666 - export const generateTypes = ({ context }: { context: IRContext }): void => { 1667 - // TODO: parser - once types are a plugin, this logic can be simplified 1668 - if (!context.config.types.export) { 1669 - return; 1670 - } 1671 - 1672 - context.createFile({ 1673 - id: typesId, 1674 - path: 'types', 1675 - }); 1676 - 1677 - if (context.ir.components) { 1678 - for (const name in context.ir.components.schemas) { 1679 - const schema = context.ir.components.schemas[name]; 1680 - 1681 - schemaToType({ 1682 - $ref: `#/components/schemas/${name}`, 1683 - context, 1684 - schema, 1685 - }); 1686 - } 1687 - 1688 - for (const name in context.ir.components.parameters) { 1689 - const parameter = context.ir.components.parameters[name]; 1690 - 1691 - schemaToType({ 1692 - $ref: `#/components/parameters/${name}`, 1693 - context, 1694 - schema: parameter.schema, 1695 - }); 1696 - } 1697 - } 1698 - 1699 - // TODO: parser - once types are a plugin, this logic can be simplified 1700 - // provide config option on types to generate path types and services 1701 - // will set it to true if needed 1702 - if (context.config.services.export || context.config.types.tree) { 1703 - for (const path in context.ir.paths) { 1704 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 1705 - 1706 - if (pathItem.delete) { 1707 - operationToType({ 1708 - context, 1709 - operation: pathItem.delete, 1710 - }); 1711 - } 1712 - 1713 - if (pathItem.get) { 1714 - operationToType({ 1715 - context, 1716 - operation: pathItem.get, 1717 - }); 1718 - } 1719 - 1720 - if (pathItem.head) { 1721 - operationToType({ 1722 - context, 1723 - operation: pathItem.head, 1724 - }); 1725 - } 1726 - 1727 - if (pathItem.options) { 1728 - operationToType({ 1729 - context, 1730 - operation: pathItem.options, 1731 - }); 1732 - } 1733 - 1734 - if (pathItem.patch) { 1735 - operationToType({ 1736 - context, 1737 - operation: pathItem.patch, 1738 - }); 1739 - } 1740 - 1741 - if (pathItem.post) { 1742 - operationToType({ 1743 - context, 1744 - operation: pathItem.post, 1745 - }); 1746 - } 1747 - 1748 - if (pathItem.put) { 1749 - operationToType({ 1750 - context, 1751 - operation: pathItem.put, 1752 - }); 1753 - } 1754 - 1755 - if (pathItem.trace) { 1756 - operationToType({ 1757 - context, 1758 - operation: pathItem.trace, 1759 - }); 1760 - } 1761 - } 1762 - 1763 - // TODO: parser - document removal of tree? migrate it? 1764 - } 1765 - };
+3 -1
packages/openapi-ts/src/openApi/index.ts
··· 57 57 ); 58 58 } 59 59 60 + export type ParserOpenApiSpec = OpenApiV3_0_3 | OpenApiV3_1_0; 61 + 60 62 // TODO: parser - add JSDoc comment 61 63 export const parseExperimental = ({ 62 64 config, ··· 70 72 const context = new IRContext({ 71 73 config, 72 74 parserConfig, 73 - spec: spec as OpenApiV3_0_3 | OpenApiV3_1_0, 75 + spec: spec as ParserOpenApiSpec, 74 76 }); 75 77 76 78 switch (context.spec.openapi) {
+206
packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts
··· 1 + import { compiler } from '../../../compiler'; 2 + import type { IRContext } from '../../../ir/context'; 3 + import { 4 + ensureValidTypeScriptJavaScriptIdentifier, 5 + type ParserOpenApiSpec, 6 + } from '../../../openApi'; 7 + import type { OpenApiV3_1_0 } from '../../../openApi/3.1.0'; 8 + import type { SchemaObject as OpenApiV3_1_0SchemaObject } from '../../../openApi/3.1.0/types/spec'; 9 + 10 + const schemasId = 'schemas'; 11 + 12 + const stripSchema = ({ 13 + context, 14 + schema, 15 + }: { 16 + context: IRContext; 17 + schema: OpenApiV3_1_0SchemaObject; 18 + }) => { 19 + if (context.config.schemas.type === 'form') { 20 + if (schema.description) { 21 + delete schema.description; 22 + } 23 + 24 + if (schema['x-enum-descriptions']) { 25 + delete schema['x-enum-descriptions']; 26 + } 27 + 28 + if (schema['x-enum-varnames']) { 29 + delete schema['x-enum-varnames']; 30 + } 31 + 32 + if (schema['x-enumNames']) { 33 + delete schema['x-enumNames']; 34 + } 35 + 36 + if (schema.title) { 37 + delete schema.title; 38 + } 39 + } 40 + }; 41 + 42 + const schemaToJsonSchema2020_12 = ({ 43 + context, 44 + schema: _schema, 45 + }: { 46 + context: IRContext; 47 + schema: OpenApiV3_1_0SchemaObject; 48 + }): object => { 49 + if (Array.isArray(_schema)) { 50 + return _schema.map((item) => 51 + schemaToJsonSchema2020_12({ 52 + context, 53 + schema: item, 54 + }), 55 + ); 56 + } 57 + 58 + const schema = structuredClone(_schema); 59 + 60 + stripSchema({ context, schema }); 61 + 62 + if (schema.$ref) { 63 + // refs are encoded probably by json-schema-ref-parser, didn't investigate 64 + // further 65 + schema.$ref = decodeURIComponent(schema.$ref); 66 + } 67 + 68 + if (schema.additionalProperties) { 69 + schema.additionalProperties = schemaToJsonSchema2020_12({ 70 + context, 71 + schema: schema.additionalProperties, 72 + }); 73 + } 74 + 75 + if (schema.allOf) { 76 + schema.allOf = schema.allOf.map((item) => 77 + schemaToJsonSchema2020_12({ 78 + context, 79 + schema: item, 80 + }), 81 + ); 82 + } 83 + 84 + if (schema.anyOf) { 85 + schema.anyOf = schema.anyOf.map((item) => 86 + schemaToJsonSchema2020_12({ 87 + context, 88 + schema: item, 89 + }), 90 + ); 91 + } 92 + 93 + if (schema.items) { 94 + schema.items = schemaToJsonSchema2020_12({ 95 + context, 96 + schema: schema.items, 97 + }); 98 + } 99 + 100 + if (schema.oneOf) { 101 + schema.oneOf = schema.oneOf.map((item) => 102 + schemaToJsonSchema2020_12({ 103 + context, 104 + schema: item, 105 + }), 106 + ); 107 + } 108 + 109 + if (schema.prefixItems) { 110 + schema.prefixItems = schema.prefixItems.map((item) => 111 + schemaToJsonSchema2020_12({ 112 + context, 113 + schema: item, 114 + }), 115 + ); 116 + } 117 + 118 + if (schema.properties) { 119 + for (const name in schema.properties) { 120 + const property = schema.properties[name]; 121 + 122 + if (typeof property !== 'boolean') { 123 + schema.properties[name] = schemaToJsonSchema2020_12({ 124 + context, 125 + schema: property, 126 + }); 127 + } 128 + } 129 + } 130 + 131 + return schema; 132 + }; 133 + 134 + const schemaName = ({ 135 + context, 136 + name, 137 + schema, 138 + }: { 139 + context: IRContext; 140 + name: string; 141 + schema: OpenApiV3_1_0SchemaObject; 142 + }): string => { 143 + const validName = ensureValidTypeScriptJavaScriptIdentifier(name); 144 + 145 + if (context.config.schemas.name) { 146 + return context.config.schemas.name(validName, schema); 147 + } 148 + 149 + return `${validName}Schema`; 150 + }; 151 + 152 + const schemasV3_1_0 = (context: IRContext<OpenApiV3_1_0>) => { 153 + if (!context.spec.components) { 154 + return; 155 + } 156 + 157 + for (const name in context.spec.components.schemas) { 158 + const schema = context.spec.components.schemas[name]; 159 + const obj = schemaToJsonSchema2020_12({ 160 + context, 161 + schema, 162 + }); 163 + const statement = compiler.constVariable({ 164 + assertion: 'const', 165 + exportConst: true, 166 + expression: compiler.objectExpression({ obj }), 167 + name: schemaName({ context, name, schema }), 168 + }); 169 + context.file({ id: schemasId })!.add(statement); 170 + } 171 + }; 172 + 173 + export const generateSchemas = async ({ 174 + context, 175 + }: { 176 + context: IRContext<ParserOpenApiSpec>; 177 + }): Promise<void> => { 178 + // TODO: parser - once schemas are a plugin, this logic can be simplified 179 + if (!context.config.schemas.export) { 180 + return; 181 + } 182 + 183 + context.createFile({ 184 + id: schemasId, 185 + path: 'schemas', 186 + }); 187 + 188 + // TODO: parser - copy-pasted from experimental parser for now 189 + switch (context.spec.openapi) { 190 + case '3.0.3': 191 + // ... 192 + break; 193 + case '3.1.0': 194 + schemasV3_1_0(context as IRContext<OpenApiV3_1_0>); 195 + break; 196 + default: 197 + break; 198 + } 199 + 200 + // OpenAPI 2.0 201 + // if ('swagger' in openApi) { 202 + // Object.entries(openApi.definitions ?? {}).forEach(([name, definition]) => { 203 + // addSchema(name, definition); 204 + // }); 205 + // } 206 + };
+481
packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts
··· 1 + import type ts from 'typescript'; 2 + 3 + import { compiler } from '../../../compiler'; 4 + import type { ObjectValue } from '../../../compiler/types'; 5 + import { 6 + clientModulePath, 7 + clientOptionsTypeName, 8 + } from '../../../generate/client'; 9 + import { 10 + operationOptionsType, 11 + serviceFunctionIdentifier, 12 + } from '../../../generate/services'; 13 + import type { IRContext } from '../../../ir/context'; 14 + import type { 15 + IROperationObject, 16 + IRPathItemObject, 17 + IRPathsObject, 18 + } from '../../../ir/ir'; 19 + import { hasOperationDataRequired } from '../../../ir/operation'; 20 + import { camelCase } from '../../../utils/camelCase'; 21 + import { escapeComment } from '../../../utils/escape'; 22 + import { getServiceName } from '../../../utils/postprocess'; 23 + import { irRef } from '../../../utils/ref'; 24 + import { transformServiceName } from '../../../utils/transform'; 25 + 26 + interface OperationIRRef { 27 + /** 28 + * Operation ID 29 + */ 30 + id: string; 31 + } 32 + 33 + const operationIrRef = ({ 34 + id, 35 + type, 36 + }: OperationIRRef & { 37 + type: 'data' | 'error' | 'response'; 38 + }): string => { 39 + let affix = ''; 40 + switch (type) { 41 + case 'data': 42 + affix = 'Data'; 43 + break; 44 + case 'error': 45 + affix = 'Error'; 46 + break; 47 + case 'response': 48 + affix = 'Response'; 49 + break; 50 + } 51 + return `${irRef}${camelCase({ 52 + input: id, 53 + pascalCase: true, 54 + })}${affix}`; 55 + }; 56 + 57 + export const operationDataRef = ({ id }: OperationIRRef): string => 58 + operationIrRef({ id, type: 'data' }); 59 + 60 + export const operationErrorRef = ({ id }: OperationIRRef): string => 61 + operationIrRef({ id, type: 'error' }); 62 + 63 + export const operationResponseRef = ({ id }: OperationIRRef): string => 64 + operationIrRef({ id, type: 'response' }); 65 + 66 + const servicesId = 'services'; 67 + 68 + const checkPrerequisites = ({ context }: { context: IRContext }) => { 69 + if (!context.config.client.name) { 70 + throw new Error( 71 + '🚫 client needs to be set to generate services - which HTTP client do you want to use?', 72 + ); 73 + } 74 + 75 + if (!context.file({ id: 'types' })) { 76 + throw new Error( 77 + '🚫 types need to be exported to generate services - enable type generation', 78 + ); 79 + } 80 + }; 81 + 82 + const requestOptions = ({ 83 + context, 84 + operation, 85 + path, 86 + }: { 87 + context: IRContext; 88 + operation: IROperationObject; 89 + path: string; 90 + }) => { 91 + const file = context.file({ id: servicesId })!; 92 + const servicesOutput = file.nameWithoutExtension(); 93 + // const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}` 94 + 95 + // TODO: parser - add response transformers 96 + // const operationName = operationResponseTypeName(operation.name); 97 + // const { name: responseTransformerName } = setUniqueTypeName({ 98 + // client, 99 + // meta: { 100 + // $ref: `transformers/${operationName}`, 101 + // name: operationName, 102 + // }, 103 + // nameTransformer: operationResponseTransformerTypeName, 104 + // }); 105 + 106 + // if (responseTransformerName) { 107 + // file.import({ 108 + // // this detection could be done safer, but it shouldn't cause any issues 109 + // asType: !responseTransformerName.endsWith('Transformer'), 110 + // module: typesModule, 111 + // name: responseTransformerName, 112 + // }); 113 + // } 114 + 115 + const obj: ObjectValue[] = [{ spread: 'options' }]; 116 + 117 + if (operation.body) { 118 + switch (operation.body.type) { 119 + case 'form-data': 120 + obj.push({ spread: 'formDataBodySerializer' }); 121 + file.import({ 122 + module: clientModulePath({ 123 + config: context.config, 124 + sourceOutput: servicesOutput, 125 + }), 126 + name: 'formDataBodySerializer', 127 + }); 128 + break; 129 + case 'json': 130 + break; 131 + case 'url-search-params': 132 + obj.push({ spread: 'urlSearchParamsBodySerializer' }); 133 + file.import({ 134 + module: clientModulePath({ 135 + config: context.config, 136 + sourceOutput: servicesOutput, 137 + }), 138 + name: 'urlSearchParamsBodySerializer', 139 + }); 140 + break; 141 + } 142 + 143 + obj.push({ 144 + key: 'headers', 145 + value: [ 146 + { 147 + key: 'Content-Type', 148 + // form-data does not need Content-Type header, browser will set it automatically 149 + value: 150 + operation.body.type === 'form-data' 151 + ? null 152 + : operation.body.mediaType, 153 + }, 154 + { 155 + spread: 'options?.headers', 156 + }, 157 + ], 158 + }); 159 + } 160 + 161 + // TODO: parser - set parseAs to skip inference if every response has the same 162 + // content type. currently impossible because successes do not contain 163 + // header information 164 + 165 + obj.push({ 166 + key: 'url', 167 + value: path, 168 + }); 169 + 170 + // TODO: parser - add response transformers 171 + // if (responseTransformerName) { 172 + // obj = [ 173 + // ...obj, 174 + // { 175 + // key: 'responseTransformer', 176 + // value: responseTransformerName, 177 + // }, 178 + // ]; 179 + // } 180 + 181 + return compiler.objectExpression({ 182 + identifiers: ['responseTransformer'], 183 + obj, 184 + }); 185 + }; 186 + 187 + const generateClassServices = ({ context }: { context: IRContext }) => { 188 + const file = context.file({ id: servicesId })!; 189 + const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}`; 190 + 191 + const services = new Map<string, Array<ts.MethodDeclaration>>(); 192 + 193 + for (const path in context.ir.paths) { 194 + const pathItem = context.ir.paths[path as keyof IRPathsObject]; 195 + 196 + for (const _method in pathItem) { 197 + const method = _method as keyof IRPathItemObject; 198 + const operation = pathItem[method]!; 199 + 200 + const identifierData = context.file({ id: 'types' })!.identifier({ 201 + $ref: operationDataRef({ id: operation.id }), 202 + namespace: 'type', 203 + }); 204 + if (identifierData.name) { 205 + file.import({ 206 + // this detection could be done safer, but it shouldn't cause any issues 207 + asType: !identifierData.name.endsWith('Transformer'), 208 + module: typesModule, 209 + name: identifierData.name, 210 + }); 211 + } 212 + 213 + const identifierError = context.file({ id: 'types' })!.identifier({ 214 + $ref: operationErrorRef({ id: operation.id }), 215 + namespace: 'type', 216 + }); 217 + if (identifierError.name) { 218 + file.import({ 219 + // this detection could be done safer, but it shouldn't cause any issues 220 + asType: !identifierError.name.endsWith('Transformer'), 221 + module: typesModule, 222 + name: identifierError.name, 223 + }); 224 + } 225 + 226 + const identifierResponse = context.file({ id: 'types' })!.identifier({ 227 + $ref: operationResponseRef({ id: operation.id }), 228 + namespace: 'type', 229 + }); 230 + if (identifierResponse.name) { 231 + file.import({ 232 + // this detection could be done safer, but it shouldn't cause any issues 233 + asType: !identifierResponse.name.endsWith('Transformer'), 234 + module: typesModule, 235 + name: identifierResponse.name, 236 + }); 237 + } 238 + 239 + const node = compiler.methodDeclaration({ 240 + accessLevel: 'public', 241 + comment: [ 242 + operation.deprecated && '@deprecated', 243 + operation.summary && escapeComment(operation.summary), 244 + operation.description && escapeComment(operation.description), 245 + ], 246 + isStatic: true, 247 + name: serviceFunctionIdentifier({ 248 + config: context.config, 249 + handleIllegal: false, 250 + id: operation.id, 251 + operation, 252 + }), 253 + parameters: [ 254 + { 255 + isRequired: hasOperationDataRequired(operation), 256 + name: 'options', 257 + type: operationOptionsType({ 258 + importedType: identifierData.name, 259 + throwOnError: 'ThrowOnError', 260 + }), 261 + }, 262 + ], 263 + returnType: undefined, 264 + statements: [ 265 + compiler.returnFunctionCall({ 266 + args: [ 267 + requestOptions({ 268 + context, 269 + operation, 270 + path, 271 + }), 272 + ], 273 + name: `(options?.client ?? client).${method}`, 274 + types: [ 275 + identifierResponse.name || 'unknown', 276 + identifierError.name || 'unknown', 277 + 'ThrowOnError', 278 + ], 279 + }), 280 + ], 281 + types: [ 282 + { 283 + default: false, 284 + extends: 'boolean', 285 + name: 'ThrowOnError', 286 + }, 287 + ], 288 + }); 289 + 290 + const uniqueTags = Array.from(new Set(operation.tags)); 291 + if (!uniqueTags.length) { 292 + uniqueTags.push('default'); 293 + } 294 + 295 + for (const tag of uniqueTags) { 296 + const serviceName = getServiceName(tag); 297 + const nodes = services.get(serviceName) ?? []; 298 + nodes.push(node); 299 + services.set(serviceName, nodes); 300 + } 301 + } 302 + } 303 + 304 + for (const [serviceName, nodes] of services) { 305 + const node = compiler.classDeclaration({ 306 + decorator: undefined, 307 + members: nodes, 308 + name: transformServiceName({ 309 + config: context.config, 310 + name: serviceName, 311 + }), 312 + }); 313 + file.add(node); 314 + } 315 + }; 316 + 317 + const generateFlatServices = ({ context }: { context: IRContext }) => { 318 + const file = context.file({ id: servicesId })!; 319 + const typesModule = `./${context.file({ id: 'types' })!.nameWithoutExtension()}`; 320 + 321 + for (const path in context.ir.paths) { 322 + const pathItem = context.ir.paths[path as keyof IRPathsObject]; 323 + 324 + for (const _method in pathItem) { 325 + const method = _method as keyof IRPathItemObject; 326 + const operation = pathItem[method]!; 327 + 328 + const identifierData = context.file({ id: 'types' })!.identifier({ 329 + $ref: operationDataRef({ id: operation.id }), 330 + namespace: 'type', 331 + }); 332 + if (identifierData.name) { 333 + file.import({ 334 + // this detection could be done safer, but it shouldn't cause any issues 335 + asType: !identifierData.name.endsWith('Transformer'), 336 + module: typesModule, 337 + name: identifierData.name, 338 + }); 339 + } 340 + 341 + const identifierError = context.file({ id: 'types' })!.identifier({ 342 + $ref: operationErrorRef({ id: operation.id }), 343 + namespace: 'type', 344 + }); 345 + if (identifierError.name) { 346 + file.import({ 347 + // this detection could be done safer, but it shouldn't cause any issues 348 + asType: !identifierError.name.endsWith('Transformer'), 349 + module: typesModule, 350 + name: identifierError.name, 351 + }); 352 + } 353 + 354 + const identifierResponse = context.file({ id: 'types' })!.identifier({ 355 + $ref: operationResponseRef({ id: operation.id }), 356 + namespace: 'type', 357 + }); 358 + if (identifierResponse.name) { 359 + file.import({ 360 + // this detection could be done safer, but it shouldn't cause any issues 361 + asType: !identifierResponse.name.endsWith('Transformer'), 362 + module: typesModule, 363 + name: identifierResponse.name, 364 + }); 365 + } 366 + 367 + const node = compiler.constVariable({ 368 + comment: [ 369 + operation.deprecated && '@deprecated', 370 + operation.summary && escapeComment(operation.summary), 371 + operation.description && escapeComment(operation.description), 372 + ], 373 + exportConst: true, 374 + expression: compiler.arrowFunction({ 375 + parameters: [ 376 + { 377 + isRequired: hasOperationDataRequired(operation), 378 + name: 'options', 379 + type: operationOptionsType({ 380 + importedType: identifierData.name, 381 + throwOnError: 'ThrowOnError', 382 + }), 383 + }, 384 + ], 385 + returnType: undefined, 386 + statements: [ 387 + compiler.returnFunctionCall({ 388 + args: [ 389 + requestOptions({ 390 + context, 391 + operation, 392 + path, 393 + }), 394 + ], 395 + name: `(options?.client ?? client).${method}`, 396 + types: [ 397 + identifierResponse.name || 'unknown', 398 + identifierError.name || 'unknown', 399 + 'ThrowOnError', 400 + ], 401 + }), 402 + ], 403 + types: [ 404 + { 405 + default: false, 406 + extends: 'boolean', 407 + name: 'ThrowOnError', 408 + }, 409 + ], 410 + }), 411 + name: serviceFunctionIdentifier({ 412 + config: context.config, 413 + handleIllegal: true, 414 + id: operation.id, 415 + operation, 416 + }), 417 + }); 418 + file.add(node); 419 + } 420 + } 421 + }; 422 + 423 + export const generateServices = ({ context }: { context: IRContext }) => { 424 + // TODO: parser - once services are a plugin, this logic can be simplified 425 + if (!context.config.services.export) { 426 + return; 427 + } 428 + 429 + checkPrerequisites({ context }); 430 + 431 + const file = context.createFile({ 432 + id: servicesId, 433 + path: 'services', 434 + }); 435 + const servicesOutput = file.nameWithoutExtension(); 436 + 437 + // import required packages and core files 438 + file.import({ 439 + module: clientModulePath({ 440 + config: context.config, 441 + sourceOutput: servicesOutput, 442 + }), 443 + name: 'createClient', 444 + }); 445 + file.import({ 446 + module: clientModulePath({ 447 + config: context.config, 448 + sourceOutput: servicesOutput, 449 + }), 450 + name: 'createConfig', 451 + }); 452 + file.import({ 453 + asType: true, 454 + module: clientModulePath({ 455 + config: context.config, 456 + sourceOutput: servicesOutput, 457 + }), 458 + name: clientOptionsTypeName(), 459 + }); 460 + 461 + // define client first 462 + const statement = compiler.constVariable({ 463 + exportConst: true, 464 + expression: compiler.callExpression({ 465 + functionName: 'createClient', 466 + parameters: [ 467 + compiler.callExpression({ 468 + functionName: 'createConfig', 469 + }), 470 + ], 471 + }), 472 + name: 'client', 473 + }); 474 + file.add(statement); 475 + 476 + if (context.config.services.asClass) { 477 + generateClassServices({ context }); 478 + } else { 479 + generateFlatServices({ context }); 480 + } 481 + };
+1139
packages/openapi-ts/src/plugins/@hey-api/types/plugin.ts
··· 1 + import type ts from 'typescript'; 2 + 3 + import type { Property } from '../../../compiler'; 4 + import { compiler } from '../../../compiler'; 5 + import type { IRContext } from '../../../ir/context'; 6 + import type { 7 + IROperationObject, 8 + IRParameterObject, 9 + IRPathsObject, 10 + IRResponseObject, 11 + IRSchemaObject, 12 + } from '../../../ir/ir'; 13 + import { addItemsToSchema } from '../../../ir/utils'; 14 + import { ensureValidTypeScriptJavaScriptIdentifier } from '../../../openApi'; 15 + import { escapeComment } from '../../../utils/escape'; 16 + import { irRef, isRefOpenApiComponent } from '../../../utils/ref'; 17 + import { 18 + operationDataRef, 19 + operationErrorRef, 20 + operationResponseRef, 21 + } from '../services/plugin'; 22 + 23 + interface SchemaWithType<T extends Required<IRSchemaObject>['type']> 24 + extends Omit<IRSchemaObject, 'type'> { 25 + type: Extract<Required<IRSchemaObject>['type'], T>; 26 + } 27 + 28 + const typesId = 'types'; 29 + 30 + const parseSchemaJsDoc = ({ schema }: { schema: IRSchemaObject }) => { 31 + const comments = [ 32 + schema.description && escapeComment(schema.description), 33 + schema.deprecated && '@deprecated', 34 + ]; 35 + return comments; 36 + }; 37 + 38 + const addJavaScriptEnum = ({ 39 + $ref, 40 + context, 41 + schema, 42 + }: { 43 + $ref: string; 44 + context: IRContext; 45 + schema: SchemaWithType<'enum'>; 46 + }) => { 47 + const identifier = context.file({ id: typesId })!.identifier({ 48 + $ref, 49 + create: true, 50 + namespace: 'value', 51 + }); 52 + 53 + // TODO: parser - this is the old parser behavior where we would NOT 54 + // print nested enum identifiers if they already exist. This is a 55 + // blocker for referencing these identifiers within the file as 56 + // we cannot guarantee just because they have a duplicate identifier, 57 + // they have a duplicate value. 58 + if (!identifier.created) { 59 + return; 60 + } 61 + 62 + const enumObject = schemaToEnumObject({ schema }); 63 + 64 + const expression = compiler.objectExpression({ 65 + multiLine: true, 66 + obj: enumObject.obj, 67 + }); 68 + const node = compiler.constVariable({ 69 + assertion: 'const', 70 + comment: parseSchemaJsDoc({ schema }), 71 + exportConst: true, 72 + expression, 73 + name: identifier.name, 74 + }); 75 + return node; 76 + }; 77 + 78 + const schemaToEnumObject = ({ schema }: { schema: IRSchemaObject }) => { 79 + const typeofItems: Array< 80 + | 'string' 81 + | 'number' 82 + | 'bigint' 83 + | 'boolean' 84 + | 'symbol' 85 + | 'undefined' 86 + | 'object' 87 + | 'function' 88 + > = []; 89 + 90 + const obj = (schema.items ?? []).map((item) => { 91 + const typeOfItemConst = typeof item.const; 92 + 93 + if (!typeofItems.includes(typeOfItemConst)) { 94 + typeofItems.push(typeOfItemConst); 95 + } 96 + 97 + let key; 98 + if (item.title) { 99 + key = item.title; 100 + } else if (typeOfItemConst === 'number') { 101 + key = `_${item.const}`; 102 + } else if (typeOfItemConst === 'boolean') { 103 + const valid = typeOfItemConst ? 'true' : 'false'; 104 + key = valid.toLocaleUpperCase(); 105 + } else { 106 + let valid = ensureValidTypeScriptJavaScriptIdentifier( 107 + item.const as string, 108 + ); 109 + if (!valid) { 110 + // TODO: parser - abstract empty string handling 111 + valid = 'empty_string'; 112 + } 113 + key = valid.toLocaleUpperCase(); 114 + } 115 + return { 116 + comments: parseSchemaJsDoc({ schema: item }), 117 + key, 118 + value: item.const, 119 + }; 120 + }); 121 + 122 + return { 123 + obj, 124 + typeofItems, 125 + }; 126 + }; 127 + 128 + const addTypeEnum = ({ 129 + $ref, 130 + context, 131 + schema, 132 + }: { 133 + $ref: string; 134 + context: IRContext; 135 + schema: SchemaWithType<'enum'>; 136 + }) => { 137 + const identifier = context.file({ id: typesId })!.identifier({ 138 + $ref, 139 + create: true, 140 + namespace: 'type', 141 + }); 142 + 143 + // TODO: parser - this is the old parser behavior where we would NOT 144 + // print nested enum identifiers if they already exist. This is a 145 + // blocker for referencing these identifiers within the file as 146 + // we cannot guarantee just because they have a duplicate identifier, 147 + // they have a duplicate value. 148 + if ( 149 + !identifier.created && 150 + context.config.types.enums !== 'typescript+namespace' 151 + ) { 152 + return; 153 + } 154 + 155 + const node = compiler.typeAliasDeclaration({ 156 + comment: parseSchemaJsDoc({ schema }), 157 + exportType: true, 158 + name: identifier.name, 159 + type: schemaToType({ 160 + context, 161 + schema: { 162 + ...schema, 163 + type: undefined, 164 + }, 165 + }), 166 + }); 167 + return node; 168 + }; 169 + 170 + const addTypeScriptEnum = ({ 171 + $ref, 172 + context, 173 + schema, 174 + }: { 175 + $ref: string; 176 + context: IRContext; 177 + schema: SchemaWithType<'enum'>; 178 + }) => { 179 + const identifier = context.file({ id: typesId })!.identifier({ 180 + $ref, 181 + create: true, 182 + namespace: 'value', 183 + }); 184 + 185 + // TODO: parser - this is the old parser behavior where we would NOT 186 + // print nested enum identifiers if they already exist. This is a 187 + // blocker for referencing these identifiers within the file as 188 + // we cannot guarantee just because they have a duplicate identifier, 189 + // they have a duplicate value. 190 + if ( 191 + !identifier.created && 192 + context.config.types.enums !== 'typescript+namespace' 193 + ) { 194 + return; 195 + } 196 + 197 + const enumObject = schemaToEnumObject({ schema }); 198 + 199 + // TypeScript enums support only string and number values so we need to fallback to types 200 + if ( 201 + enumObject.typeofItems.filter( 202 + (type) => type !== 'number' && type !== 'string', 203 + ).length 204 + ) { 205 + const node = addTypeEnum({ 206 + $ref, 207 + context, 208 + schema, 209 + }); 210 + return node; 211 + } 212 + 213 + const node = compiler.enumDeclaration({ 214 + leadingComment: parseSchemaJsDoc({ schema }), 215 + name: identifier.name, 216 + obj: enumObject.obj, 217 + }); 218 + return node; 219 + }; 220 + 221 + const arrayTypeToIdentifier = ({ 222 + context, 223 + namespace, 224 + schema, 225 + }: { 226 + context: IRContext; 227 + namespace: Array<ts.Statement>; 228 + schema: SchemaWithType<'array'>; 229 + }) => { 230 + if (!schema.items) { 231 + return compiler.typeArrayNode( 232 + compiler.keywordTypeNode({ 233 + keyword: 'unknown', 234 + }), 235 + ); 236 + } 237 + 238 + schema = deduplicateSchema({ schema }); 239 + 240 + // at least one item is guaranteed 241 + const itemTypes = schema.items!.map((item) => 242 + schemaToType({ 243 + context, 244 + namespace, 245 + schema: item, 246 + }), 247 + ); 248 + 249 + if (itemTypes.length === 1) { 250 + return compiler.typeArrayNode(itemTypes[0]); 251 + } 252 + 253 + if (schema.logicalOperator === 'and') { 254 + return compiler.typeArrayNode( 255 + compiler.typeIntersectionNode({ types: itemTypes }), 256 + ); 257 + } 258 + 259 + return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes })); 260 + }; 261 + 262 + const booleanTypeToIdentifier = ({ 263 + schema, 264 + }: { 265 + context: IRContext; 266 + namespace: Array<ts.Statement>; 267 + schema: SchemaWithType<'boolean'>; 268 + }) => { 269 + if (schema.const !== undefined) { 270 + return compiler.literalTypeNode({ 271 + literal: compiler.ots.boolean(schema.const as boolean), 272 + }); 273 + } 274 + 275 + return compiler.keywordTypeNode({ 276 + keyword: 'boolean', 277 + }); 278 + }; 279 + 280 + const enumTypeToIdentifier = ({ 281 + $ref, 282 + context, 283 + namespace, 284 + schema, 285 + }: { 286 + $ref?: string; 287 + context: IRContext; 288 + namespace: Array<ts.Statement>; 289 + schema: SchemaWithType<'enum'>; 290 + }): ts.TypeNode => { 291 + // TODO: parser - add option to inline enums 292 + if ($ref) { 293 + const isRefComponent = isRefOpenApiComponent($ref); 294 + 295 + // when enums are disabled (default), emit only reusable components 296 + // as types, otherwise the output would be broken if we skipped all enums 297 + if (!context.config.types.enums && isRefComponent) { 298 + const typeNode = addTypeEnum({ 299 + $ref, 300 + context, 301 + schema, 302 + }); 303 + if (typeNode) { 304 + context.file({ id: typesId })!.add(typeNode); 305 + } 306 + } 307 + 308 + if (context.config.types.enums === 'javascript') { 309 + const typeNode = addTypeEnum({ 310 + $ref, 311 + context, 312 + schema, 313 + }); 314 + if (typeNode) { 315 + context.file({ id: typesId })!.add(typeNode); 316 + } 317 + 318 + const objectNode = addJavaScriptEnum({ 319 + $ref, 320 + context, 321 + schema, 322 + }); 323 + if (objectNode) { 324 + context.file({ id: typesId })!.add(objectNode); 325 + } 326 + } 327 + 328 + if (context.config.types.enums === 'typescript') { 329 + const enumNode = addTypeScriptEnum({ 330 + $ref, 331 + context, 332 + schema, 333 + }); 334 + if (enumNode) { 335 + context.file({ id: typesId })!.add(enumNode); 336 + } 337 + } 338 + 339 + if (context.config.types.enums === 'typescript+namespace') { 340 + const enumNode = addTypeScriptEnum({ 341 + $ref, 342 + context, 343 + schema, 344 + }); 345 + if (enumNode) { 346 + if (isRefComponent) { 347 + context.file({ id: typesId })!.add(enumNode); 348 + } else { 349 + // emit enum inside TypeScript namespace 350 + namespace.push(enumNode); 351 + } 352 + } 353 + } 354 + } 355 + 356 + const type = schemaToType({ 357 + context, 358 + schema: { 359 + ...schema, 360 + type: undefined, 361 + }, 362 + }); 363 + return type; 364 + }; 365 + 366 + const numberTypeToIdentifier = ({ 367 + schema, 368 + }: { 369 + context: IRContext; 370 + namespace: Array<ts.Statement>; 371 + schema: SchemaWithType<'number'>; 372 + }) => { 373 + if (schema.const !== undefined) { 374 + return compiler.literalTypeNode({ 375 + literal: compiler.ots.number(schema.const as number), 376 + }); 377 + } 378 + 379 + return compiler.keywordTypeNode({ 380 + keyword: 'number', 381 + }); 382 + }; 383 + 384 + const objectTypeToIdentifier = ({ 385 + context, 386 + namespace, 387 + schema, 388 + }: { 389 + context: IRContext; 390 + namespace: Array<ts.Statement>; 391 + schema: SchemaWithType<'object'>; 392 + }) => { 393 + let indexProperty: Property | undefined; 394 + const schemaProperties: Array<Property> = []; 395 + const indexPropertyItems: Array<IRSchemaObject> = []; 396 + const required = schema.required ?? []; 397 + let hasOptionalProperties = false; 398 + 399 + for (const name in schema.properties) { 400 + const property = schema.properties[name]; 401 + const isRequired = required.includes(name); 402 + schemaProperties.push({ 403 + comment: parseSchemaJsDoc({ schema: property }), 404 + isReadOnly: property.accessScope === 'read', 405 + isRequired, 406 + name, 407 + type: schemaToType({ 408 + $ref: `${irRef}${name}`, 409 + context, 410 + namespace, 411 + schema: property, 412 + }), 413 + }); 414 + indexPropertyItems.push(property); 415 + 416 + if (!isRequired) { 417 + hasOptionalProperties = true; 418 + } 419 + } 420 + 421 + if (schema.additionalProperties) { 422 + indexPropertyItems.unshift(schema.additionalProperties); 423 + 424 + if (hasOptionalProperties) { 425 + indexPropertyItems.push({ 426 + type: 'void', 427 + }); 428 + } 429 + 430 + indexProperty = { 431 + isRequired: true, 432 + name: 'key', 433 + type: schemaToType({ 434 + context, 435 + namespace, 436 + schema: 437 + indexPropertyItems.length === 1 438 + ? indexPropertyItems[0] 439 + : { 440 + items: indexPropertyItems, 441 + logicalOperator: 'or', 442 + }, 443 + }), 444 + }; 445 + } 446 + 447 + return compiler.typeInterfaceNode({ 448 + indexProperty, 449 + properties: schemaProperties, 450 + useLegacyResolution: false, 451 + }); 452 + }; 453 + 454 + const stringTypeToIdentifier = ({ 455 + schema, 456 + }: { 457 + context: IRContext; 458 + namespace: Array<ts.Statement>; 459 + schema: SchemaWithType<'string'>; 460 + }) => { 461 + if (schema.const !== undefined) { 462 + return compiler.literalTypeNode({ 463 + literal: compiler.stringLiteral({ text: schema.const as string }), 464 + }); 465 + } 466 + 467 + if (schema.format) { 468 + if (schema.format === 'binary') { 469 + return compiler.typeUnionNode({ 470 + types: [ 471 + compiler.typeReferenceNode({ 472 + typeName: 'Blob', 473 + }), 474 + compiler.typeReferenceNode({ 475 + typeName: 'File', 476 + }), 477 + ], 478 + }); 479 + } 480 + } 481 + 482 + return compiler.keywordTypeNode({ 483 + keyword: 'string', 484 + }); 485 + }; 486 + 487 + const tupleTypeToIdentifier = ({ 488 + context, 489 + namespace, 490 + schema, 491 + }: { 492 + context: IRContext; 493 + namespace: Array<ts.Statement>; 494 + schema: SchemaWithType<'tuple'>; 495 + }) => { 496 + const itemTypes: Array<ts.TypeNode> = []; 497 + 498 + for (const item of schema.items ?? []) { 499 + itemTypes.push( 500 + schemaToType({ 501 + context, 502 + namespace, 503 + schema: item, 504 + }), 505 + ); 506 + } 507 + 508 + return compiler.typeTupleNode({ 509 + types: itemTypes, 510 + }); 511 + }; 512 + 513 + const schemaTypeToIdentifier = ({ 514 + $ref, 515 + context, 516 + namespace, 517 + schema, 518 + }: { 519 + $ref?: string; 520 + context: IRContext; 521 + namespace: Array<ts.Statement>; 522 + schema: IRSchemaObject; 523 + }): ts.TypeNode => { 524 + switch (schema.type as Required<IRSchemaObject>['type']) { 525 + case 'array': 526 + return arrayTypeToIdentifier({ 527 + context, 528 + namespace, 529 + schema: schema as SchemaWithType<'array'>, 530 + }); 531 + case 'boolean': 532 + return booleanTypeToIdentifier({ 533 + context, 534 + namespace, 535 + schema: schema as SchemaWithType<'boolean'>, 536 + }); 537 + case 'enum': 538 + return enumTypeToIdentifier({ 539 + $ref, 540 + context, 541 + namespace, 542 + schema: schema as SchemaWithType<'enum'>, 543 + }); 544 + case 'null': 545 + return compiler.literalTypeNode({ 546 + literal: compiler.null(), 547 + }); 548 + case 'number': 549 + return numberTypeToIdentifier({ 550 + context, 551 + namespace, 552 + schema: schema as SchemaWithType<'number'>, 553 + }); 554 + case 'object': 555 + return objectTypeToIdentifier({ 556 + context, 557 + namespace, 558 + schema: schema as SchemaWithType<'object'>, 559 + }); 560 + case 'string': 561 + return stringTypeToIdentifier({ 562 + context, 563 + namespace, 564 + schema: schema as SchemaWithType<'string'>, 565 + }); 566 + case 'tuple': 567 + return tupleTypeToIdentifier({ 568 + context, 569 + namespace, 570 + schema: schema as SchemaWithType<'tuple'>, 571 + }); 572 + case 'unknown': 573 + return compiler.keywordTypeNode({ 574 + keyword: 'unknown', 575 + }); 576 + case 'void': 577 + return compiler.keywordTypeNode({ 578 + keyword: 'undefined', 579 + }); 580 + } 581 + }; 582 + 583 + /** 584 + * Ensure we don't produce redundant types, e.g. string | string. 585 + */ 586 + const deduplicateSchema = <T extends IRSchemaObject>({ 587 + schema, 588 + }: { 589 + schema: T; 590 + }): T => { 591 + if (!schema.items) { 592 + return schema; 593 + } 594 + 595 + const uniqueItems: Array<IRSchemaObject> = []; 596 + const typeIds: Array<string> = []; 597 + 598 + for (const item of schema.items) { 599 + // skip nested schemas for now, handle if necessary 600 + if ( 601 + !item.type || 602 + item.type === 'boolean' || 603 + item.type === 'null' || 604 + item.type === 'number' || 605 + item.type === 'string' || 606 + item.type === 'unknown' || 607 + item.type === 'void' 608 + ) { 609 + // const needs namespace to handle empty string values, otherwise 610 + // fallback would equal an actual value and we would skip an item 611 + const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`; 612 + if (!typeIds.includes(typeId)) { 613 + typeIds.push(typeId); 614 + uniqueItems.push(item); 615 + } 616 + continue; 617 + } 618 + 619 + uniqueItems.push(item); 620 + } 621 + 622 + schema.items = uniqueItems; 623 + 624 + if ( 625 + schema.items.length <= 1 && 626 + schema.type !== 'array' && 627 + schema.type !== 'enum' && 628 + schema.type !== 'tuple' 629 + ) { 630 + // bring the only item up to clean up the schema 631 + const liftedSchema = schema.items[0]; 632 + delete schema.logicalOperator; 633 + delete schema.items; 634 + schema = { 635 + ...schema, 636 + ...liftedSchema, 637 + }; 638 + } 639 + 640 + // exclude unknown if it's the only type left 641 + if (schema.type === 'unknown') { 642 + return {} as T; 643 + } 644 + 645 + return schema; 646 + }; 647 + 648 + const irParametersToIrSchema = ({ 649 + parameters, 650 + }: { 651 + parameters: Record<string, IRParameterObject>; 652 + }): IRSchemaObject => { 653 + const irSchema: IRSchemaObject = { 654 + type: 'object', 655 + }; 656 + 657 + if (parameters) { 658 + const properties: Record<string, IRSchemaObject> = {}; 659 + const required: Array<string> = []; 660 + 661 + for (const name in parameters) { 662 + const parameter = parameters[name]; 663 + 664 + properties[name] = deduplicateSchema({ 665 + schema: parameter.schema, 666 + }); 667 + 668 + if (parameter.required) { 669 + required.push(name); 670 + } 671 + } 672 + 673 + irSchema.properties = properties; 674 + 675 + if (required.length) { 676 + irSchema.required = required; 677 + } 678 + } 679 + 680 + return irSchema; 681 + }; 682 + 683 + const operationToDataType = ({ 684 + context, 685 + operation, 686 + }: { 687 + context: IRContext; 688 + operation: IROperationObject; 689 + }) => { 690 + const data: IRSchemaObject = { 691 + type: 'object', 692 + }; 693 + const dataRequired: Array<string> = []; 694 + 695 + if (operation.body) { 696 + if (!data.properties) { 697 + data.properties = {}; 698 + } 699 + 700 + data.properties.body = operation.body.schema; 701 + 702 + if (operation.body.required) { 703 + dataRequired.push('body'); 704 + } 705 + } 706 + 707 + if (operation.parameters) { 708 + if (!data.properties) { 709 + data.properties = {}; 710 + } 711 + 712 + // TODO: parser - handle cookie parameters 713 + 714 + if (operation.parameters.header) { 715 + data.properties.headers = irParametersToIrSchema({ 716 + parameters: operation.parameters.header, 717 + }); 718 + 719 + if (data.properties.headers.required) { 720 + dataRequired.push('headers'); 721 + } 722 + } 723 + 724 + if (operation.parameters.path) { 725 + data.properties.path = irParametersToIrSchema({ 726 + parameters: operation.parameters.path, 727 + }); 728 + 729 + if (data.properties.path.required) { 730 + dataRequired.push('path'); 731 + } 732 + } 733 + 734 + if (operation.parameters.query) { 735 + data.properties.query = irParametersToIrSchema({ 736 + parameters: operation.parameters.query, 737 + }); 738 + 739 + if (data.properties.query.required) { 740 + dataRequired.push('query'); 741 + } 742 + } 743 + } 744 + 745 + data.required = dataRequired; 746 + 747 + if (data.properties) { 748 + const identifier = context.file({ id: typesId })!.identifier({ 749 + $ref: operationDataRef({ id: operation.id }), 750 + create: true, 751 + namespace: 'type', 752 + }); 753 + const node = compiler.typeAliasDeclaration({ 754 + exportType: true, 755 + name: identifier.name, 756 + type: schemaToType({ 757 + context, 758 + schema: data, 759 + }), 760 + }); 761 + context.file({ id: typesId })!.add(node); 762 + } 763 + }; 764 + 765 + type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default'; 766 + 767 + const statusCodeToGroup = ({ 768 + statusCode, 769 + }: { 770 + statusCode: string; 771 + }): StatusGroup => { 772 + switch (statusCode) { 773 + case '1XX': 774 + return '1XX'; 775 + case '2XX': 776 + return '2XX'; 777 + case '3XX': 778 + return '3XX'; 779 + case '4XX': 780 + return '4XX'; 781 + case '5XX': 782 + return '5XX'; 783 + case 'default': 784 + return 'default'; 785 + default: 786 + return `${statusCode[0]}XX` as StatusGroup; 787 + } 788 + }; 789 + 790 + const operationToResponseTypes = ({ 791 + context, 792 + operation, 793 + }: { 794 + context: IRContext; 795 + operation: IROperationObject; 796 + }) => { 797 + if (!operation.responses) { 798 + return; 799 + } 800 + 801 + let errors: IRSchemaObject = {}; 802 + const errorsItems: Array<IRSchemaObject> = []; 803 + 804 + let responses: IRSchemaObject = {}; 805 + const responsesItems: Array<IRSchemaObject> = []; 806 + 807 + let defaultResponse: IRResponseObject | undefined; 808 + 809 + for (const name in operation.responses) { 810 + const response = operation.responses[name]!; 811 + 812 + switch (statusCodeToGroup({ statusCode: name })) { 813 + case '1XX': 814 + case '3XX': 815 + // TODO: parser - handle informational and redirection status codes 816 + break; 817 + case '2XX': 818 + responsesItems.push(response.schema); 819 + break; 820 + case '4XX': 821 + case '5XX': 822 + errorsItems.push(response.schema); 823 + break; 824 + case 'default': 825 + // store default response to be evaluated last 826 + defaultResponse = response; 827 + break; 828 + } 829 + } 830 + 831 + // infer default response type 832 + if (defaultResponse) { 833 + let inferred = false; 834 + 835 + // assume default is intended for success if none exists yet 836 + if (!responsesItems.length) { 837 + responsesItems.push(defaultResponse.schema); 838 + inferred = true; 839 + } 840 + 841 + const description = ( 842 + defaultResponse.schema.description ?? '' 843 + ).toLocaleLowerCase(); 844 + const $ref = (defaultResponse.schema.$ref ?? '').toLocaleLowerCase(); 845 + 846 + // TODO: parser - this could be rewritten using regular expressions 847 + const successKeywords = ['success']; 848 + if ( 849 + successKeywords.some( 850 + (keyword) => description.includes(keyword) || $ref.includes(keyword), 851 + ) 852 + ) { 853 + responsesItems.push(defaultResponse.schema); 854 + inferred = true; 855 + } 856 + 857 + // TODO: parser - this could be rewritten using regular expressions 858 + const errorKeywords = ['error', 'problem']; 859 + if ( 860 + errorKeywords.some( 861 + (keyword) => description.includes(keyword) || $ref.includes(keyword), 862 + ) 863 + ) { 864 + errorsItems.push(defaultResponse.schema); 865 + inferred = true; 866 + } 867 + 868 + // if no keyword match, assume default schema is intended for error 869 + if (!inferred) { 870 + errorsItems.push(defaultResponse.schema); 871 + } 872 + } 873 + 874 + if (errorsItems.length) { 875 + errors = addItemsToSchema({ 876 + items: errorsItems, 877 + mutateSchemaOneItem: true, 878 + schema: errors, 879 + }); 880 + errors = deduplicateSchema({ schema: errors }); 881 + if (Object.keys(errors).length) { 882 + const identifier = context.file({ id: typesId })!.identifier({ 883 + $ref: operationErrorRef({ id: operation.id }), 884 + create: true, 885 + namespace: 'type', 886 + }); 887 + const node = compiler.typeAliasDeclaration({ 888 + exportType: true, 889 + name: identifier.name, 890 + type: schemaToType({ 891 + context, 892 + schema: errors, 893 + }), 894 + }); 895 + context.file({ id: typesId })!.add(node); 896 + } 897 + } 898 + 899 + if (responsesItems.length) { 900 + responses = addItemsToSchema({ 901 + items: responsesItems, 902 + mutateSchemaOneItem: true, 903 + schema: responses, 904 + }); 905 + responses = deduplicateSchema({ schema: responses }); 906 + if (Object.keys(responses).length) { 907 + const identifier = context.file({ id: typesId })!.identifier({ 908 + $ref: operationResponseRef({ id: operation.id }), 909 + create: true, 910 + namespace: 'type', 911 + }); 912 + const node = compiler.typeAliasDeclaration({ 913 + exportType: true, 914 + name: identifier.name, 915 + type: schemaToType({ 916 + context, 917 + schema: responses, 918 + }), 919 + }); 920 + context.file({ id: typesId })!.add(node); 921 + } 922 + } 923 + }; 924 + 925 + const operationToType = ({ 926 + context, 927 + operation, 928 + }: { 929 + context: IRContext; 930 + operation: IROperationObject; 931 + }) => { 932 + operationToDataType({ 933 + context, 934 + operation, 935 + }); 936 + 937 + operationToResponseTypes({ 938 + context, 939 + operation, 940 + }); 941 + }; 942 + 943 + export const schemaToType = ({ 944 + $ref, 945 + context, 946 + namespace = [], 947 + schema, 948 + }: { 949 + $ref?: string; 950 + context: IRContext; 951 + namespace?: Array<ts.Statement>; 952 + schema: IRSchemaObject; 953 + }): ts.TypeNode => { 954 + let type: ts.TypeNode | undefined; 955 + 956 + if (schema.$ref) { 957 + const identifier = context.file({ id: typesId })!.identifier({ 958 + $ref: schema.$ref, 959 + create: true, 960 + namespace: 'type', 961 + }); 962 + type = compiler.typeReferenceNode({ 963 + typeName: identifier.name, 964 + }); 965 + } else if (schema.type) { 966 + type = schemaTypeToIdentifier({ 967 + $ref, 968 + context, 969 + namespace, 970 + schema, 971 + }); 972 + } else if (schema.items) { 973 + schema = deduplicateSchema({ schema }); 974 + if (schema.items) { 975 + const itemTypes = schema.items.map((item) => 976 + schemaToType({ 977 + context, 978 + namespace, 979 + schema: item, 980 + }), 981 + ); 982 + type = 983 + schema.logicalOperator === 'and' 984 + ? compiler.typeIntersectionNode({ types: itemTypes }) 985 + : compiler.typeUnionNode({ types: itemTypes }); 986 + } else { 987 + type = schemaToType({ 988 + context, 989 + namespace, 990 + schema, 991 + }); 992 + } 993 + } else { 994 + // catch-all fallback for failed schemas 995 + type = schemaTypeToIdentifier({ 996 + context, 997 + namespace, 998 + schema: { 999 + type: 'unknown', 1000 + }, 1001 + }); 1002 + } 1003 + 1004 + // emit nodes only if $ref points to a reusable component 1005 + if ($ref && isRefOpenApiComponent($ref)) { 1006 + // emit namespace if it has any members 1007 + if (namespace.length) { 1008 + const identifier = context.file({ id: typesId })!.identifier({ 1009 + $ref, 1010 + create: true, 1011 + namespace: 'value', 1012 + }); 1013 + const node = compiler.namespaceDeclaration({ 1014 + name: identifier.name, 1015 + statements: namespace, 1016 + }); 1017 + context.file({ id: typesId })!.add(node); 1018 + } 1019 + 1020 + // enum handler emits its own artifacts 1021 + if (schema.type !== 'enum') { 1022 + const identifier = context.file({ id: typesId })!.identifier({ 1023 + $ref, 1024 + create: true, 1025 + namespace: 'type', 1026 + }); 1027 + const node = compiler.typeAliasDeclaration({ 1028 + comment: parseSchemaJsDoc({ schema }), 1029 + exportType: true, 1030 + name: identifier.name, 1031 + type, 1032 + }); 1033 + context.file({ id: typesId })!.add(node); 1034 + } 1035 + } 1036 + 1037 + return type; 1038 + }; 1039 + 1040 + export const generateTypes = ({ context }: { context: IRContext }): void => { 1041 + // TODO: parser - once types are a plugin, this logic can be simplified 1042 + if (!context.config.types.export) { 1043 + return; 1044 + } 1045 + 1046 + context.createFile({ 1047 + id: typesId, 1048 + path: 'types', 1049 + }); 1050 + 1051 + if (context.ir.components) { 1052 + for (const name in context.ir.components.schemas) { 1053 + const schema = context.ir.components.schemas[name]; 1054 + 1055 + schemaToType({ 1056 + $ref: `#/components/schemas/${name}`, 1057 + context, 1058 + schema, 1059 + }); 1060 + } 1061 + 1062 + for (const name in context.ir.components.parameters) { 1063 + const parameter = context.ir.components.parameters[name]; 1064 + 1065 + schemaToType({ 1066 + $ref: `#/components/parameters/${name}`, 1067 + context, 1068 + schema: parameter.schema, 1069 + }); 1070 + } 1071 + } 1072 + 1073 + // TODO: parser - once types are a plugin, this logic can be simplified 1074 + // provide config option on types to generate path types and services 1075 + // will set it to true if needed 1076 + if (context.config.services.export || context.config.types.tree) { 1077 + for (const path in context.ir.paths) { 1078 + const pathItem = context.ir.paths[path as keyof IRPathsObject]; 1079 + 1080 + if (pathItem.delete) { 1081 + operationToType({ 1082 + context, 1083 + operation: pathItem.delete, 1084 + }); 1085 + } 1086 + 1087 + if (pathItem.get) { 1088 + operationToType({ 1089 + context, 1090 + operation: pathItem.get, 1091 + }); 1092 + } 1093 + 1094 + if (pathItem.head) { 1095 + operationToType({ 1096 + context, 1097 + operation: pathItem.head, 1098 + }); 1099 + } 1100 + 1101 + if (pathItem.options) { 1102 + operationToType({ 1103 + context, 1104 + operation: pathItem.options, 1105 + }); 1106 + } 1107 + 1108 + if (pathItem.patch) { 1109 + operationToType({ 1110 + context, 1111 + operation: pathItem.patch, 1112 + }); 1113 + } 1114 + 1115 + if (pathItem.post) { 1116 + operationToType({ 1117 + context, 1118 + operation: pathItem.post, 1119 + }); 1120 + } 1121 + 1122 + if (pathItem.put) { 1123 + operationToType({ 1124 + context, 1125 + operation: pathItem.put, 1126 + }); 1127 + } 1128 + 1129 + if (pathItem.trace) { 1130 + operationToType({ 1131 + context, 1132 + operation: pathItem.trace, 1133 + }); 1134 + } 1135 + } 1136 + 1137 + // TODO: parser - document removal of tree? migrate it? 1138 + } 1139 + };
+6 -4
packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts
··· 11 11 clientOptionsTypeName, 12 12 } from '../../../generate/client'; 13 13 import { 14 - operationDataRef, 15 - operationErrorRef, 16 14 operationOptionsType, 17 - operationResponseRef, 18 15 serviceFunctionIdentifier, 19 16 } from '../../../generate/services'; 20 - import { schemaToType } from '../../../generate/types'; 21 17 import { relativeModulePath } from '../../../generate/utils'; 22 18 import type { IRContext } from '../../../ir/context'; 23 19 import type { ··· 33 29 import { getConfig } from '../../../utils/config'; 34 30 import { getServiceName } from '../../../utils/postprocess'; 35 31 import { transformServiceName } from '../../../utils/transform'; 32 + import { 33 + operationDataRef, 34 + operationErrorRef, 35 + operationResponseRef, 36 + } from '../../@hey-api/services/plugin'; 37 + import { schemaToType } from '../../@hey-api/types/plugin'; 36 38 import type { PluginHandler } from '../../types'; 37 39 import type { PluginConfig as ReactQueryPluginConfig } from '../react-query'; 38 40 import type { PluginConfig as SolidQueryPluginConfig } from '../solid-query';
+2 -1
packages/openapi-ts/src/types/config.ts
··· 1 1 import type { IROperationObject } from '../ir/ir'; 2 2 import type { OpenApiV2Schema, OpenApiV3Schema } from '../openApi'; 3 + import type { SchemaObject as OpenApiV3_1_0SchemaObject } from '../openApi/3.1.0/types/spec'; 3 4 import type { ClientPlugins, UserPlugins } from '../plugins/'; 4 5 import type { Operation } from '../types/client'; 5 6 import type { ExtractArrayOfObjects } from './utils'; ··· 131 132 */ 132 133 name?: ( 133 134 name: string, 134 - schema: OpenApiV2Schema | OpenApiV3Schema, 135 + schema: OpenApiV2Schema | OpenApiV3Schema | OpenApiV3_1_0SchemaObject, 135 136 ) => string; 136 137 /** 137 138 * Choose schema type to generate. Select 'form' if you don't want
+11 -10
packages/openapi-ts/test/sample.cjs
··· 17 17 // input: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 18 18 // name: 'foo', 19 19 output: { 20 - format: 'prettier', 21 - lint: 'eslint', 20 + // format: 'prettier', 21 + // lint: 'eslint', 22 22 path: './test/generated/sample/', 23 23 }, 24 - plugins: [ 25 - '@tanstack/react-query', 26 - // '@hey-api/services', 27 - // 'zod', 28 - ], 24 + // plugins: [ 25 + // '@tanstack/react-query', 26 + // // '@hey-api/services', 27 + // // 'zod', 28 + // ], 29 29 schemas: { 30 - export: false, 30 + // export: false, 31 + type: 'json', 31 32 }, 32 33 services: { 33 34 // asClass: true, 34 - // export: false, 35 + export: false, 35 36 // filter: '^GET /api/v{api-version}/simple:operation$', 36 37 // export: false, 37 38 // name: '^Parameters', ··· 41 42 // enums: 'typescript', 42 43 // enums: 'typescript+namespace', 43 44 enums: 'javascript', 44 - // export: false, 45 + export: false, 45 46 // include: 46 47 // '^(_400|CompositionWithOneOfAndProperties)', 47 48 // name: 'PascalCase',