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 #1184 from hey-api/feat/parser-plugin-transformers

feat: rewrite date type transform to new parser

authored by

Lubos and committed by
GitHub
2834ccfb 55d57151

+919 -326
+4 -26
packages/openapi-ts/src/generate/output.ts
··· 4 4 import type { OpenApi } from '../openApi'; 5 5 import { generateSchemas } from '../plugins/@hey-api/schemas/plugin'; 6 6 import { generateServices } from '../plugins/@hey-api/services/plugin'; 7 + import { generateTransformers } from '../plugins/@hey-api/transformers/plugin'; 7 8 import { generateTypes } from '../plugins/@hey-api/types/plugin'; 8 9 import type { Client } from '../types/client'; 9 10 import type { Files } from '../types/utils'; ··· 124 125 }); 125 126 } 126 127 127 - // types.gen.ts 128 + // TODO: parser - move types, schemas, transformers, and services into plugins 128 129 generateTypes({ context }); 129 - 130 - // schemas.gen.ts 131 130 generateSchemas({ context }); 132 - 133 - // transformers 134 - if ( 135 - context.config.services.export && 136 - // client.services.length && 137 - context.config.types.dates === 'types+transform' 138 - ) { 139 - // await generateLegacyTransformers({ 140 - // client, 141 - // onNode: (node) => { 142 - // files.types?.add(node); 143 - // }, 144 - // onRemoveNode: () => { 145 - // files.types?.removeNode(); 146 - // }, 147 - // }); 148 - } 149 - 150 - // services.gen.ts 131 + generateTransformers({ context }); 151 132 generateServices({ context }); 152 133 153 - // TODO: parser - remove after moving types, services, transformers, and schemas into plugin 154 - // index.ts. Any files generated after this won't be included in exports 155 - // from the index file. 134 + // TODO: parser - remove index file after above is migrated to plugins 156 135 generateIndexFile({ files: context.files }); 157 136 158 - // plugins 159 137 for (const plugin of context.config.plugins) { 160 138 plugin.handler({ 161 139 context,
+145 -1
packages/openapi-ts/src/ir/operation.ts
··· 1 - import type { IROperationObject } from './ir'; 1 + import type { IROperationObject, IRResponseObject, IRSchemaObject } from './ir'; 2 2 import type { Pagination } from './pagination'; 3 3 import { 4 4 hasParametersObjectRequired, 5 5 parameterWithPagination, 6 6 } from './parameter'; 7 + import { deduplicateSchema } from './schema'; 8 + import { addItemsToSchema } from './utils'; 7 9 8 10 export const hasOperationDataRequired = ( 9 11 operation: IROperationObject, ··· 36 38 37 39 return parameterWithPagination(operation.parameters); 38 40 }; 41 + 42 + type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default'; 43 + 44 + const statusCodeToGroup = ({ 45 + statusCode, 46 + }: { 47 + statusCode: string; 48 + }): StatusGroup => { 49 + switch (statusCode) { 50 + case '1XX': 51 + return '1XX'; 52 + case '2XX': 53 + return '2XX'; 54 + case '3XX': 55 + return '3XX'; 56 + case '4XX': 57 + return '4XX'; 58 + case '5XX': 59 + return '5XX'; 60 + case 'default': 61 + return 'default'; 62 + default: 63 + return `${statusCode[0]}XX` as StatusGroup; 64 + } 65 + }; 66 + 67 + interface OperationResponsesMap { 68 + error: IRSchemaObject | undefined; 69 + response: IRSchemaObject | undefined; 70 + } 71 + 72 + export const operationResponsesMap = ( 73 + operation: IROperationObject, 74 + ): OperationResponsesMap => { 75 + const result: OperationResponsesMap = { 76 + error: undefined, 77 + response: undefined, 78 + }; 79 + 80 + if (!operation.responses) { 81 + return result; 82 + } 83 + 84 + let errors: IRSchemaObject = {}; 85 + const errorsItems: Array<IRSchemaObject> = []; 86 + 87 + let responses: IRSchemaObject = {}; 88 + const responsesItems: Array<IRSchemaObject> = []; 89 + 90 + let defaultResponse: IRResponseObject | undefined; 91 + 92 + for (const name in operation.responses) { 93 + const response = operation.responses[name]!; 94 + 95 + switch (statusCodeToGroup({ statusCode: name })) { 96 + case '1XX': 97 + case '3XX': 98 + // TODO: parser - handle informational and redirection status codes 99 + break; 100 + case '2XX': 101 + responsesItems.push(response.schema); 102 + break; 103 + case '4XX': 104 + case '5XX': 105 + errorsItems.push(response.schema); 106 + break; 107 + case 'default': 108 + // store default response to be evaluated last 109 + defaultResponse = response; 110 + break; 111 + } 112 + } 113 + 114 + // infer default response type 115 + if (defaultResponse) { 116 + let inferred = false; 117 + 118 + // assume default is intended for success if none exists yet 119 + if (!responsesItems.length) { 120 + responsesItems.push(defaultResponse.schema); 121 + inferred = true; 122 + } 123 + 124 + const description = ( 125 + defaultResponse.schema.description ?? '' 126 + ).toLocaleLowerCase(); 127 + const $ref = (defaultResponse.schema.$ref ?? '').toLocaleLowerCase(); 128 + 129 + // TODO: parser - this could be rewritten using regular expressions 130 + const successKeywords = ['success']; 131 + if ( 132 + successKeywords.some( 133 + (keyword) => description.includes(keyword) || $ref.includes(keyword), 134 + ) 135 + ) { 136 + responsesItems.push(defaultResponse.schema); 137 + inferred = true; 138 + } 139 + 140 + // TODO: parser - this could be rewritten using regular expressions 141 + const errorKeywords = ['error', 'problem']; 142 + if ( 143 + errorKeywords.some( 144 + (keyword) => description.includes(keyword) || $ref.includes(keyword), 145 + ) 146 + ) { 147 + errorsItems.push(defaultResponse.schema); 148 + inferred = true; 149 + } 150 + 151 + // if no keyword match, assume default schema is intended for error 152 + if (!inferred) { 153 + errorsItems.push(defaultResponse.schema); 154 + } 155 + } 156 + 157 + if (errorsItems.length) { 158 + errors = addItemsToSchema({ 159 + items: errorsItems, 160 + mutateSchemaOneItem: true, 161 + schema: errors, 162 + }); 163 + errors = deduplicateSchema({ schema: errors }); 164 + if (Object.keys(errors).length) { 165 + result.error = errors; 166 + } 167 + } 168 + 169 + if (responsesItems.length) { 170 + responses = addItemsToSchema({ 171 + items: responsesItems, 172 + mutateSchemaOneItem: true, 173 + schema: responses, 174 + }); 175 + responses = deduplicateSchema({ schema: responses }); 176 + if (Object.keys(responses).length) { 177 + result.response = responses; 178 + } 179 + } 180 + 181 + return result; 182 + };
+66
packages/openapi-ts/src/ir/schema.ts
··· 1 + import type { IRSchemaObject } from './ir'; 2 + 3 + /** 4 + * Ensure we don't produce redundant types, e.g. string | string. 5 + */ 6 + export const deduplicateSchema = <T extends IRSchemaObject>({ 7 + schema, 8 + }: { 9 + schema: T; 10 + }): T => { 11 + if (!schema.items) { 12 + return schema; 13 + } 14 + 15 + const uniqueItems: Array<IRSchemaObject> = []; 16 + const typeIds: Array<string> = []; 17 + 18 + for (const item of schema.items) { 19 + // skip nested schemas for now, handle if necessary 20 + if ( 21 + !item.type || 22 + item.type === 'boolean' || 23 + item.type === 'null' || 24 + item.type === 'number' || 25 + item.type === 'string' || 26 + item.type === 'unknown' || 27 + item.type === 'void' 28 + ) { 29 + // const needs namespace to handle empty string values, otherwise 30 + // fallback would equal an actual value and we would skip an item 31 + const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`; 32 + if (!typeIds.includes(typeId)) { 33 + typeIds.push(typeId); 34 + uniqueItems.push(item); 35 + } 36 + continue; 37 + } 38 + 39 + uniqueItems.push(item); 40 + } 41 + 42 + schema.items = uniqueItems; 43 + 44 + if ( 45 + schema.items.length <= 1 && 46 + schema.type !== 'array' && 47 + schema.type !== 'enum' && 48 + schema.type !== 'tuple' 49 + ) { 50 + // bring the only item up to clean up the schema 51 + const liftedSchema = schema.items[0]; 52 + delete schema.logicalOperator; 53 + delete schema.items; 54 + schema = { 55 + ...schema, 56 + ...liftedSchema, 57 + }; 58 + } 59 + 60 + // exclude unknown if it's the only type left 61 + if (schema.type === 'unknown') { 62 + return {} as T; 63 + } 64 + 65 + return schema; 66 + };
+2 -2
packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts
··· 170 170 } 171 171 }; 172 172 173 - export const generateSchemas = async ({ 173 + export const generateSchemas = ({ 174 174 context, 175 175 }: { 176 176 context: IRContext<ParserOpenApiSpec>; 177 - }): Promise<void> => { 177 + }): void => { 178 178 // TODO: parser - once schemas are a plugin, this logic can be simplified 179 179 if (!context.config.schemas.export) { 180 180 return;
+1 -1
packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts
··· 420 420 } 421 421 }; 422 422 423 - export const generateServices = ({ context }: { context: IRContext }) => { 423 + export const generateServices = ({ context }: { context: IRContext }): void => { 424 424 // TODO: parser - once services are a plugin, this logic can be simplified 425 425 if (!context.config.services.export) { 426 426 return;
+8
packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts
··· 1 + import type { PluginConfig } from './types'; 2 + 3 + export const defaultConfig: Required<PluginConfig> = { 4 + handler: () => {}, 5 + handlerLegacy: () => {}, 6 + name: '@hey-api/transformers', 7 + output: 'transformers', 8 + };
+13
packages/openapi-ts/src/plugins/@hey-api/transformers/index.ts
··· 1 + import { defaultConfig } from './config'; 2 + import type { PluginConfig, UserConfig } from './types'; 3 + 4 + export { defaultConfig } from './config'; 5 + export type { PluginConfig, UserConfig } from './types'; 6 + 7 + /** 8 + * Type helper for Hey API transformers plugin, returns {@link PluginConfig} object 9 + */ 10 + export const defineConfig = (config?: UserConfig): PluginConfig => ({ 11 + ...defaultConfig, 12 + ...config, 13 + });
+341
packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts
··· 1 + // import type ts from 'typescript'; 2 + 3 + import type { IRContext } from '../../../ir/context'; 4 + import type { IRPathItemObject, IRPathsObject } from '../../../ir/ir'; 5 + import { operationResponsesMap } from '../../../ir/operation'; 6 + // import { compiler } from '../compiler'; 7 + // import { getOperationKey } from '../openApi/common/parser/operation'; 8 + // import type { ModelMeta, OperationResponse } from '../types/client'; 9 + // import { isModelDate, unsetUniqueTypeName } from '../utils/type'; 10 + // import { 11 + // modelResponseTransformerTypeName, 12 + // operationResponseTransformerTypeName, 13 + // operationResponseTypeName, 14 + // } from './services'; 15 + // import { generateType, type TypesProps } from './types'; 16 + 17 + const transformersId = 'transformers'; 18 + 19 + // interface ModelProps extends TypesProps { 20 + // meta?: ModelMeta; 21 + // path: Array<string>; 22 + // } 23 + 24 + // const dataVariableName = 'data'; 25 + 26 + // const getRefModels = ({ 27 + // client, 28 + // model, 29 + // }: Pick<TypesProps, 'client' | 'model'>) => { 30 + // const refModels = model.$refs.map((ref) => { 31 + // const refModel = client.models.find((model) => model.meta?.$ref === ref); 32 + // if (!refModel) { 33 + // throw new Error( 34 + // `Ref ${ref} could not be found. Transformers cannot be generated without having access to all refs.`, 35 + // ); 36 + // } 37 + // return refModel; 38 + // }); 39 + // return refModels; 40 + // }; 41 + 42 + // const ensureModelResponseTransformerExists = ( 43 + // props: Omit<ModelProps, 'path'>, 44 + // ) => { 45 + // const modelName = props.model.meta!.name; 46 + 47 + // const { name } = generateType({ 48 + // ...props, 49 + // meta: { 50 + // $ref: `transformers/${modelName}`, 51 + // name: modelName, 52 + // }, 53 + // nameTransformer: modelResponseTransformerTypeName, 54 + // onCreated: (name) => { 55 + // const statements = processModel({ 56 + // ...props, 57 + // meta: { 58 + // $ref: `transformers/${modelName}`, 59 + // name, 60 + // }, 61 + // path: [dataVariableName], 62 + // }); 63 + // generateResponseTransformer({ 64 + // ...props, 65 + // async: false, 66 + // name, 67 + // statements, 68 + // }); 69 + // }, 70 + // type: `(${dataVariableName}: any) => ${modelName}`, 71 + // }); 72 + 73 + // const result = { 74 + // created: Boolean(props.client.types[name]), 75 + // name, 76 + // }; 77 + // return result; 78 + // }; 79 + 80 + // const processArray = (props: ModelProps) => { 81 + // const { model } = props; 82 + // const refModels = getRefModels(props); 83 + 84 + // if (refModels.length === 1) { 85 + // const { created, name: nameModelResponseTransformer } = 86 + // ensureModelResponseTransformerExists({ ...props, model: refModels[0] }); 87 + 88 + // if (!created) { 89 + // return []; 90 + // } 91 + 92 + // return [ 93 + // compiler.transformArrayMutation({ 94 + // path: props.path, 95 + // transformerName: nameModelResponseTransformer, 96 + // }), 97 + // ]; 98 + // } 99 + 100 + // if ( 101 + // isModelDate(model) || 102 + // (model.link && 103 + // !Array.isArray(model.link) && 104 + // model.link.export === 'any-of' && 105 + // model.link.properties.find((property) => isModelDate(property))) 106 + // ) { 107 + // return [ 108 + // compiler.transformArrayMap({ 109 + // path: props.path, 110 + // transformExpression: compiler.conditionalExpression({ 111 + // condition: compiler.identifier({ text: 'item' }), 112 + // whenFalse: compiler.identifier({ text: 'item' }), 113 + // whenTrue: compiler.transformNewDate({ 114 + // parameterName: 'item', 115 + // }), 116 + // }), 117 + // }), 118 + // ]; 119 + // } 120 + 121 + // // Not transform for this type 122 + // return []; 123 + // }; 124 + 125 + // const processProperty = (props: ModelProps) => { 126 + // const { model } = props; 127 + // const path = [...props.path, model.name]; 128 + 129 + // if ( 130 + // model.type === 'string' && 131 + // model.export !== 'array' && 132 + // isModelDate(model) 133 + // ) { 134 + // return [compiler.transformDateMutation({ path })]; 135 + // } 136 + 137 + // // otherwise we recurse in case it's an object/array, and if it's not that will just bail with [] 138 + // return processModel({ 139 + // ...props, 140 + // model, 141 + // path, 142 + // }); 143 + // }; 144 + 145 + // const processModel = (props: ModelProps): ts.Statement[] => { 146 + // const { model } = props; 147 + 148 + // switch (model.export) { 149 + // case 'array': 150 + // return processArray(props); 151 + // case 'interface': 152 + // return model.properties.flatMap((property) => 153 + // processProperty({ ...props, model: property }), 154 + // ); 155 + // case 'reference': { 156 + // if (model.$refs.length !== 1) { 157 + // return []; 158 + // } 159 + // const refModels = getRefModels(props); 160 + 161 + // const { created, name: nameModelResponseTransformer } = 162 + // ensureModelResponseTransformerExists({ ...props, model: refModels[0] }); 163 + 164 + // if (!created) { 165 + // return []; 166 + // } 167 + 168 + // return model.in === 'response' 169 + // ? [ 170 + // compiler.expressionToStatement({ 171 + // expression: compiler.callExpression({ 172 + // functionName: nameModelResponseTransformer, 173 + // parameters: [dataVariableName], 174 + // }), 175 + // }), 176 + // ] 177 + // : compiler.transformFunctionMutation({ 178 + // path: props.path, 179 + // transformerName: nameModelResponseTransformer, 180 + // }); 181 + // } 182 + // // unsupported 183 + // default: 184 + // return []; 185 + // } 186 + // }; 187 + 188 + // const generateResponseTransformer = ({ 189 + // async, 190 + // client, 191 + // name, 192 + // statements, 193 + // }: Pick<TypesProps, 'client'> & { 194 + // async: boolean; 195 + // name: string; 196 + // statements: Array<ts.Statement>; 197 + // }) => { 198 + // const result = { 199 + // created: false, 200 + // name, 201 + // }; 202 + 203 + // if (!statements.length) { 204 + // // clean up created type for response transformer if it turns out 205 + // // the transformer was never generated 206 + // unsetUniqueTypeName({ 207 + // client, 208 + // name, 209 + // }); 210 + // files.types?.removeNode(); 211 + // return result; 212 + // } 213 + 214 + // const expression = compiler.arrowFunction({ 215 + // async, 216 + // multiLine: true, 217 + // parameters: [ 218 + // { 219 + // name: dataVariableName, 220 + // }, 221 + // ], 222 + // statements: [ 223 + // ...statements, 224 + // compiler.returnVariable({ 225 + // expression: dataVariableName, 226 + // }), 227 + // ], 228 + // }); 229 + // const statement = compiler.constVariable({ 230 + // exportConst: true, 231 + // expression, 232 + // name, 233 + // typeName: name, 234 + // }); 235 + // files.types?.add(statement); 236 + 237 + // return { 238 + // created: true, 239 + // name, 240 + // }; 241 + // }; 242 + 243 + // handles only response transformers for now 244 + export const generateTransformers = ({ 245 + context, 246 + }: { 247 + context: IRContext; 248 + }): void => { 249 + // TODO: parser - once transformers are a plugin, this logic can be simplified 250 + if ( 251 + !context.config.services.export || 252 + // client.services.length && 253 + context.config.types.dates !== 'types+transform' 254 + ) { 255 + return; 256 + } 257 + 258 + context.createFile({ 259 + id: transformersId, 260 + path: 'transformers', 261 + }); 262 + 263 + for (const path in context.ir.paths) { 264 + const pathItem = context.ir.paths[path as keyof IRPathsObject]; 265 + 266 + for (const _method in pathItem) { 267 + const method = _method as keyof IRPathItemObject; 268 + const operation = pathItem[method]!; 269 + 270 + const { response } = operationResponsesMap(operation); 271 + 272 + if (!response) { 273 + continue; 274 + } 275 + 276 + if (response.items && response.items.length > 1) { 277 + if (context.config.debug) { 278 + console.warn( 279 + `❗️ Transformers warning: route ${`${method.toUpperCase()} ${path}`} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, 280 + ); 281 + } 282 + continue; 283 + } 284 + 285 + // console.warn(operation.id, response) 286 + 287 + // const name = operationResponseTypeName(operation.name); 288 + // generateType({ 289 + // client, 290 + // meta: { 291 + // $ref: `transformers/${name}`, 292 + // name, 293 + // }, 294 + // nameTransformer: operationResponseTransformerTypeName, 295 + // onCreated: (nameCreated) => { 296 + // const statements = 297 + // successResponses.length > 1 298 + // ? successResponses.flatMap((response) => { 299 + // const statements = processModel({ 300 + // client, 301 + // meta: { 302 + // $ref: `transformers/${name}`, 303 + // name, 304 + // }, 305 + // model: response, 306 + // path: [dataVariableName], 307 + // }); 308 + 309 + // // assume unprocessed responses are void 310 + // if (!statements.length) { 311 + // return []; 312 + // } 313 + 314 + // return [ 315 + // compiler.ifStatement({ 316 + // expression: compiler.safeAccessExpression(['data']), 317 + // thenStatement: ts.factory.createBlock(statements), 318 + // }), 319 + // ]; 320 + // }) 321 + // : processModel({ 322 + // client, 323 + // meta: { 324 + // $ref: `transformers/${name}`, 325 + // name, 326 + // }, 327 + // model: successResponses[0], 328 + // path: [dataVariableName], 329 + // }); 330 + // generateResponseTransformer({ 331 + // async: true, 332 + // client, 333 + // name: nameCreated, 334 + // statements, 335 + // }); 336 + // }, 337 + // type: `(${dataVariableName}: any) => Promise<${name}>`, 338 + // }); 339 + } 340 + } 341 + };
+20
packages/openapi-ts/src/plugins/@hey-api/transformers/types.ts
··· 1 + import type { PluginHandler, PluginLegacyHandler } from '../../types'; 2 + 3 + interface Config { 4 + /** 5 + * Generate Hey API transformers from the provided input. 6 + */ 7 + name: '@hey-api/transformers'; 8 + /** 9 + * Name of the generated file. 10 + * @default 'transformers' 11 + */ 12 + output?: string; 13 + } 14 + 15 + export interface PluginConfig extends Config { 16 + handler: PluginHandler<Config>; 17 + handlerLegacy: PluginLegacyHandler<Config>; 18 + } 19 + 20 + export interface UserConfig extends Omit<Config, 'output'> {}
+49 -280
packages/openapi-ts/src/plugins/@hey-api/types/plugin.ts
··· 6 6 import type { 7 7 IROperationObject, 8 8 IRParameterObject, 9 + IRPathItemObject, 9 10 IRPathsObject, 10 - IRResponseObject, 11 11 IRSchemaObject, 12 12 } from '../../../ir/ir'; 13 - import { addItemsToSchema } from '../../../ir/utils'; 13 + import { operationResponsesMap } from '../../../ir/operation'; 14 + import { deduplicateSchema } from '../../../ir/schema'; 14 15 import { ensureValidTypeScriptJavaScriptIdentifier } from '../../../openApi'; 15 16 import { escapeComment } from '../../../utils/escape'; 16 17 import { irRef, isRefOpenApiComponent } from '../../../utils/ref'; ··· 452 453 }; 453 454 454 455 const stringTypeToIdentifier = ({ 456 + context, 455 457 schema, 456 458 }: { 457 459 context: IRContext; ··· 476 478 }), 477 479 ], 478 480 }); 481 + } 482 + 483 + if (schema.format === 'date-time' || schema.format === 'date') { 484 + // TODO: parser - add ability to skip type transformers 485 + if (context.config.types.dates) { 486 + return compiler.typeReferenceNode({ typeName: 'Date' }); 487 + } 479 488 } 480 489 } 481 490 ··· 580 589 } 581 590 }; 582 591 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 592 const irParametersToIrSchema = ({ 649 593 parameters, 650 594 }: { ··· 762 706 } 763 707 }; 764 708 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 = ({ 709 + const operationToType = ({ 791 710 context, 792 711 operation, 793 712 }: { 794 713 context: IRContext; 795 714 operation: IROperationObject; 796 715 }) => { 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; 716 + operationToDataType({ 717 + context, 718 + operation, 719 + }); 808 720 809 - for (const name in operation.responses) { 810 - const response = operation.responses[name]!; 721 + const { error, response } = operationResponsesMap(operation); 811 722 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 - } 723 + if (error) { 724 + const identifier = context.file({ id: typesId })!.identifier({ 725 + $ref: operationErrorRef({ id: operation.id }), 726 + create: true, 727 + namespace: 'type', 728 + }); 729 + const node = compiler.typeAliasDeclaration({ 730 + exportType: true, 731 + name: identifier.name, 732 + type: schemaToType({ 733 + context, 734 + schema: error, 735 + }), 736 + }); 737 + context.file({ id: typesId })!.add(node); 829 738 } 830 739 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, 740 + if (response) { 741 + const identifier = context.file({ id: typesId })!.identifier({ 742 + $ref: operationResponseRef({ id: operation.id }), 743 + create: true, 744 + namespace: 'type', 879 745 }); 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, 746 + const node = compiler.typeAliasDeclaration({ 747 + exportType: true, 748 + name: identifier.name, 749 + type: schemaToType({ 750 + context, 751 + schema: response, 752 + }), 904 753 }); 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 - } 754 + context.file({ id: typesId })!.add(node); 922 755 } 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 756 }; 942 757 943 758 export const schemaToType = ({ ··· 1077 892 for (const path in context.ir.paths) { 1078 893 const pathItem = context.ir.paths[path as keyof IRPathsObject]; 1079 894 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 - } 895 + for (const _method in pathItem) { 896 + const method = _method as keyof IRPathItemObject; 897 + const operation = pathItem[method]!; 1114 898 1115 - if (pathItem.post) { 1116 899 operationToType({ 1117 900 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, 901 + operation, 1133 902 }); 1134 903 } 1135 904 }
+12 -9
packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts
··· 119 119 parameters: [ 120 120 { 121 121 name: 'queryKey', 122 - type: compiler.typeNode('QueryKey<Options>'), 122 + type: compiler.typeReferenceNode({ typeName: 'QueryKey<Options>' }), 123 123 }, 124 124 { 125 125 name: 'page', 126 - type: compiler.typeNode('K'), 126 + type: compiler.typeReferenceNode({ typeName: 'K' }), 127 127 }, 128 128 ], 129 129 statements: [ ··· 293 293 294 294 const createQueryKeyFunction = ({ file }: { file: Files[keyof Files] }) => { 295 295 const returnType = compiler.indexedAccessTypeNode({ 296 - indexType: compiler.typeNode(0), 297 - objectType: compiler.typeNode(queryKeyName, [ 298 - compiler.typeNode(TOptionsType), 299 - ]), 296 + indexType: compiler.literalTypeNode({ 297 + literal: compiler.ots.number(0), 298 + }), 299 + objectType: compiler.typeReferenceNode({ 300 + typeArguments: [compiler.typeReferenceNode({ typeName: TOptionsType })], 301 + typeName: queryKeyName, 302 + }), 300 303 }); 301 304 302 305 const infiniteIdentifier = compiler.identifier({ text: 'infinite' }); ··· 307 310 parameters: [ 308 311 { 309 312 name: 'id', 310 - type: compiler.typeNode('string'), 313 + type: compiler.typeReferenceNode({ typeName: 'string' }), 311 314 }, 312 315 { 313 316 isRequired: false, 314 317 name: 'options', 315 - type: compiler.typeNode(TOptionsType), 318 + type: compiler.typeReferenceNode({ typeName: TOptionsType }), 316 319 }, 317 320 { 318 321 isRequired: false, 319 322 name: 'infinite', 320 - type: compiler.typeNode('boolean'), 323 + type: compiler.typeReferenceNode({ typeName: 'boolean' }), 321 324 }, 322 325 ], 323 326 returnType,
+6
packages/openapi-ts/src/plugins/index.ts
··· 7 7 type PluginConfig as PluginHeyApiServices, 8 8 } from './@hey-api/services'; 9 9 import { 10 + defaultConfig as heyApiTransformersDefaultConfig, 11 + type PluginConfig as PluginHeyApiTransformers, 12 + } from './@hey-api/transformers'; 13 + import { 10 14 defaultConfig as heyApiTypesDefaultConfig, 11 15 type PluginConfig as PluginHeyApiTypes, 12 16 } from './@hey-api/types'; ··· 50 54 export type ClientPlugins = 51 55 | PluginHeyApiSchemas 52 56 | PluginHeyApiServices 57 + | PluginHeyApiTransformers 53 58 | PluginHeyApiTypes 54 59 | PluginTanStackReactQuery 55 60 | PluginTanStackSolidQuery ··· 60 65 export const defaultPluginConfigs: DefaultPluginConfigsMap<ClientPlugins> = { 61 66 '@hey-api/schemas': heyApiSchemasDefaultConfig, 62 67 '@hey-api/services': heyApiServicesDefaultConfig, 68 + '@hey-api/transformers': heyApiTransformersDefaultConfig, 63 69 '@hey-api/types': heyApiTypesDefaultConfig, 64 70 '@tanstack/react-query': tanStackReactQueryDefaultConfig, 65 71 '@tanstack/solid-query': tanStackSolidQueryDefaultConfig,
+6 -7
packages/openapi-ts/test/sample.cjs
··· 10 10 }, 11 11 // debug: true, 12 12 experimental_parser: true, 13 - // input: './test/spec/v3-transforms.json', 14 13 // input: './test/spec/v3.json', 15 - input: './test/spec/3.1.0/full.json', 14 + input: './test/spec/3.1.0/transformers.json', 16 15 // input: './test/spec/v2.json', 17 16 // input: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 18 17 // name: 'foo', ··· 27 26 // // 'zod', 28 27 // ], 29 28 schemas: { 30 - // export: false, 31 - type: 'json', 29 + export: false, 30 + // type: 'json', 32 31 }, 33 32 services: { 34 33 // asClass: true, 35 - export: false, 34 + // export: false, 36 35 // filter: '^GET /api/v{api-version}/simple:operation$', 37 36 // export: false, 38 37 // name: '^Parameters', 39 38 }, 40 39 types: { 41 - // dates: 'types+transform', 40 + dates: 'types+transform', 42 41 // enums: 'typescript', 43 42 // enums: 'typescript+namespace', 44 43 enums: 'javascript', 45 - export: false, 44 + // export: false, 46 45 // include: 47 46 // '^(_400|CompositionWithOneOfAndProperties)', 48 47 // name: 'PascalCase',
+246
packages/openapi-ts/test/spec/3.1.0/transformers.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "swagger", 5 + "version": "v1.0" 6 + }, 7 + "servers": [ 8 + { 9 + "url": "http://localhost:3000/base" 10 + } 11 + ], 12 + "paths": { 13 + "/api/model-with-dates": { 14 + "post": { 15 + "operationId": "parentModelWithDates", 16 + "responses": { 17 + "200": { 18 + "description": "Success", 19 + "content": { 20 + "application/json; type=collection": { 21 + "schema": { 22 + "$ref": "#/components/schemas/ParentModelWithDates" 23 + } 24 + } 25 + } 26 + }, 27 + "201": { 28 + "description": "Success" 29 + } 30 + } 31 + }, 32 + "put": { 33 + "operationId": "modelWithDates", 34 + "responses": { 35 + "200": { 36 + "description": "Success", 37 + "content": { 38 + "application/json; type=collection": { 39 + "schema": { 40 + "$ref": "#/components/schemas/ModelWithDates" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + }, 48 + "/api/model-with-dates-array": { 49 + "put": { 50 + "operationId": "modelWithDatesArray", 51 + "responses": { 52 + "200": { 53 + "description": "Success", 54 + "content": { 55 + "application/json; type=collection": { 56 + "schema": { 57 + "type": "array", 58 + "items": { 59 + "$ref": "#/components/schemas/ModelWithDates" 60 + } 61 + } 62 + } 63 + } 64 + } 65 + } 66 + } 67 + }, 68 + "/api/array-of-dates": { 69 + "put": { 70 + "operationId": "arrayOfDates", 71 + "responses": { 72 + "200": { 73 + "description": "Success", 74 + "content": { 75 + "application/json; type=collection": { 76 + "schema": { 77 + "type": "array", 78 + "items": { 79 + "type": "string", 80 + "format": "date-time" 81 + } 82 + } 83 + } 84 + } 85 + } 86 + } 87 + } 88 + }, 89 + "/api/date": { 90 + "put": { 91 + "operationId": "date", 92 + "responses": { 93 + "200": { 94 + "description": "Success", 95 + "content": { 96 + "application/json; type=collection": { 97 + "schema": { 98 + "type": "string", 99 + "format": "date-time" 100 + } 101 + } 102 + } 103 + } 104 + } 105 + } 106 + }, 107 + "/api/multiple-responses": { 108 + "put": { 109 + "operationId": "multiple-responses", 110 + "responses": { 111 + "200": { 112 + "description": "Updated", 113 + "content": { 114 + "application/json; type=collection": { 115 + "schema": { 116 + "type": "array", 117 + "items": { 118 + "$ref": "#/components/schemas/ModelWithDates" 119 + } 120 + } 121 + } 122 + } 123 + }, 124 + "201": { 125 + "description": "Created", 126 + "content": { 127 + "application/json; type=collection": { 128 + "schema": { 129 + "type": "array", 130 + "items": { 131 + "$ref": "#/components/schemas/SimpleModel" 132 + } 133 + } 134 + } 135 + } 136 + } 137 + } 138 + } 139 + } 140 + }, 141 + "components": { 142 + "schemas": { 143 + "SimpleModel": { 144 + "description": "This is a model that contains a some dates", 145 + "type": "object", 146 + "required": ["id", "name", "enabled"], 147 + "properties": { 148 + "id": { 149 + "type": "number" 150 + }, 151 + "name": { 152 + "maxLength": 255, 153 + "type": "string" 154 + }, 155 + "enabled": { 156 + "type": "boolean", 157 + "readOnly": true 158 + } 159 + } 160 + }, 161 + "ModelWithDates": { 162 + "description": "This is a model that contains a some dates", 163 + "type": "object", 164 + "required": ["id", "name", "enabled", "modified"], 165 + "properties": { 166 + "id": { 167 + "type": "number" 168 + }, 169 + "name": { 170 + "maxLength": 255, 171 + "type": "string" 172 + }, 173 + "enabled": { 174 + "type": "boolean", 175 + "readOnly": true 176 + }, 177 + "modified": { 178 + "type": "string", 179 + "format": "date-time", 180 + "readOnly": true 181 + }, 182 + "expires": { 183 + "type": "string", 184 + "format": "date", 185 + "readOnly": true 186 + } 187 + } 188 + }, 189 + "ParentModelWithDates": { 190 + "description": "This is a model that contains a some dates and arrays", 191 + "type": "object", 192 + "required": ["id", "name"], 193 + "properties": { 194 + "id": { 195 + "type": "number" 196 + }, 197 + "modified": { 198 + "type": "string", 199 + "format": "date-time", 200 + "readOnly": true 201 + }, 202 + "items": { 203 + "type": "array", 204 + "items": { 205 + "$ref": "#/components/schemas/ModelWithDates" 206 + } 207 + }, 208 + "item": { 209 + "$ref": "#/components/schemas/ModelWithDates" 210 + }, 211 + "nullable-date": { 212 + "type": "array", 213 + "items": { 214 + "anyOf": [ 215 + { "type": "string", "format": "date-time" }, 216 + { "type": "null" } 217 + ] 218 + } 219 + }, 220 + "simpleItems": { 221 + "type": "array", 222 + "items": { 223 + "$ref": "#/components/schemas/SimpleModel" 224 + } 225 + }, 226 + "simpleItem": { 227 + "$ref": "#/components/schemas/SimpleModel" 228 + }, 229 + "dates": { 230 + "type": "array", 231 + "items": { 232 + "type": "string", 233 + "format": "date-time" 234 + } 235 + }, 236 + "strings": { 237 + "type": "array", 238 + "items": { 239 + "type": "string" 240 + } 241 + } 242 + } 243 + } 244 + } 245 + } 246 + }