diff --git a/packages/openapi-ts/src/compiler/classes.ts b/packages/openapi-ts/src/compiler/classes.ts index b2fff7c93..9a083e21d 100644 --- a/packages/openapi-ts/src/compiler/classes.ts +++ b/packages/openapi-ts/src/compiler/classes.ts @@ -41,7 +41,7 @@ const toAccessLevelModifiers = (access?: AccessLevel): ts.ModifierLike[] => { * @param parameters - the parameters to conver to declarations * @returns ts.ParameterDeclaration[] */ -const toParameterDeclarations = (parameters: FunctionParameter[]) => +export const toParameterDeclarations = (parameters: FunctionParameter[]) => parameters.map((p) => { const modifiers = toAccessLevelModifiers(p.accessLevel); if (p.isReadOnly) { @@ -69,8 +69,8 @@ const toParameterDeclarations = (parameters: FunctionParameter[]) => * @returns ts.ConstructorDeclaration */ export const createConstructorDeclaration = ({ - accessLevel = undefined, - comment = undefined, + accessLevel, + comment, multiLine = true, parameters = [], statements = [], @@ -105,13 +105,13 @@ export const createConstructorDeclaration = ({ * @returns ts.MethodDeclaration */ export const createMethodDeclaration = ({ - accessLevel = undefined, - comment = undefined, + accessLevel, + comment, isStatic = false, multiLine = true, name, parameters = [], - returnType = undefined, + returnType, statements = [], }: { accessLevel?: AccessLevel; @@ -156,7 +156,7 @@ type ClassDecorator = { * @returns ts.ClassDeclaration */ export const createClassDeclaration = ({ - decorator = undefined, + decorator, members = [], name, }: { @@ -195,28 +195,3 @@ export const createClassDeclaration = ({ m, ); }; - -/** - * Create a return function call. Example `return call(param);`. - * @param args - arguments to pass to the function. - * @param name - name of the function to call. - * @returns ts.ReturnStatement - */ -export const createReturnFunctionCall = ({ - args = [], - name, -}: { - args: any[]; - name: string; -}) => - ts.factory.createReturnStatement( - ts.factory.createCallExpression( - ts.factory.createIdentifier(name), - undefined, - args - .map((arg) => - ts.isExpression(arg) ? arg : ts.factory.createIdentifier(arg), - ) - .filter(isType), - ), - ); diff --git a/packages/openapi-ts/src/compiler/index.ts b/packages/openapi-ts/src/compiler/index.ts index e10c62748..a993ed5f5 100644 --- a/packages/openapi-ts/src/compiler/index.ts +++ b/packages/openapi-ts/src/compiler/index.ts @@ -5,6 +5,7 @@ import ts from 'typescript'; import * as classes from './classes'; import * as module from './module'; +import * as _return from './return'; import * as typedef from './typedef'; import * as types from './types'; import { stringToTsNodes, tsNodeToString } from './utils'; @@ -55,9 +56,7 @@ export class TypeScriptFile { this._items = [...this._items, ...nodes]; } - public addNamedImport( - ...params: Parameters - ): void { + public addImport(...params: Parameters): void { this._imports = [...this._imports, compiler.import.named(...params)]; } @@ -121,16 +120,18 @@ export const compiler = { constructor: classes.createConstructorDeclaration, create: classes.createClassDeclaration, method: classes.createMethodDeclaration, - return: classes.createReturnFunctionCall, }, export: { all: module.createExportAllDeclaration, - asConst: module.createExportVariableAsConst, + const: module.createExportConstVariable, named: module.createNamedExportDeclarations, }, import: { named: module.createNamedImportDeclarations, }, + return: { + functionCall: _return.createReturnFunctionCall, + }, typedef: { alias: typedef.createTypeAliasDeclaration, array: typedef.createTypeArrayNode, @@ -144,6 +145,7 @@ export const compiler = { types: { array: types.createArrayType, enum: types.createEnumDeclaration, + function: types.createFunction, object: types.createObjectType, }, utils: { diff --git a/packages/openapi-ts/src/compiler/module.ts b/packages/openapi-ts/src/compiler/module.ts index dfa62f03e..f59e5f6a2 100644 --- a/packages/openapi-ts/src/compiler/module.ts +++ b/packages/openapi-ts/src/compiler/module.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; -import { ots } from './utils'; +import { addLeadingJSDocComment, type Comments, ots } from './utils'; /** * Create export all declaration. Example: `export * from './y'`. @@ -53,32 +53,45 @@ export const createNamedExportDeclarations = ( }; /** - * Create an export variable as const statement. Example: `export x = {} as const`. - * @param name - name of the variable. - * @param expression - expression for the variable. + * Create a const variable export. Optionally, it can use const assertion. + * Example: `export x = {} as const`. + * @param constAssertion use const assertion? + * @param expression expression for the variable. + * @param name name of the variable. * @returns ts.VariableStatement */ -export const createExportVariableAsConst = ( - name: string, - expression: ts.Expression, -): ts.VariableStatement => - ts.factory.createVariableStatement( +export const createExportConstVariable = ({ + comment, + constAssertion = false, + expression, + name, +}: { + comment?: Comments; + constAssertion?: boolean; + expression: ts.Expression; + name: string; +}): ts.VariableStatement => { + const initializer = constAssertion + ? ts.factory.createAsExpression( + expression, + ts.factory.createTypeReferenceNode('const'), + ) + : expression; + const declaration = ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(name), + undefined, + undefined, + initializer, + ); + const statement = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(name), - undefined, - undefined, - ts.factory.createAsExpression( - expression, - ts.factory.createTypeReferenceNode('const'), - ), - ), - ], - ts.NodeFlags.Const, - ), + ts.factory.createVariableDeclarationList([declaration], ts.NodeFlags.Const), ); + if (comment) { + addLeadingJSDocComment(statement, comment); + } + return statement; +}; /** * Create a named import declaration. Example: `import { X } from './y'`. diff --git a/packages/openapi-ts/src/compiler/return.ts b/packages/openapi-ts/src/compiler/return.ts new file mode 100644 index 000000000..91e788d86 --- /dev/null +++ b/packages/openapi-ts/src/compiler/return.ts @@ -0,0 +1,33 @@ +import ts from 'typescript'; + +import { isType } from './utils'; + +/** + * Create a return function call statement. + * Example `return fn(params)`. + * @param args arguments to pass to the function. + * @param name name of the function to call. + * @param types list of function types + * @returns ts.ReturnStatement + */ +export const createReturnFunctionCall = ({ + args = [], + name, + types = [], +}: { + args: any[]; + name: string; + types?: string[]; +}) => { + const expression = ts.factory.createCallExpression( + ts.factory.createIdentifier(name), + types.map((type) => ts.factory.createTypeReferenceNode(type)), + args + .map((arg) => + ts.isExpression(arg) ? arg : ts.factory.createIdentifier(arg), + ) + .filter(isType), + ); + const statement = ts.factory.createReturnStatement(expression); + return statement; +}; diff --git a/packages/openapi-ts/src/compiler/types.ts b/packages/openapi-ts/src/compiler/types.ts index d8ac30824..50cb514c1 100644 --- a/packages/openapi-ts/src/compiler/types.ts +++ b/packages/openapi-ts/src/compiler/types.ts @@ -1,25 +1,27 @@ import ts from 'typescript'; +import { type FunctionParameter, toParameterDeclarations } from './classes'; +import { createTypeNode } from './typedef'; import { addLeadingJSDocComment, type Comments, isType, ots } from './utils'; /** * Convert an unknown value to an expression. - * @param value - the unknown value. - * @param unescape - if string should be unescaped. + * @param identifiers - list of keys that are treated as identifiers. * @param shorthand - if shorthand syntax is allowed. - * @param indentifier - list of keys that are treated as indentifiers. + * @param unescape - if string should be unescaped. + * @param value - the unknown value. * @returns ts.Expression */ export const toExpression = ({ - value, - unescape = false, - shorthand = false, identifiers = [], + shorthand = false, + unescape = false, + value, }: { - value: T; - unescape?: boolean; - shorthand?: boolean; identifiers?: string[]; + shorthand?: boolean; + unescape?: boolean; + value: T; }): ts.Expression | undefined => { if (value === null) { return ts.factory.createNull(); @@ -46,6 +48,36 @@ export const toExpression = ({ } }; +/** + * Create Function type expression. + */ +export const createFunction = ({ + comment, + multiLine, + parameters = [], + returnType, + statements = [], +}: { + comment?: Comments; + multiLine?: boolean; + parameters?: FunctionParameter[]; + returnType?: string | ts.TypeNode; + statements?: ts.Statement[]; +}) => { + const expression = ts.factory.createArrowFunction( + undefined, + undefined, + toParameterDeclarations(parameters), + returnType ? createTypeNode(returnType) : undefined, + undefined, + ts.factory.createBlock(statements, multiLine), + ); + if (comment) { + addLeadingJSDocComment(expression, comment); + } + return expression; +}; + /** * Create Array type expression. * @param arr - The array to create. @@ -65,6 +97,18 @@ export const createArrayType = ({ (!Array.isArray(arr[0]) && typeof arr[0] === 'object') || multiLine, ); +export type ObjectValue = + | { spread: string } + | { + key: string; + value: any; + }; + +type ObjectAssignment = + | ts.PropertyAssignment + | ts.ShorthandPropertyAssignment + | ts.SpreadAssignment; + /** * Create Object type expression. * @param comments - comments to add to each property. @@ -75,7 +119,9 @@ export const createArrayType = ({ * @param unescape - if properties strings should be unescaped. * @returns ts.ObjectLiteralExpression */ -export const createObjectType = ({ +export const createObjectType = < + T extends Record | Array, +>({ comments, identifiers = [], leadingComment, @@ -92,49 +138,115 @@ export const createObjectType = ({ shorthand?: boolean; unescape?: boolean; }): ts.ObjectLiteralExpression => { - const properties = Object.entries(obj) - .map(([key, value]) => { - // Pass all object properties as identifiers if the whole object is a indentifier - let initializer: ts.Expression | undefined = toExpression({ - identifiers: identifiers.includes(key) ? Object.keys(value) : [], - shorthand, - unescape, - value, - }); - if (!initializer) { - return undefined; - } - // Create a identifier if the current key is one and it is not an object - if ( - identifiers.includes(key) && - !ts.isObjectLiteralExpression(initializer) - ) { - initializer = ts.factory.createIdentifier(value as string); - } - // Check key value equality before possibly modifying it - const hasShorthandSupport = key === value; - if ( - key.match(/^[0-9]/) && - key.match(/\D+/g) && - !key.startsWith("'") && - !key.endsWith("'") - ) { - key = `'${key}'`; - } - if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) { - key = `'${key}'`; - } - const assignment = - shorthand && hasShorthandSupport - ? ts.factory.createShorthandPropertyAssignment(value) - : ts.factory.createPropertyAssignment(key, initializer); - const comment = comments?.[key]; - if (comment) { - addLeadingJSDocComment(assignment, comment); - } - return assignment; - }) - .filter(isType); + const properties = Array.isArray(obj) + ? obj + .map((value: ObjectValue) => { + // Check key value equality before possibly modifying it + let canShorthand = false; + if ('key' in value) { + let { key } = value; + canShorthand = key === value.value; + if ( + key.match(/^[0-9]/) && + key.match(/\D+/g) && + !key.startsWith("'") && + !key.endsWith("'") + ) { + key = `'${key}'`; + } + if ( + key.match(/\W/g) && + !key.startsWith("'") && + !key.endsWith("'") + ) { + key = `'${key}'`; + } + } + let assignment: ObjectAssignment; + if ('spread' in value) { + assignment = ts.factory.createSpreadAssignment( + ts.factory.createIdentifier(value.spread), + ); + } else if (shorthand && canShorthand) { + assignment = ts.factory.createShorthandPropertyAssignment( + value.value, + ); + } else { + let initializer: ts.Expression | undefined = toExpression({ + identifiers: identifiers.includes(value.key) + ? Object.keys(value.value) + : [], + shorthand, + unescape, + value: value.value, + }); + if (!initializer) { + return undefined; + } + // Create a identifier if the current key is one and it is not an object + if ( + identifiers.includes(value.key) && + !ts.isObjectLiteralExpression(initializer) + ) { + initializer = ts.factory.createIdentifier(value.value as string); + } + assignment = ts.factory.createPropertyAssignment( + value.key, + initializer, + ); + } + if ('key' in value) { + const comment = comments?.[value.key]; + if (comment) { + addLeadingJSDocComment(assignment, comment); + } + } + return assignment; + }) + .filter(isType) + : Object.entries(obj) + .map(([key, value]) => { + // Pass all object properties as identifiers if the whole object is an identifier + let initializer: ts.Expression | undefined = toExpression({ + identifiers: identifiers.includes(key) ? Object.keys(value) : [], + shorthand, + unescape, + value, + }); + if (!initializer) { + return undefined; + } + // Create a identifier if the current key is one and it is not an object + if ( + identifiers.includes(key) && + !ts.isObjectLiteralExpression(initializer) + ) { + initializer = ts.factory.createIdentifier(value as string); + } + // Check key value equality before possibly modifying it + const canShorthand = key === value; + if ( + key.match(/^[0-9]/) && + key.match(/\D+/g) && + !key.startsWith("'") && + !key.endsWith("'") + ) { + key = `'${key}'`; + } + if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) { + key = `'${key}'`; + } + const assignment = + shorthand && canShorthand + ? ts.factory.createShorthandPropertyAssignment(value) + : ts.factory.createPropertyAssignment(key, initializer); + const comment = comments?.[key]; + if (comment) { + addLeadingJSDocComment(assignment, comment); + } + return assignment; + }) + .filter(isType); const expression = ts.factory.createObjectLiteralExpression( properties as any[], @@ -150,22 +262,22 @@ export const createObjectType = ({ /** * Create enum declaration. Example `export enum T = { X, Y };` + * @param comments - comments to add to each property of enum. + * @param leadingComment - leading comment to add to enum. * @param name - the name of the enum. * @param obj - the object representing the enum. - * @param leadingComment - leading comment to add to enum. - * @param comments - comments to add to each property of enum. * @returns */ export const createEnumDeclaration = ({ + comments, + leadingComment, name, obj, - leadingComment, - comments, }: { + comments?: Record; + leadingComment?: Comments; name: string; obj: T; - leadingComment?: Comments; - comments?: Record; }): ts.EnumDeclaration => { const declaration = ts.factory.createEnumDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts index 2e206c207..4758d68a5 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts @@ -11,16 +11,16 @@ import type { OpenApiServer } from './OpenApiServer'; * {@link} https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object */ export interface OpenApiOperation { - tags?: string[]; - summary?: string; + callbacks?: Dictionary; + deprecated?: boolean; description?: string; externalDocs?: OpenApiExternalDocs; operationId?: string; parameters?: OpenApiParameter[]; requestBody?: OpenApiRequestBody; responses: OpenApiResponses; - callbacks?: Dictionary; - deprecated?: boolean; security?: OpenApiSecurityRequirement[]; servers?: OpenApiServer[]; + summary?: string; + tags?: string[]; } diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts index e21605f8e..a4535b73f 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts @@ -10,6 +10,8 @@ export const getOperationRequestBody = ( openApi: OpenApi, body: OpenApiRequestBody, ): OperationParameter => { + const name = body['x-body-name'] ?? 'requestBody'; + const requestBody: OperationParameter = { $refs: [], base: 'unknown', @@ -26,8 +28,8 @@ export const getOperationRequestBody = ( isRequired: body.required === true, link: null, mediaType: null, - name: body['x-body-name'] ?? 'requestBody', - prop: body['x-body-name'] ?? 'requestBody', + name, + prop: name, properties: [], template: null, type: 'unknown', diff --git a/packages/openapi-ts/src/utils/sort.ts b/packages/openapi-ts/src/utils/sort.ts index 51a13b18f..809e6c944 100644 --- a/packages/openapi-ts/src/utils/sort.ts +++ b/packages/openapi-ts/src/utils/sort.ts @@ -4,6 +4,9 @@ export function sort(a: string, b: string): number { return nameA.localeCompare(nameB, 'en'); } +export const sorterByName = (a: T, b: T) => + sort(a.name, b.name); + export function sortByName(items: T[]): T[] { - return items.sort((a, b) => sort(a.name, b.name)); + return items.sort(sorterByName); } diff --git a/packages/openapi-ts/src/utils/write/schemas.ts b/packages/openapi-ts/src/utils/write/schemas.ts index e3d87b505..e3c17ee3b 100644 --- a/packages/openapi-ts/src/utils/write/schemas.ts +++ b/packages/openapi-ts/src/utils/write/schemas.ts @@ -54,7 +54,11 @@ export const processSchemas = async ({ const validName = `$${ensureValidTypeScriptJavaScriptIdentifier(name)}`; const obj = ensureValidSchemaOutput(schema); const expression = compiler.types.object({ obj }); - const statement = compiler.export.asConst(validName, expression); + const statement = compiler.export.const({ + constAssertion: true, + expression, + name: validName, + }); file.add(statement); }; diff --git a/packages/openapi-ts/src/utils/write/services.ts b/packages/openapi-ts/src/utils/write/services.ts index 266cd21e0..80225b86a 100644 --- a/packages/openapi-ts/src/utils/write/services.ts +++ b/packages/openapi-ts/src/utils/write/services.ts @@ -4,8 +4,10 @@ import { ClassElement, compiler, FunctionParameter, + type Node, TypeScriptFile, } from '../../compiler'; +import type { ObjectValue } from '../../compiler/types'; import type { Operation, OperationParameter, Service } from '../../openApi'; import type { Client } from '../../types/client'; import { getConfig } from '../config'; @@ -14,6 +16,7 @@ import { modelIsRequired } from '../required'; import { transformServiceName } from '../transform'; import { unique } from '../unique'; +type OnNode = (node: Node) => void; type OnImport = (importedType: string) => void; export const operationDataTypeName = (operation: Operation) => @@ -22,25 +25,22 @@ export const operationDataTypeName = (operation: Operation) => export const operationResponseTypeName = (operation: Operation) => `${camelcase(operation.name, { pascalCase: true })}Response`; -const toOperationParamType = ( - operation: Operation, - onImport: OnImport, -): FunctionParameter[] => { - const config = getConfig(); - - const importedType = operationDataTypeName(operation); - +const toOperationParamType = (operation: Operation): FunctionParameter[] => { if (!operation.parameters.length) { return []; } - onImport(importedType); + const config = getConfig(); + + const importedType = operationDataTypeName(operation); if (config.useOptions) { - const isOptional = operation.parameters.every((p) => !p.isRequired); + const isRequired = operation.parameters.some( + (parameter) => parameter.isRequired, + ); return [ { - default: isOptional ? {} : undefined, + default: isRequired ? undefined : {}, name: 'data', type: importedType, }, @@ -58,26 +58,29 @@ const toOperationParamType = ( }); }; -const toOperationReturnType = (operation: Operation, onImport: OnImport) => { +const toOperationReturnType = (operation: Operation) => { const config = getConfig(); - const results = operation.results; + + let returnType = compiler.typedef.basic('void'); + // TODO: we should return nothing when results don't exist // can't remove this logic without removing request/name config // as it complicates things - let returnType = compiler.typedef.basic('void'); - if (results.length) { + if (operation.results.length) { const importedType = operationResponseTypeName(operation); - onImport(importedType); returnType = compiler.typedef.union([importedType]); } + if (config.useOptions && config.services.response === 'response') { returnType = compiler.typedef.basic('ApiResult', [returnType]); } + if (config.client === 'angular') { returnType = compiler.typedef.basic('Observable', [returnType]); } else { returnType = compiler.typedef.basic('CancelablePromise', [returnType]); } + return returnType; }; @@ -116,6 +119,22 @@ const toOperationComment = (operation: Operation) => { const toRequestOptions = (operation: Operation) => { const config = getConfig(); + + if (config.client.startsWith('@hey-api')) { + const obj: ObjectValue[] = [ + { + spread: 'data', + }, + { + key: 'url', + value: operation.path, + }, + ]; + return compiler.types.object({ + obj, + }); + } + const toObj = (parameters: OperationParameter[]) => parameters.reduce( (prev, curr) => { @@ -137,21 +156,27 @@ const toRequestOptions = (operation: Operation) => { method: operation.method, url: operation.path, }; + if (operation.parametersPath.length) { obj.path = toObj(operation.parametersPath); } + if (operation.parametersCookie.length) { obj.cookies = toObj(operation.parametersCookie); } + if (operation.parametersHeader.length) { obj.headers = toObj(operation.parametersHeader); } + if (operation.parametersQuery.length) { obj.query = toObj(operation.parametersQuery); } + if (operation.parametersForm.length) { obj.formData = toObj(operation.parametersForm); } + if (operation.parametersBody) { if (operation.parametersBody.in === 'formData') { if (config.useOptions) { @@ -168,12 +193,15 @@ const toRequestOptions = (operation: Operation) => { } } } + if (operation.parametersBody?.mediaType) { obj.mediaType = operation.parametersBody?.mediaType; } + if (operation.responseHeader) { obj.responseHeader = operation.responseHeader; } + if (operation.errors.length) { const errors: Record = {}; operation.errors.forEach((err) => { @@ -181,8 +209,9 @@ const toRequestOptions = (operation: Operation) => { }); obj.errors = errors; } + return compiler.types.object({ - identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'], + identifiers: ['body', 'cookies', 'formData', 'headers', 'path', 'query'], obj, shorthand: true, }); @@ -190,45 +219,91 @@ const toRequestOptions = (operation: Operation) => { const toOperationStatements = (operation: Operation) => { const config = getConfig(); - const statements: any[] = []; - const requestOptions = toRequestOptions(operation); + + const options = toRequestOptions(operation); + + if (config.client.startsWith('@hey-api')) { + const returnType = operation.results.length + ? operationResponseTypeName(operation) + : 'void'; + return [ + compiler.return.functionCall({ + args: [options], + name: `client.${operation.method.toLocaleLowerCase()}`, + types: [returnType], + }), + ]; + } + if (config.name) { - statements.push( - compiler.class.return({ - args: [requestOptions], + return [ + compiler.return.functionCall({ + args: [options], name: 'this.httpRequest.request', }), - ); - } else { - if (config.client === 'angular') { - statements.push( - compiler.class.return({ - args: ['OpenAPI', 'this.http', requestOptions], - name: '__request', - }), - ); - } else { - statements.push( - compiler.class.return({ - args: ['OpenAPI', requestOptions], - name: '__request', - }), - ); - } + ]; + } + + if (config.client === 'angular') { + return [ + compiler.return.functionCall({ + args: ['OpenAPI', 'this.http', options], + name: '__request', + }), + ]; } - return statements; + + return [ + compiler.return.functionCall({ + args: ['OpenAPI', options], + name: '__request', + }), + ]; }; -export const processService = (service: Service, onImport: OnImport) => { +export const processService = ( + service: Service, + onNode: OnNode, + onImport: OnImport, +) => { const config = getConfig(); + + service.operations.forEach((operation) => { + if (operation.parameters.length) { + const importedType = operationDataTypeName(operation); + onImport(importedType); + } + + if (operation.results.length) { + const importedType = operationResponseTypeName(operation); + onImport(importedType); + } + }); + + if (config.client.startsWith('@hey-api')) { + service.operations.forEach((operation) => { + const expression = compiler.types.function({ + parameters: toOperationParamType(operation), + statements: toOperationStatements(operation), + }); + const statement = compiler.export.const({ + comment: toOperationComment(operation), + expression, + name: operation.name, + }); + onNode(statement); + }); + return; + } + const members: ClassElement[] = service.operations.map((operation) => { const node = compiler.class.method({ accessLevel: 'public', comment: toOperationComment(operation), isStatic: config.name === undefined && config.client !== 'angular', name: operation.name, - parameters: toOperationParamType(operation, onImport), - returnType: toOperationReturnType(operation, onImport), + parameters: toOperationParamType(operation), + returnType: toOperationReturnType(operation), statements: toOperationStatements(operation), }); return node; @@ -265,7 +340,7 @@ export const processService = (service: Service, onImport: OnImport) => { ); } - return compiler.class.create({ + const statement = compiler.class.create({ decorator: config.client === 'angular' ? { args: [{ providedIn: 'root' }], name: 'Injectable' } @@ -273,6 +348,7 @@ export const processService = (service: Service, onImport: OnImport) => { members, name: transformServiceName(service.name), }); + onNode(statement); }; export const processServices = async ({ @@ -282,9 +358,7 @@ export const processServices = async ({ client: Client; files: Record; }): Promise => { - const file = files.services; - - if (!file) { + if (!files.services) { return; } @@ -293,57 +367,54 @@ export const processServices = async ({ let imports: string[] = []; for (const service of client.services) { - const serviceClass = processService(service, (importedType) => { - imports = [...imports, importedType]; - }); - file.add(serviceClass); + processService( + service, + (node) => { + files.services?.add(node); + }, + (importedType) => { + imports = [...imports, importedType]; + }, + ); } // Import required packages and core files. - if (config.client === 'angular') { - file.addNamedImport('Injectable', '@angular/core'); + if (config.client.startsWith('@hey-api')) { + files.services?.addImport(['client'], config.client); + } else { + if (config.client === 'angular') { + files.services?.addImport('Injectable', '@angular/core'); - if (!config.name) { - file.addNamedImport('HttpClient', '@angular/common/http'); - } + if (!config.name) { + files.services?.addImport('HttpClient', '@angular/common/http'); + } - file.addNamedImport({ isTypeOnly: true, name: 'Observable' }, 'rxjs'); - } else { - if (config.client.startsWith('@hey-api')) { - file.addNamedImport( - { isTypeOnly: true, name: 'CancelablePromise' }, - config.client, + files.services?.addImport( + { isTypeOnly: true, name: 'Observable' }, + 'rxjs', ); } else { - file.addNamedImport( + files.services?.addImport( { isTypeOnly: true, name: 'CancelablePromise' }, './core/CancelablePromise', ); } - } - if (config.services.response === 'response') { - file.addNamedImport( - { isTypeOnly: true, name: 'ApiResult' }, - './core/ApiResult', - ); - } + if (config.services.response === 'response') { + files.services?.addImport( + { isTypeOnly: true, name: 'ApiResult' }, + './core/ApiResult', + ); + } - if (config.name) { - file.addNamedImport( - { isTypeOnly: config.client !== 'angular', name: 'BaseHttpRequest' }, - './core/BaseHttpRequest', - ); - } else { - if (config.client.startsWith('@hey-api')) { - file.addNamedImport('OpenAPI', config.client); - file.addNamedImport( - { alias: '__request', name: 'request' }, - config.client, + if (config.name) { + files.services?.addImport( + { isTypeOnly: config.client !== 'angular', name: 'BaseHttpRequest' }, + './core/BaseHttpRequest', ); } else { - file.addNamedImport('OpenAPI', './core/OpenAPI'); - file.addNamedImport( + files.services?.addImport('OpenAPI', './core/OpenAPI'); + files.services?.addImport( { alias: '__request', name: 'request' }, './core/request', ); @@ -355,6 +426,6 @@ export const processServices = async ({ const models = imports .filter(unique) .map((name) => ({ isTypeOnly: true, name })); - file.addNamedImport(models, `./${files.types.getName(false)}`); + files.services?.addImport(models, `./${files.types.getName(false)}`); } }; diff --git a/packages/openapi-ts/src/utils/write/types.ts b/packages/openapi-ts/src/utils/write/types.ts index 70ba02e78..41a42ee4a 100644 --- a/packages/openapi-ts/src/utils/write/types.ts +++ b/packages/openapi-ts/src/utils/write/types.ts @@ -10,7 +10,7 @@ import type { Client } from '../../types/client'; import { getConfig } from '../config'; import { enumKey, enumName, enumUnionType, enumValue } from '../enum'; import { escapeComment } from '../escape'; -import { sortByName } from '../sort'; +import { sortByName, sorterByName } from '../sort'; import { transformTypeName } from '../transform'; import { operationDataTypeName, operationResponseTypeName } from './services'; import { toType } from './type'; @@ -105,7 +105,11 @@ const processEnum = ( obj: properties, unescape: true, }); - const node = compiler.export.asConst(name, expression); + const node = compiler.export.const({ + constAssertion: true, + expression, + name, + }); onNode(node); } }; @@ -145,6 +149,8 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { const pathsMap = new Map(); + const config = getConfig(); + client.services.forEach((service) => { service.operations.forEach((operation) => { const hasReq = operation.parameters.length; @@ -165,7 +171,72 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { } if (hasReq) { - methodMap.set('req', sortByName([...operation.parameters])); + const bodyParameter = operation.parameters + .filter((parameter) => parameter.in === 'body') + .sort(sorterByName)[0]; + const bodyParameters: OperationParameter = { + ...emptyModel, + ...bodyParameter, + in: 'body', + isRequired: bodyParameter ? bodyParameter.isRequired : false, + // mediaType: null, + name: 'body', + prop: 'body', + }; + const headerParameters: OperationParameter = { + ...emptyModel, + in: 'header', + isRequired: operation.parameters + .filter((parameter) => parameter.in === 'header') + .some((parameter) => parameter.isRequired), + mediaType: null, + name: 'header', + prop: 'header', + properties: operation.parameters + .filter((parameter) => parameter.in === 'header') + .sort(sorterByName), + }; + const pathParameters: OperationParameter = { + ...emptyModel, + in: 'path', + isRequired: operation.parameters + .filter((parameter) => parameter.in === 'path') + .some((parameter) => parameter.isRequired), + mediaType: null, + name: 'path', + prop: 'path', + properties: operation.parameters + .filter((parameter) => parameter.in === 'path') + .sort(sorterByName), + }; + const queryParameters: OperationParameter = { + ...emptyModel, + in: 'query', + isRequired: operation.parameters + .filter((parameter) => parameter.in === 'query') + .some((parameter) => parameter.isRequired), + mediaType: null, + name: 'query', + prop: 'query', + properties: operation.parameters + .filter((parameter) => parameter.in === 'query') + .sort(sorterByName), + }; + const operationProperties = config.client.startsWith('@hey-api') + ? [ + bodyParameters, + headerParameters, + pathParameters, + queryParameters, + ].filter( + (param) => + param.properties.length || + param.$refs.length || + param.mediaType, + ) + : sortByName([...operation.parameters]); + + methodMap.set('req', operationProperties); // create type export for operation data const name = operationDataTypeName(operation); @@ -173,9 +244,8 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { client.serviceTypes = [...client.serviceTypes, name]; const type = toType({ ...emptyModel, - export: 'interface', isRequired: true, - properties: sortByName([...operation.parameters]), + properties: operationProperties, }); const node = compiler.typedef.alias(name, type); onNode(node); @@ -259,7 +329,6 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { const reqResKey: Model = { ...emptyModel, - export: 'interface', isRequired: true, name, properties: reqResParameters, @@ -269,7 +338,6 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { ); const methodKey: Model = { ...emptyModel, - export: 'interface', isRequired: true, name: method.toLocaleLowerCase(), properties: methodParameters, @@ -278,7 +346,6 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { }); const pathKey: Model = { ...emptyModel, - export: 'interface', isRequired: true, name: `'${path}'`, properties: pathParameters, @@ -288,7 +355,6 @@ const processServiceTypes = (client: Client, onNode: OnNode) => { const type = toType({ ...emptyModel, - export: 'interface', properties, }); const namespace = serviceExportedNamespace();