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

Merge pull request #2998 from hey-api/refactor/dsl-type-nodes-7

refactor: fastify plugin use dsl

authored by

Lubos and committed by
GitHub
381803f8 595ae0ed

+90 -111
+2 -2
packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/fastify/default/fastify.gen.ts
··· 41 41 Reply: DummyBResponses; 42 42 }>; 43 43 callWithDuplicateResponses: RouteHandler<{ 44 - Reply: Omit<CallWithDuplicateResponsesErrors, 'default'> & CallWithDuplicateResponsesResponses; 44 + Reply: Omit<CallWithDuplicateResponsesErrors, "default"> & CallWithDuplicateResponsesResponses; 45 45 }>; 46 46 callWithResponses: RouteHandler<{ 47 - Reply: Omit<CallWithResponsesErrors, 'default'> & CallWithResponsesResponses; 47 + Reply: Omit<CallWithResponsesErrors, "default"> & CallWithResponsesResponses; 48 48 }>; 49 49 collectionFormat: RouteHandler<{ 50 50 Querystring: CollectionFormatData['query'];
+8 -8
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/fastify/default/fastify.gen.ts
··· 7 7 export type RouteHandlers = { 8 8 import: RouteHandler<{ 9 9 Body: ImportData['body']; 10 - Reply: Omit<ImportResponses, 'default'>; 10 + Reply: Omit<ImportResponses, "default">; 11 11 }>; 12 12 apiVVersionODataControllerCount: RouteHandler<{ 13 13 Reply: ApiVVersionODataControllerCountResponses; ··· 43 43 Querystring?: GetCallWithOptionalParamData['query']; 44 44 }>; 45 45 postCallWithOptionalParam: RouteHandler<{ 46 - Body: PostCallWithOptionalParamData['body']; 46 + Body?: PostCallWithOptionalParamData['body']; 47 47 Querystring: PostCallWithOptionalParamData['query']; 48 48 Reply: PostCallWithOptionalParamResponses; 49 49 }>; 50 50 postApiVbyApiVersionRequestBody: RouteHandler<{ 51 - Body: PostApiVbyApiVersionRequestBodyData['body']; 51 + Body?: PostApiVbyApiVersionRequestBodyData['body']; 52 52 Querystring?: PostApiVbyApiVersionRequestBodyData['query']; 53 53 }>; 54 54 postApiVbyApiVersionFormData: RouteHandler<{ 55 - Body: PostApiVbyApiVersionFormDataData['body']; 55 + Body?: PostApiVbyApiVersionFormDataData['body']; 56 56 Querystring?: PostApiVbyApiVersionFormDataData['query']; 57 57 }>; 58 58 callWithDefaultParameters: RouteHandler<{ ··· 77 77 Reply: DummyBResponses; 78 78 }>; 79 79 callWithDuplicateResponses: RouteHandler<{ 80 - Reply: Omit<CallWithDuplicateResponsesErrors, 'default'> & CallWithDuplicateResponsesResponses; 80 + Reply: Omit<CallWithDuplicateResponsesErrors, "default"> & CallWithDuplicateResponsesResponses; 81 81 }>; 82 82 callWithResponses: RouteHandler<{ 83 - Reply: Omit<CallWithResponsesErrors, 'default'> & CallWithResponsesResponses; 83 + Reply: Omit<CallWithResponsesErrors, "default"> & CallWithResponsesResponses; 84 84 }>; 85 85 collectionFormat: RouteHandler<{ 86 86 Querystring: CollectionFormatData['query']; ··· 107 107 Reply: MultipartResponseResponses; 108 108 }>; 109 109 multipartRequest: RouteHandler<{ 110 - Body: MultipartRequestData['body']; 110 + Body?: MultipartRequestData['body']; 111 111 }>; 112 112 complexParams: RouteHandler<{ 113 - Body: ComplexParamsData['body']; 113 + Body?: ComplexParamsData['body']; 114 114 Params: ComplexParamsData['path']; 115 115 Reply: ComplexParamsResponses; 116 116 }>;
+8 -8
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/fastify/default/fastify.gen.ts
··· 7 7 export type RouteHandlers = { 8 8 import: RouteHandler<{ 9 9 Body: ImportData['body']; 10 - Reply: Omit<ImportResponses, 'default'>; 10 + Reply: Omit<ImportResponses, "default">; 11 11 }>; 12 12 apiVVersionODataControllerCount: RouteHandler<{ 13 13 Reply: ApiVVersionODataControllerCountResponses; ··· 43 43 Querystring?: GetCallWithOptionalParamData['query']; 44 44 }>; 45 45 postCallWithOptionalParam: RouteHandler<{ 46 - Body: PostCallWithOptionalParamData['body']; 46 + Body?: PostCallWithOptionalParamData['body']; 47 47 Querystring: PostCallWithOptionalParamData['query']; 48 48 Reply: PostCallWithOptionalParamResponses; 49 49 }>; 50 50 postApiVbyApiVersionRequestBody: RouteHandler<{ 51 - Body: PostApiVbyApiVersionRequestBodyData['body']; 51 + Body?: PostApiVbyApiVersionRequestBodyData['body']; 52 52 Querystring?: PostApiVbyApiVersionRequestBodyData['query']; 53 53 }>; 54 54 postApiVbyApiVersionFormData: RouteHandler<{ 55 - Body: PostApiVbyApiVersionFormDataData['body']; 55 + Body?: PostApiVbyApiVersionFormDataData['body']; 56 56 Querystring?: PostApiVbyApiVersionFormDataData['query']; 57 57 }>; 58 58 callWithDefaultParameters: RouteHandler<{ ··· 77 77 Reply: DummyBResponses; 78 78 }>; 79 79 callWithDuplicateResponses: RouteHandler<{ 80 - Reply: Omit<CallWithDuplicateResponsesErrors, 'default'> & CallWithDuplicateResponsesResponses; 80 + Reply: Omit<CallWithDuplicateResponsesErrors, "default"> & CallWithDuplicateResponsesResponses; 81 81 }>; 82 82 callWithResponses: RouteHandler<{ 83 - Reply: Omit<CallWithResponsesErrors, 'default'> & CallWithResponsesResponses; 83 + Reply: Omit<CallWithResponsesErrors, "default"> & CallWithResponsesResponses; 84 84 }>; 85 85 collectionFormat: RouteHandler<{ 86 86 Querystring: CollectionFormatData['query']; ··· 107 107 Reply: MultipartResponseResponses; 108 108 }>; 109 109 multipartRequest: RouteHandler<{ 110 - Body: MultipartRequestData['body']; 110 + Body?: MultipartRequestData['body']; 111 111 }>; 112 112 complexParams: RouteHandler<{ 113 - Body: ComplexParamsData['body']; 113 + Body?: ComplexParamsData['body']; 114 114 Params: ComplexParamsData['path']; 115 115 Reply: ComplexParamsResponses; 116 116 }>;
+56 -88
packages/openapi-ts/src/plugins/fastify/plugin.ts
··· 1 - import type ts from 'typescript'; 2 - 3 1 import { operationResponsesMap } from '~/ir/operation'; 4 2 import { hasParameterGroupObjectRequired } from '~/ir/parameter'; 5 3 import type { IR } from '~/ir/types'; 6 - import { type Property, tsc } from '~/tsc'; 4 + import { $ } from '~/ts-dsl'; 7 5 8 6 import type { FastifyPlugin } from './types'; 9 7 ··· 13 11 }: { 14 12 operation: IR.OperationObject; 15 13 plugin: FastifyPlugin['Instance']; 16 - }): Property | undefined => { 17 - const properties: Array<Property> = []; 14 + }) => { 15 + const type = $.type.object(); 18 16 19 17 const symbolDataType = plugin.querySymbol({ 20 18 category: 'type', ··· 25 23 }); 26 24 if (symbolDataType) { 27 25 if (operation.body) { 28 - properties.push({ 29 - isRequired: operation.body.required, 30 - name: 'Body', 31 - type: `${symbolDataType.placeholder}['body']`, 32 - }); 26 + type.prop('Body', (p) => 27 + p 28 + .optional(!operation.body!.required) 29 + .type(`${symbolDataType.placeholder}['body']`), 30 + ); 33 31 } 34 32 35 33 if (operation.parameters) { 36 34 if (operation.parameters.header) { 37 - properties.push({ 38 - isRequired: hasParameterGroupObjectRequired( 39 - operation.parameters.header, 40 - ), 41 - name: 'Headers', 42 - type: `${symbolDataType.placeholder}['headers']`, 43 - }); 35 + type.prop('Headers', (p) => 36 + p 37 + .optional( 38 + !hasParameterGroupObjectRequired(operation.parameters!.header), 39 + ) 40 + .type(`${symbolDataType.placeholder}['headers']`), 41 + ); 44 42 } 45 43 46 44 if (operation.parameters.path) { 47 - properties.push({ 48 - isRequired: hasParameterGroupObjectRequired( 49 - operation.parameters.path, 50 - ), 51 - name: 'Params', 52 - type: `${symbolDataType.placeholder}['path']`, 53 - }); 45 + type.prop('Params', (p) => 46 + p 47 + .optional( 48 + !hasParameterGroupObjectRequired(operation.parameters!.path), 49 + ) 50 + .type(`${symbolDataType.placeholder}['path']`), 51 + ); 54 52 } 55 53 56 54 if (operation.parameters.query) { 57 - properties.push({ 58 - isRequired: hasParameterGroupObjectRequired( 59 - operation.parameters.query, 60 - ), 61 - name: 'Querystring', 62 - type: `${symbolDataType.placeholder}['query']`, 63 - }); 55 + type.prop('Querystring', (p) => 56 + p 57 + .optional( 58 + !hasParameterGroupObjectRequired(operation.parameters!.query), 59 + ) 60 + .type(`${symbolDataType.placeholder}['query']`), 61 + ); 64 62 } 65 63 } 66 64 } 67 65 68 66 const { errors, responses } = operationResponsesMap(operation); 69 67 70 - let errorsTypeReference: ts.TypeReferenceNode | undefined = undefined; 68 + let errorsTypeReference: ReturnType<typeof $.type> | undefined; 71 69 const symbolErrorType = plugin.querySymbol({ 72 70 category: 'type', 73 71 resource: 'operation', ··· 79 77 if (keys.length) { 80 78 const hasDefaultResponse = keys.includes('default'); 81 79 if (!hasDefaultResponse) { 82 - errorsTypeReference = tsc.typeReferenceNode({ 83 - typeName: symbolErrorType.placeholder, 84 - }); 80 + errorsTypeReference = $.type(symbolErrorType.placeholder); 85 81 } else if (keys.length > 1) { 86 - const errorsType = tsc.typeReferenceNode({ 87 - typeName: symbolErrorType.placeholder, 88 - }); 89 - const defaultType = tsc.literalTypeNode({ 90 - literal: tsc.stringLiteral({ text: 'default' }), 91 - }); 92 - errorsTypeReference = tsc.typeReferenceNode({ 93 - typeArguments: [errorsType, defaultType], 94 - typeName: 'Omit', 95 - }); 82 + errorsTypeReference = $.type('Omit', (t) => 83 + t.generics( 84 + $.type(symbolErrorType.placeholder), 85 + $.type.literal('default'), 86 + ), 87 + ); 96 88 } 97 89 } 98 90 } 99 91 100 - let responsesTypeReference: ts.TypeReferenceNode | undefined = undefined; 92 + let responsesTypeReference: ReturnType<typeof $.type> | undefined = undefined; 101 93 const symbolResponseType = plugin.querySymbol({ 102 94 category: 'type', 103 95 resource: 'operation', ··· 109 101 if (keys.length) { 110 102 const hasDefaultResponse = keys.includes('default'); 111 103 if (!hasDefaultResponse) { 112 - responsesTypeReference = tsc.typeReferenceNode({ 113 - typeName: symbolResponseType.placeholder, 114 - }); 104 + responsesTypeReference = $.type(symbolResponseType.placeholder); 115 105 } else if (keys.length > 1) { 116 - const responsesType = tsc.typeReferenceNode({ 117 - typeName: symbolResponseType.placeholder, 118 - }); 119 - const defaultType = tsc.literalTypeNode({ 120 - literal: tsc.stringLiteral({ text: 'default' }), 121 - }); 122 - responsesTypeReference = tsc.typeReferenceNode({ 123 - typeArguments: [responsesType, defaultType], 124 - typeName: 'Omit', 125 - }); 106 + responsesTypeReference = $.type('Omit', (t) => 107 + t.generics( 108 + $.type(symbolResponseType.placeholder), 109 + $.type.literal('default'), 110 + ), 111 + ); 126 112 } 127 113 } 128 114 } 129 115 130 116 const replyTypes = [errorsTypeReference, responsesTypeReference].filter( 131 - Boolean, 117 + (t): t is ReturnType<typeof $.type> => t !== undefined, 132 118 ); 133 119 if (replyTypes.length) { 134 - properties.push({ 135 - name: 'Reply', 136 - type: tsc.typeIntersectionNode({ 137 - types: replyTypes, 138 - }), 139 - }); 120 + type.prop('Reply', (p) => p.type($.type.and(...replyTypes))); 140 121 } 141 122 142 - if (!properties.length) { 123 + if (type.isEmpty) { 143 124 return; 144 125 } 145 126 ··· 148 129 resource: 'route-handler', 149 130 tool: 'fastify', 150 131 }); 151 - const routeHandler: Property = { 132 + return { 152 133 name: operation.id, 153 - type: tsc.typeReferenceNode({ 154 - typeArguments: [ 155 - tsc.typeInterfaceNode({ 156 - properties, 157 - useLegacyResolution: false, 158 - }), 159 - ], 160 - typeName: symbolRouteHandler.placeholder, 161 - }), 134 + type: $.type(symbolRouteHandler.placeholder, (t) => t.generic(type)), 162 135 }; 163 - return routeHandler; 164 136 }; 165 137 166 138 export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { ··· 181 153 name: 'RouteHandlers', 182 154 }); 183 155 184 - const routeHandlers: Array<Property> = []; 156 + const type = $.type.object(); 185 157 186 158 plugin.forEach( 187 159 'operation', 188 160 ({ operation }) => { 189 161 const routeHandler = operationToRouteHandler({ operation, plugin }); 190 162 if (routeHandler) { 191 - routeHandlers.push(routeHandler); 163 + type.prop(routeHandler.name, (p) => p.type(routeHandler.type)); 192 164 } 193 165 }, 194 166 { ··· 196 168 }, 197 169 ); 198 170 199 - const node = tsc.typeAliasDeclaration({ 200 - exportType: symbolRouteHandlers.exported, 201 - name: symbolRouteHandlers.placeholder, 202 - type: tsc.typeInterfaceNode({ 203 - properties: routeHandlers, 204 - useLegacyResolution: false, 205 - }), 206 - }); 171 + const node = $.type 172 + .alias(symbolRouteHandlers.placeholder) 173 + .export(symbolRouteHandlers.exported) 174 + .type(type); 207 175 plugin.setSymbolValue(symbolRouteHandlers, node); 208 176 };
+16 -5
packages/openapi-ts/src/ts-dsl/type/object.ts
··· 1 1 /* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ 2 2 import ts from 'typescript'; 3 3 4 + import type { MaybeTsDsl } from '../base'; 4 5 import { TypeTsDsl } from '../base'; 5 6 import { mixin } from '../mixins/apply'; 6 7 import { OptionalMixin } from '../mixins/optional'; 7 8 8 9 export class TypeObjectTsDsl extends TypeTsDsl<ts.TypeNode> { 9 10 private props: Array<TypePropTsDsl> = []; 11 + 12 + /** Returns true if object has at least one property or spread. */ 13 + hasProps(): boolean { 14 + return this.props.length > 0; 15 + } 16 + 17 + /** Returns true if object has no properties or spreads. */ 18 + get isEmpty(): boolean { 19 + return !this.props.length; 20 + } 10 21 11 22 /** Adds a property signature (returns property builder). */ 12 23 prop(name: string, fn: (p: TypePropTsDsl) => void): this { ··· 22 33 23 34 class TypePropTsDsl extends TypeTsDsl<ts.TypeElement> { 24 35 private name: string; 25 - private typeInput?: string | ts.TypeNode; 36 + private _type?: string | MaybeTsDsl<ts.TypeNode>; 26 37 27 38 constructor(name: string, fn: (p: TypePropTsDsl) => void) { 28 39 super(); ··· 31 42 } 32 43 33 44 /** Sets the property type. */ 34 - type(type: string | ts.TypeNode): this { 35 - this.typeInput = type; 45 + type(type: string | MaybeTsDsl<ts.TypeNode>): this { 46 + this._type = type; 36 47 return this; 37 48 } 38 49 39 50 /** Builds and returns the property signature. */ 40 51 $render(): ts.TypeElement { 41 - if (!this.typeInput) { 52 + if (!this._type) { 42 53 throw new Error(`Type not specified for property '${this.name}'`); 43 54 } 44 55 return ts.factory.createPropertySignature( 45 56 undefined, 46 57 this.name, 47 58 this.questionToken, 48 - this.$type(this.typeInput), 59 + this.$type(this._type), 49 60 ); 50 61 } 51 62 }