diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/fastify/default/fastify.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/fastify/default/fastify.gen.ts index f6a8efdaf..4b8d75f8e 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/fastify/default/fastify.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/fastify/default/fastify.gen.ts @@ -41,10 +41,10 @@ export type RouteHandlers = { Reply: DummyBResponses; }>; callWithDuplicateResponses: RouteHandler<{ - Reply: Omit & CallWithDuplicateResponsesResponses; + Reply: Omit & CallWithDuplicateResponsesResponses; }>; callWithResponses: RouteHandler<{ - Reply: Omit & CallWithResponsesResponses; + Reply: Omit & CallWithResponsesResponses; }>; collectionFormat: RouteHandler<{ Querystring: CollectionFormatData['query']; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/fastify/default/fastify.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/fastify/default/fastify.gen.ts index 3b9a8380a..12b310791 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/fastify/default/fastify.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/fastify/default/fastify.gen.ts @@ -7,7 +7,7 @@ import type { ApiVVersionODataControllerCountResponses, CallToTestOrderOfParamsD export type RouteHandlers = { import: RouteHandler<{ Body: ImportData['body']; - Reply: Omit; + Reply: Omit; }>; apiVVersionODataControllerCount: RouteHandler<{ Reply: ApiVVersionODataControllerCountResponses; @@ -43,16 +43,16 @@ export type RouteHandlers = { Querystring?: GetCallWithOptionalParamData['query']; }>; postCallWithOptionalParam: RouteHandler<{ - Body: PostCallWithOptionalParamData['body']; + Body?: PostCallWithOptionalParamData['body']; Querystring: PostCallWithOptionalParamData['query']; Reply: PostCallWithOptionalParamResponses; }>; postApiVbyApiVersionRequestBody: RouteHandler<{ - Body: PostApiVbyApiVersionRequestBodyData['body']; + Body?: PostApiVbyApiVersionRequestBodyData['body']; Querystring?: PostApiVbyApiVersionRequestBodyData['query']; }>; postApiVbyApiVersionFormData: RouteHandler<{ - Body: PostApiVbyApiVersionFormDataData['body']; + Body?: PostApiVbyApiVersionFormDataData['body']; Querystring?: PostApiVbyApiVersionFormDataData['query']; }>; callWithDefaultParameters: RouteHandler<{ @@ -77,10 +77,10 @@ export type RouteHandlers = { Reply: DummyBResponses; }>; callWithDuplicateResponses: RouteHandler<{ - Reply: Omit & CallWithDuplicateResponsesResponses; + Reply: Omit & CallWithDuplicateResponsesResponses; }>; callWithResponses: RouteHandler<{ - Reply: Omit & CallWithResponsesResponses; + Reply: Omit & CallWithResponsesResponses; }>; collectionFormat: RouteHandler<{ Querystring: CollectionFormatData['query']; @@ -107,10 +107,10 @@ export type RouteHandlers = { Reply: MultipartResponseResponses; }>; multipartRequest: RouteHandler<{ - Body: MultipartRequestData['body']; + Body?: MultipartRequestData['body']; }>; complexParams: RouteHandler<{ - Body: ComplexParamsData['body']; + Body?: ComplexParamsData['body']; Params: ComplexParamsData['path']; Reply: ComplexParamsResponses; }>; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/fastify/default/fastify.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/fastify/default/fastify.gen.ts index 3b9a8380a..12b310791 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/fastify/default/fastify.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/fastify/default/fastify.gen.ts @@ -7,7 +7,7 @@ import type { ApiVVersionODataControllerCountResponses, CallToTestOrderOfParamsD export type RouteHandlers = { import: RouteHandler<{ Body: ImportData['body']; - Reply: Omit; + Reply: Omit; }>; apiVVersionODataControllerCount: RouteHandler<{ Reply: ApiVVersionODataControllerCountResponses; @@ -43,16 +43,16 @@ export type RouteHandlers = { Querystring?: GetCallWithOptionalParamData['query']; }>; postCallWithOptionalParam: RouteHandler<{ - Body: PostCallWithOptionalParamData['body']; + Body?: PostCallWithOptionalParamData['body']; Querystring: PostCallWithOptionalParamData['query']; Reply: PostCallWithOptionalParamResponses; }>; postApiVbyApiVersionRequestBody: RouteHandler<{ - Body: PostApiVbyApiVersionRequestBodyData['body']; + Body?: PostApiVbyApiVersionRequestBodyData['body']; Querystring?: PostApiVbyApiVersionRequestBodyData['query']; }>; postApiVbyApiVersionFormData: RouteHandler<{ - Body: PostApiVbyApiVersionFormDataData['body']; + Body?: PostApiVbyApiVersionFormDataData['body']; Querystring?: PostApiVbyApiVersionFormDataData['query']; }>; callWithDefaultParameters: RouteHandler<{ @@ -77,10 +77,10 @@ export type RouteHandlers = { Reply: DummyBResponses; }>; callWithDuplicateResponses: RouteHandler<{ - Reply: Omit & CallWithDuplicateResponsesResponses; + Reply: Omit & CallWithDuplicateResponsesResponses; }>; callWithResponses: RouteHandler<{ - Reply: Omit & CallWithResponsesResponses; + Reply: Omit & CallWithResponsesResponses; }>; collectionFormat: RouteHandler<{ Querystring: CollectionFormatData['query']; @@ -107,10 +107,10 @@ export type RouteHandlers = { Reply: MultipartResponseResponses; }>; multipartRequest: RouteHandler<{ - Body: MultipartRequestData['body']; + Body?: MultipartRequestData['body']; }>; complexParams: RouteHandler<{ - Body: ComplexParamsData['body']; + Body?: ComplexParamsData['body']; Params: ComplexParamsData['path']; Reply: ComplexParamsResponses; }>; diff --git a/packages/openapi-ts/src/plugins/fastify/plugin.ts b/packages/openapi-ts/src/plugins/fastify/plugin.ts index f11a497e7..bfb810cc3 100644 --- a/packages/openapi-ts/src/plugins/fastify/plugin.ts +++ b/packages/openapi-ts/src/plugins/fastify/plugin.ts @@ -1,9 +1,7 @@ -import type ts from 'typescript'; - import { operationResponsesMap } from '~/ir/operation'; import { hasParameterGroupObjectRequired } from '~/ir/parameter'; import type { IR } from '~/ir/types'; -import { type Property, tsc } from '~/tsc'; +import { $ } from '~/ts-dsl'; import type { FastifyPlugin } from './types'; @@ -13,8 +11,8 @@ const operationToRouteHandler = ({ }: { operation: IR.OperationObject; plugin: FastifyPlugin['Instance']; -}): Property | undefined => { - const properties: Array = []; +}) => { + const type = $.type.object(); const symbolDataType = plugin.querySymbol({ category: 'type', @@ -25,49 +23,49 @@ const operationToRouteHandler = ({ }); if (symbolDataType) { if (operation.body) { - properties.push({ - isRequired: operation.body.required, - name: 'Body', - type: `${symbolDataType.placeholder}['body']`, - }); + type.prop('Body', (p) => + p + .optional(!operation.body!.required) + .type(`${symbolDataType.placeholder}['body']`), + ); } if (operation.parameters) { if (operation.parameters.header) { - properties.push({ - isRequired: hasParameterGroupObjectRequired( - operation.parameters.header, - ), - name: 'Headers', - type: `${symbolDataType.placeholder}['headers']`, - }); + type.prop('Headers', (p) => + p + .optional( + !hasParameterGroupObjectRequired(operation.parameters!.header), + ) + .type(`${symbolDataType.placeholder}['headers']`), + ); } if (operation.parameters.path) { - properties.push({ - isRequired: hasParameterGroupObjectRequired( - operation.parameters.path, - ), - name: 'Params', - type: `${symbolDataType.placeholder}['path']`, - }); + type.prop('Params', (p) => + p + .optional( + !hasParameterGroupObjectRequired(operation.parameters!.path), + ) + .type(`${symbolDataType.placeholder}['path']`), + ); } if (operation.parameters.query) { - properties.push({ - isRequired: hasParameterGroupObjectRequired( - operation.parameters.query, - ), - name: 'Querystring', - type: `${symbolDataType.placeholder}['query']`, - }); + type.prop('Querystring', (p) => + p + .optional( + !hasParameterGroupObjectRequired(operation.parameters!.query), + ) + .type(`${symbolDataType.placeholder}['query']`), + ); } } } const { errors, responses } = operationResponsesMap(operation); - let errorsTypeReference: ts.TypeReferenceNode | undefined = undefined; + let errorsTypeReference: ReturnType | undefined; const symbolErrorType = plugin.querySymbol({ category: 'type', resource: 'operation', @@ -79,25 +77,19 @@ const operationToRouteHandler = ({ if (keys.length) { const hasDefaultResponse = keys.includes('default'); if (!hasDefaultResponse) { - errorsTypeReference = tsc.typeReferenceNode({ - typeName: symbolErrorType.placeholder, - }); + errorsTypeReference = $.type(symbolErrorType.placeholder); } else if (keys.length > 1) { - const errorsType = tsc.typeReferenceNode({ - typeName: symbolErrorType.placeholder, - }); - const defaultType = tsc.literalTypeNode({ - literal: tsc.stringLiteral({ text: 'default' }), - }); - errorsTypeReference = tsc.typeReferenceNode({ - typeArguments: [errorsType, defaultType], - typeName: 'Omit', - }); + errorsTypeReference = $.type('Omit', (t) => + t.generics( + $.type(symbolErrorType.placeholder), + $.type.literal('default'), + ), + ); } } } - let responsesTypeReference: ts.TypeReferenceNode | undefined = undefined; + let responsesTypeReference: ReturnType | undefined = undefined; const symbolResponseType = plugin.querySymbol({ category: 'type', resource: 'operation', @@ -109,37 +101,26 @@ const operationToRouteHandler = ({ if (keys.length) { const hasDefaultResponse = keys.includes('default'); if (!hasDefaultResponse) { - responsesTypeReference = tsc.typeReferenceNode({ - typeName: symbolResponseType.placeholder, - }); + responsesTypeReference = $.type(symbolResponseType.placeholder); } else if (keys.length > 1) { - const responsesType = tsc.typeReferenceNode({ - typeName: symbolResponseType.placeholder, - }); - const defaultType = tsc.literalTypeNode({ - literal: tsc.stringLiteral({ text: 'default' }), - }); - responsesTypeReference = tsc.typeReferenceNode({ - typeArguments: [responsesType, defaultType], - typeName: 'Omit', - }); + responsesTypeReference = $.type('Omit', (t) => + t.generics( + $.type(symbolResponseType.placeholder), + $.type.literal('default'), + ), + ); } } } const replyTypes = [errorsTypeReference, responsesTypeReference].filter( - Boolean, + (t): t is ReturnType => t !== undefined, ); if (replyTypes.length) { - properties.push({ - name: 'Reply', - type: tsc.typeIntersectionNode({ - types: replyTypes, - }), - }); + type.prop('Reply', (p) => p.type($.type.and(...replyTypes))); } - if (!properties.length) { + if (type.isEmpty) { return; } @@ -148,19 +129,10 @@ const operationToRouteHandler = ({ resource: 'route-handler', tool: 'fastify', }); - const routeHandler: Property = { + return { name: operation.id, - type: tsc.typeReferenceNode({ - typeArguments: [ - tsc.typeInterfaceNode({ - properties, - useLegacyResolution: false, - }), - ], - typeName: symbolRouteHandler.placeholder, - }), + type: $.type(symbolRouteHandler.placeholder, (t) => t.generic(type)), }; - return routeHandler; }; export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { @@ -181,14 +153,14 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { name: 'RouteHandlers', }); - const routeHandlers: Array = []; + const type = $.type.object(); plugin.forEach( 'operation', ({ operation }) => { const routeHandler = operationToRouteHandler({ operation, plugin }); if (routeHandler) { - routeHandlers.push(routeHandler); + type.prop(routeHandler.name, (p) => p.type(routeHandler.type)); } }, { @@ -196,13 +168,9 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { }, ); - const node = tsc.typeAliasDeclaration({ - exportType: symbolRouteHandlers.exported, - name: symbolRouteHandlers.placeholder, - type: tsc.typeInterfaceNode({ - properties: routeHandlers, - useLegacyResolution: false, - }), - }); + const node = $.type + .alias(symbolRouteHandlers.placeholder) + .export(symbolRouteHandlers.exported) + .type(type); plugin.setSymbolValue(symbolRouteHandlers, node); }; diff --git a/packages/openapi-ts/src/ts-dsl/type/object.ts b/packages/openapi-ts/src/ts-dsl/type/object.ts index 129a73fd1..423941f06 100644 --- a/packages/openapi-ts/src/ts-dsl/type/object.ts +++ b/packages/openapi-ts/src/ts-dsl/type/object.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging */ import ts from 'typescript'; +import type { MaybeTsDsl } from '../base'; import { TypeTsDsl } from '../base'; import { mixin } from '../mixins/apply'; import { OptionalMixin } from '../mixins/optional'; @@ -8,6 +9,16 @@ import { OptionalMixin } from '../mixins/optional'; export class TypeObjectTsDsl extends TypeTsDsl { private props: Array = []; + /** Returns true if object has at least one property or spread. */ + hasProps(): boolean { + return this.props.length > 0; + } + + /** Returns true if object has no properties or spreads. */ + get isEmpty(): boolean { + return !this.props.length; + } + /** Adds a property signature (returns property builder). */ prop(name: string, fn: (p: TypePropTsDsl) => void): this { const propTsDsl = new TypePropTsDsl(name, fn); @@ -22,7 +33,7 @@ export class TypeObjectTsDsl extends TypeTsDsl { class TypePropTsDsl extends TypeTsDsl { private name: string; - private typeInput?: string | ts.TypeNode; + private _type?: string | MaybeTsDsl; constructor(name: string, fn: (p: TypePropTsDsl) => void) { super(); @@ -31,21 +42,21 @@ class TypePropTsDsl extends TypeTsDsl { } /** Sets the property type. */ - type(type: string | ts.TypeNode): this { - this.typeInput = type; + type(type: string | MaybeTsDsl): this { + this._type = type; return this; } /** Builds and returns the property signature. */ $render(): ts.TypeElement { - if (!this.typeInput) { + if (!this._type) { throw new Error(`Type not specified for property '${this.name}'`); } return ts.factory.createPropertySignature( undefined, this.name, this.questionToken, - this.$type(this.typeInput), + this.$type(this._type), ); } }