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 #384 from hey-api/feat/move-services-to-compiler-api

feat: generate services using compiler api

authored by

Jordan Shatford and committed by
GitHub
d1b6c231 17539bcf

+451 -254
-4
packages/openapi-ts/rollup.config.ts
··· 24 24 const templateSpec = handlebars.precompile(template, { 25 25 knownHelpers: { 26 26 camelCase: true, 27 - dataDestructure: true, 28 27 equals: true, 29 28 ifdef: true, 30 - nameOperationDataType: true, 31 29 notEquals: true, 32 - toOperationComment: true, 33 - toRequestOptions: true, 34 30 useDateType: true, 35 31 }, 36 32 knownHelpersOnly: true,
+202
packages/openapi-ts/src/compiler/classes.ts
··· 1 + import ts from 'typescript'; 2 + 3 + import { createTypeNode } from './typedef'; 4 + import { toExpression } from './types'; 5 + import { addLeadingComment, Comments, isType } from './utils'; 6 + 7 + type AccessLevel = 'public' | 'protected' | 'private'; 8 + 9 + export type FunctionParameter = { 10 + accessLevel?: AccessLevel; 11 + default?: any; 12 + isReadOnly?: boolean; 13 + isRequired?: boolean; 14 + name: string; 15 + type: any | ts.TypeNode; 16 + }; 17 + 18 + /** 19 + * Convert AccessLevel to proper TypeScript compiler API modifier. 20 + * @param access - the access level. 21 + * @returns ts.ModifierLike[] 22 + */ 23 + const toAccessLevelModifiers = (access?: AccessLevel): ts.ModifierLike[] => { 24 + const keyword = 25 + access === 'public' 26 + ? ts.SyntaxKind.PublicKeyword 27 + : access === 'protected' 28 + ? ts.SyntaxKind.ProtectedKeyword 29 + : access === 'private' 30 + ? ts.SyntaxKind.PrivateKeyword 31 + : undefined; 32 + const modifiers: ts.ModifierLike[] = []; 33 + if (keyword) { 34 + modifiers.push(ts.factory.createModifier(keyword)); 35 + } 36 + return modifiers; 37 + }; 38 + 39 + /** 40 + * Convert parameters to the declaration array expected by compiler API. 41 + * @param parameters - the parameters to conver to declarations 42 + * @returns ts.ParameterDeclaration[] 43 + */ 44 + const toParameterDeclarations = (parameters: FunctionParameter[]) => 45 + parameters.map(p => { 46 + const modifiers = toAccessLevelModifiers(p.accessLevel); 47 + if (p.isReadOnly) { 48 + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); 49 + } 50 + return ts.factory.createParameterDeclaration( 51 + modifiers, 52 + undefined, 53 + ts.factory.createIdentifier(p.name), 54 + p.isRequired !== undefined && !p.isRequired 55 + ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) 56 + : undefined, 57 + p.type !== undefined ? createTypeNode(p.type) : undefined, 58 + p.default !== undefined ? toExpression({ value: p.default }) : undefined 59 + ); 60 + }); 61 + 62 + /** 63 + * Create a class constructor declaration. 64 + * @param accessLevel - the access level of the constructor. 65 + * @param comment - comment to add to function. 66 + * @param multiLine - if it should be multi line. 67 + * @param parameters - parameters for the constructor. 68 + * @param statements - statements to put in the contructor body. 69 + * @returns ts.ConstructorDeclaration 70 + */ 71 + export const createConstructorDeclaration = ({ 72 + accessLevel = undefined, 73 + comment = undefined, 74 + multiLine = true, 75 + parameters = [], 76 + statements = [], 77 + }: { 78 + accessLevel?: AccessLevel; 79 + comment?: Comments; 80 + multiLine?: boolean; 81 + parameters?: FunctionParameter[]; 82 + statements?: ts.Statement[]; 83 + }) => { 84 + const node = ts.factory.createConstructorDeclaration( 85 + toAccessLevelModifiers(accessLevel), 86 + toParameterDeclarations(parameters), 87 + ts.factory.createBlock(statements, multiLine) 88 + ); 89 + if (comment?.length) { 90 + addLeadingComment(node, comment); 91 + } 92 + return node; 93 + }; 94 + 95 + /** 96 + * Create a class method declaration. 97 + * @param accessLevel - the access level of the method. 98 + * @param comment - comment to add to function. 99 + * @param isStatic - if the function is static. 100 + * @param multiLine - if it should be multi line. 101 + * @param name - name of the method. 102 + * @param parameters - parameters for the method. 103 + * @param returnType - the return type of the method. 104 + * @param statements - statements to put in the contructor body. 105 + * @returns ts.MethodDeclaration 106 + */ 107 + export const createMethodDeclaration = ({ 108 + accessLevel = undefined, 109 + comment = undefined, 110 + isStatic = false, 111 + multiLine = true, 112 + name, 113 + parameters = [], 114 + returnType = undefined, 115 + statements = [], 116 + }: { 117 + accessLevel?: AccessLevel; 118 + comment?: Comments; 119 + isStatic?: boolean; 120 + multiLine?: boolean; 121 + name: string; 122 + parameters?: FunctionParameter[]; 123 + returnType?: string | ts.TypeNode; 124 + statements?: ts.Statement[]; 125 + }) => { 126 + const modifiers = toAccessLevelModifiers(accessLevel); 127 + if (isStatic) { 128 + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); 129 + } 130 + const node = ts.factory.createMethodDeclaration( 131 + modifiers, 132 + undefined, 133 + ts.factory.createIdentifier(name), 134 + undefined, 135 + [], 136 + toParameterDeclarations(parameters), 137 + returnType ? createTypeNode(returnType) : undefined, 138 + ts.factory.createBlock(statements, multiLine) 139 + ); 140 + if (comment?.length) { 141 + addLeadingComment(node, comment); 142 + } 143 + return node; 144 + }; 145 + 146 + type ClassDecorator = { 147 + name: string; 148 + args: any[]; 149 + }; 150 + 151 + /** 152 + * Create a class declaration. 153 + * @param decorator - the class decorator 154 + * @param members - elements in the class. 155 + * @param name - name of the class. 156 + * @returns ts.ClassDeclaration 157 + */ 158 + export const createClassDeclaration = ({ 159 + decorator = undefined, 160 + members = [], 161 + name, 162 + }: { 163 + decorator?: ClassDecorator; 164 + members?: ts.ClassElement[]; 165 + name: string; 166 + }) => { 167 + const modifiers: ts.ModifierLike[] = [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)]; 168 + if (decorator) { 169 + modifiers.unshift( 170 + ts.factory.createDecorator( 171 + ts.factory.createCallExpression( 172 + ts.factory.createIdentifier(decorator.name), 173 + undefined, 174 + decorator.args.map(arg => toExpression({ value: arg })).filter(isType<ts.Expression>) 175 + ) 176 + ) 177 + ); 178 + } 179 + // Add newline between each class member. 180 + const m: ts.ClassElement[] = []; 181 + members.forEach(member => { 182 + m.push(member); 183 + // @ts-ignore 184 + m.push(ts.factory.createIdentifier('\n')); 185 + }); 186 + return ts.factory.createClassDeclaration(modifiers, ts.factory.createIdentifier(name), [], [], m); 187 + }; 188 + 189 + /** 190 + * Create a return function call. Example `return call(param);`. 191 + * @param args - arguments to pass to the function. 192 + * @param name - name of the function to call. 193 + * @returns ts.ReturnStatement 194 + */ 195 + export const createReturnFunctionCall = ({ args = [], name }: { args: any[]; name: string }) => 196 + ts.factory.createReturnStatement( 197 + ts.factory.createCallExpression( 198 + ts.factory.createIdentifier(name), 199 + undefined, 200 + args.map(arg => ts.factory.createIdentifier(arg)).filter(isType<ts.Identifier>) 201 + ) 202 + );
+11 -2
packages/openapi-ts/src/compiler/index.ts
··· 3 3 4 4 import ts from 'typescript'; 5 5 6 + import * as classes from './classes'; 6 7 import * as module from './module'; 7 8 import * as typedef from './typedef'; 8 9 import * as types from './types'; 9 - import { addLeadingComment, tsNodeToString } from './utils'; 10 + import { addLeadingComment, stringToTsNodes, tsNodeToString } from './utils'; 10 11 12 + export type { FunctionParameter } from './classes'; 11 13 export type { Property } from './typedef'; 12 14 export type { Comments } from './utils'; 13 - export type { Node, TypeNode } from 'typescript'; 15 + export type { ClassElement, Node, TypeNode } from 'typescript'; 14 16 15 17 // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 18 export const generatedFileName = (fileName: string, insertGen = true) => { ··· 75 77 } 76 78 77 79 export const compiler = { 80 + class: { 81 + constructor: classes.createConstructorDeclaration, 82 + create: classes.createClassDeclaration, 83 + method: classes.createMethodDeclaration, 84 + return: classes.createReturnFunctionCall, 85 + }, 78 86 export: { 79 87 all: module.createExportAllDeclaration, 80 88 asConst: module.createExportVariableAsConst, ··· 99 107 object: types.createObjectType, 100 108 }, 101 109 utils: { 110 + toNode: stringToTsNodes, 102 111 toString: tsNodeToString, 103 112 }, 104 113 };
+7 -2
packages/openapi-ts/src/compiler/typedef.ts
··· 2 2 3 3 import { addLeadingComment, type Comments, tsNodeToString } from './utils'; 4 4 5 - export const createTypeNode = (base: any | ts.TypeNode) => 6 - ts.isTypeNode(base) ? base : ts.factory.createTypeReferenceNode(base); 5 + export const createTypeNode = (base: any | ts.TypeNode, args?: (any | ts.TypeNode)[]): ts.TypeNode => 6 + ts.isTypeNode(base) 7 + ? base 8 + : ts.factory.createTypeReferenceNode( 9 + base, 10 + args?.map(arg => createTypeNode(arg)) 11 + ); 7 12 8 13 /** 9 14 * Create a type alias declaration. Example `export type X = Y;`.
+10 -8
packages/openapi-ts/src/compiler/types.ts
··· 10 10 * @param indentifier - list of keys that are treated as indentifiers. 11 11 * @returns ts.Expression 12 12 */ 13 - const toExpression = <T = unknown>({ 13 + export const toExpression = <T = unknown>({ 14 14 value, 15 15 unescape = false, 16 16 shorthand = false, ··· 21 21 shorthand?: boolean; 22 22 identifiers?: string[]; 23 23 }): ts.Expression | undefined => { 24 + if (value === null) { 25 + return ts.factory.createNull(); 26 + } 27 + 24 28 if (Array.isArray(value)) { 25 29 return createArrayType({ arr: value }); 26 30 } 27 31 28 - if (typeof value === 'object' && value !== null) { 32 + if (typeof value === 'object') { 29 33 return createObjectType({ identifiers, obj: value, shorthand }); 30 34 } 31 35 ··· 39 43 40 44 if (typeof value === 'string') { 41 45 return ots.string(value, unescape); 42 - } 43 - 44 - if (value === null) { 45 - return ts.factory.createNull(); 46 46 } 47 47 }; 48 48 ··· 106 106 if (identifiers.includes(key) && !ts.isObjectLiteralExpression(initializer)) { 107 107 initializer = ts.factory.createIdentifier(value as string); 108 108 } 109 + // Check key value equality before possibly modifying it 110 + const hasShorthandSupport = key === value; 109 111 if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) { 110 112 key = `'${key}'`; 111 113 } 112 114 const assignment = 113 - shorthand && key === value 114 - ? ts.factory.createShorthandPropertyAssignment(key) 115 + shorthand && hasShorthandSupport 116 + ? ts.factory.createShorthandPropertyAssignment(value) 115 117 : ts.factory.createPropertyAssignment(key, initializer); 116 118 const c = comments?.[key]; 117 119 if (c?.length) {
+10
packages/openapi-ts/src/compiler/utils.ts
··· 34 34 } 35 35 } 36 36 37 + /** 38 + * Convert a string to a TypeScript Node 39 + * @param s - the string to convert. 40 + * @returns ts.Node 41 + */ 42 + export function stringToTsNodes(s: string): ts.Node { 43 + const file = createSourceFile(s); 44 + return file.statements[0]; 45 + } 46 + 37 47 // ots for openapi-ts is helpers to reduce repetition of basic ts factory functions. 38 48 export const ots = { 39 49 // Create a boolean expression based on value.
-45
packages/openapi-ts/src/templates/exportService.hbs
··· 1 - {{#equals @root.$config.client 'angular'}} 2 - @Injectable({ 3 - providedIn: 'root', 4 - }) 5 - {{/equals}} 6 - export class {{{name}}}{{{@root.$config.postfixServices}}} { 7 - {{#if @root.$config.name}} 8 - constructor(public readonly httpRequest: BaseHttpRequest) {} 9 - 10 - {{else}} 11 - {{#equals @root.$config.client 'angular'}} 12 - constructor(public readonly http: HttpClient) {} 13 - 14 - {{/equals}} 15 - {{/if}} 16 - {{#each operations}} 17 - {{{toOperationComment this}}} 18 - {{#if @root.$config.name}} 19 - {{#equals @root.$config.client 'angular'}} 20 - public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { 21 - {{{dataDestructure this}}} 22 - return this.httpRequest.request({{{toRequestOptions this}}}); 23 - } 24 - {{else}} 25 - public {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { 26 - {{{dataDestructure this}}} 27 - return this.httpRequest.request({{{toRequestOptions this}}}); 28 - } 29 - {{/equals}} 30 - {{else}} 31 - {{#equals @root.$config.client 'angular'}} 32 - public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { 33 - {{{dataDestructure this}}} 34 - return __request(OpenAPI, this.http, {{{toRequestOptions this}}}); 35 - } 36 - {{else}} 37 - public static {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { 38 - {{{dataDestructure this}}} 39 - return __request(OpenAPI, {{{toRequestOptions this}}}); 40 - } 41 - {{/equals}} 42 - {{/if}} 43 - 44 - {{/each}} 45 - }
-3
packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts
··· 28 28 registerHandlebarHelpers(); 29 29 const helpers = Object.keys(Handlebars.helpers); 30 30 expect(helpers).toContain('camelCase'); 31 - expect(helpers).toContain('dataDestructure'); 32 31 expect(helpers).toContain('equals'); 33 32 expect(helpers).toContain('ifdef'); 34 - expect(helpers).toContain('nameOperationDataType'); 35 33 expect(helpers).toContain('notEquals'); 36 34 }); 37 35 }); ··· 58 56 useOptions: false, 59 57 }); 60 58 const templates = registerHandlebarTemplates(); 61 - expect(templates.exports.service).toBeDefined(); 62 59 expect(templates.core.settings).toBeDefined(); 63 60 expect(templates.core.apiError).toBeDefined(); 64 61 expect(templates.core.apiRequestOptions).toBeDefined();
-166
packages/openapi-ts/src/utils/handlebars.ts
··· 1 1 import camelCase from 'camelcase'; 2 2 import Handlebars from 'handlebars/runtime'; 3 3 4 - import { compiler } from '../compiler'; 5 - import { addLeadingComment } from '../compiler/utils'; 6 - import type { Operation, OperationParameter, Service } from '../openApi'; 7 4 import templateClient from '../templates/client.hbs'; 8 5 import angularGetHeaders from '../templates/core/angular/getHeaders.hbs'; 9 6 import angularGetRequestBody from '../templates/core/angular/getRequestBody.hbs'; ··· 48 45 import xhrGetResponseHeader from '../templates/core/xhr/getResponseHeader.hbs'; 49 46 import xhrRequest from '../templates/core/xhr/request.hbs'; 50 47 import xhrSendRequest from '../templates/core/xhr/sendRequest.hbs'; 51 - import templateExportService from '../templates/exportService.hbs'; 52 - import { getConfig } from './config'; 53 - import { escapeComment, escapeDescription, escapeName } from './escape'; 54 - import { getDefaultPrintable, modelIsRequired } from './required'; 55 - 56 - const dataDestructure = (operation: Operation) => { 57 - const config = getConfig(); 58 - 59 - if (config.name) { 60 - if (config.useOptions) { 61 - if (operation.parameters.length) { 62 - return `const { 63 - ${operation.parameters.map(parameter => parameter.name).join(',\n')} 64 - } = data;`; 65 - } 66 - } 67 - } else { 68 - if (config.useOptions) { 69 - if (operation.parameters.length) { 70 - // TODO: extract query parameters from query key 71 - return `const { 72 - ${operation.parameters.map(parameter => parameter.name).join(',\n')} 73 - } = data;`; 74 - } 75 - } 76 - } 77 - return ''; 78 - }; 79 - 80 - export const serviceExportedNamespace = () => '$OpenApiTs'; 81 - 82 - export const nameOperationDataType = (namespace: 'req' | 'res', operation: Service['operations'][number]) => { 83 - const config = getConfig(); 84 - const exported = serviceExportedNamespace(); 85 - const baseTypePath = `${exported}['${operation.path}']['${operation.method.toLocaleLowerCase()}']['${namespace}']`; 86 - if (namespace === 'req') { 87 - if (!operation.parameters.length) { 88 - return ''; 89 - } 90 - 91 - if (config.useOptions) { 92 - const isOptional = operation.parameters.every(p => !p.isRequired); 93 - return isOptional ? `data: ${baseTypePath} = {}` : `data: ${baseTypePath}`; 94 - } 95 - 96 - return operation.parameters 97 - .map(p => { 98 - const typePath = `${baseTypePath}['${p.name}']`; 99 - const defaultValue = getDefaultPrintable(p); 100 - const defaultString = defaultValue !== undefined ? ` = ${defaultValue}` : ''; 101 - return `${p.name}${modelIsRequired(p)}: ${typePath}${defaultString}`; 102 - }) 103 - .join(', '); 104 - } 105 - const results = operation.results.filter(result => result.code >= 200 && result.code < 300); 106 - // TODO: we should return nothing when results don't exist 107 - // can't remove this logic without removing request/name config 108 - // as it complicates things 109 - if (!results.length) { 110 - return compiler.utils.toString(compiler.typedef.basic('void')); 111 - } 112 - const types = results.map(result => `${baseTypePath}[${String(result.code)}]`); 113 - const union = compiler.utils.toString(compiler.typedef.union(types)); 114 - if (config.useOptions && config.serviceResponse === 'response') { 115 - return `ApiResult<${union}>`; 116 - } 117 - return union; 118 - }; 119 48 120 49 export const registerHandlebarHelpers = (): void => { 121 50 Handlebars.registerHelper('camelCase', camelCase); 122 - Handlebars.registerHelper('dataDestructure', dataDestructure); 123 51 124 52 Handlebars.registerHelper( 125 53 'equals', ··· 128 56 } 129 57 ); 130 58 131 - Handlebars.registerHelper('toRequestOptions', (operation: Operation) => { 132 - const toObj = (parameters: OperationParameter[]) => 133 - parameters.reduce( 134 - (prev, curr) => { 135 - const key = curr.prop; 136 - const value = curr.name; 137 - if (key === value) { 138 - prev[key] = key; 139 - } else if (escapeName(key) === key) { 140 - prev[key] = value; 141 - } else { 142 - prev[`'${key}'`] = value; 143 - } 144 - return prev; 145 - }, 146 - {} as Record<string, unknown> 147 - ); 148 - 149 - const obj: Record<string, any> = { 150 - method: operation.method, 151 - url: operation.path, 152 - }; 153 - if (operation.parametersPath.length) { 154 - obj.path = toObj(operation.parametersPath); 155 - } 156 - if (operation.parametersCookie.length) { 157 - obj.cookies = toObj(operation.parametersCookie); 158 - } 159 - if (operation.parametersHeader.length) { 160 - obj.headers = toObj(operation.parametersHeader); 161 - } 162 - if (operation.parametersQuery.length) { 163 - obj.query = toObj(operation.parametersQuery); 164 - } 165 - if (operation.parametersForm.length) { 166 - obj.formData = toObj(operation.parametersForm); 167 - } 168 - if (operation.parametersBody) { 169 - if (operation.parametersBody.in === 'formData') { 170 - obj.formData = operation.parametersBody.name; 171 - } 172 - if (operation.parametersBody.in === 'body') { 173 - obj.body = operation.parametersBody.name; 174 - } 175 - } 176 - if (operation.parametersBody?.mediaType) { 177 - obj.mediaType = operation.parametersBody?.mediaType; 178 - } 179 - if (operation.responseHeader) { 180 - obj.responseHeader = operation.responseHeader; 181 - } 182 - if (operation.errors.length) { 183 - const errors: Record<number, string> = {}; 184 - operation.errors.forEach(err => { 185 - errors[err.code] = escapeDescription(err.description); 186 - }); 187 - obj.errors = errors; 188 - } 189 - return compiler.utils.toString( 190 - compiler.types.object({ 191 - identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'], 192 - obj, 193 - shorthand: true, 194 - }) 195 - ); 196 - }); 197 - 198 - Handlebars.registerHelper('toOperationComment', (operation: Operation) => { 199 - const config = getConfig(); 200 - let params: string[] = []; 201 - if (!config.useOptions && operation.parameters.length) { 202 - params = operation.parameters.map( 203 - p => `@param ${p.name} ${p.description ? escapeComment(p.description) : ''}` 204 - ); 205 - } 206 - const comment = [ 207 - operation.deprecated && '@deprecated', 208 - operation.summary && escapeComment(operation.summary), 209 - operation.description && escapeComment(operation.description), 210 - ...params, 211 - ...operation.results.map(r => `@returns ${r.type} ${r.description ? escapeComment(r.description) : ''}`), 212 - '@throws ApiError', 213 - ]; 214 - return addLeadingComment(undefined, comment, false, true); 215 - }); 216 - 217 59 Handlebars.registerHelper('ifdef', function (this: unknown, ...args): string { 218 60 const options = args.pop(); 219 61 if (!args.every(value => !value)) { ··· 221 63 } 222 64 return options.inverse(this); 223 65 }); 224 - 225 - Handlebars.registerHelper('nameOperationDataType', nameOperationDataType); 226 66 227 67 Handlebars.registerHelper( 228 68 'notEquals', ··· 244 84 request: Handlebars.TemplateDelegate; 245 85 settings: Handlebars.TemplateDelegate; 246 86 }; 247 - exports: { 248 - service: Handlebars.TemplateDelegate; 249 - }; 250 87 } 251 88 252 89 /** ··· 268 105 httpRequest: Handlebars.template(templateCoreHttpRequest), 269 106 request: Handlebars.template(templateCoreRequest), 270 107 settings: Handlebars.template(templateCoreSettings), 271 - }, 272 - exports: { 273 - service: Handlebars.template(templateExportService), 274 108 }, 275 109 }; 276 110
-3
packages/openapi-ts/src/utils/write/__tests__/mocks.ts
··· 14 14 request: vi.fn().mockReturnValue('request'), 15 15 settings: vi.fn().mockReturnValue('settings'), 16 16 }, 17 - exports: { 18 - service: vi.fn().mockReturnValue('service'), 19 - }, 20 17 };
+1 -2
packages/openapi-ts/src/utils/write/__tests__/services.spec.ts
··· 4 4 5 5 import { setConfig } from '../../config'; 6 6 import { writeServices } from '../services'; 7 - import { mockTemplates } from './mocks'; 8 7 import { openApi } from './models'; 9 8 10 9 vi.mock('node:fs'); ··· 46 45 version: 'v1', 47 46 }; 48 47 49 - await writeServices(openApi, '/', client, mockTemplates); 48 + await writeServices(openApi, '/', client); 50 49 51 50 expect(writeFileSync).toHaveBeenCalled(); 52 51 });
+1 -1
packages/openapi-ts/src/utils/write/models.ts
··· 6 6 import { getConfig } from '../config'; 7 7 import { enumKey, enumName, enumUnionType, enumValue } from '../enum'; 8 8 import { escapeComment } from '../escape'; 9 - import { serviceExportedNamespace } from '../handlebars'; 10 9 import { sortByName } from '../sort'; 10 + import { serviceExportedNamespace } from './services'; 11 11 import { toType } from './type'; 12 12 13 13 type OnNode = (node: Node, type?: 'enum') => void;
+209 -18
packages/openapi-ts/src/utils/write/services.ts
··· 1 - import { filePath, generatedFileName, TypeScriptFile } from '../../compiler'; 2 - import type { OpenApi } from '../../openApi'; 1 + import { ClassElement, compiler, filePath, FunctionParameter, generatedFileName, TypeScriptFile } from '../../compiler'; 2 + import type { OpenApi, Operation, OperationParameter, Service } from '../../openApi'; 3 3 import type { Client } from '../../types/client'; 4 4 import { getConfig } from '../config'; 5 - import { serviceExportedNamespace, type Templates } from '../handlebars'; 5 + import { escapeComment, escapeDescription, escapeName } from '../escape'; 6 + import { modelIsRequired } from '../required'; 6 7 import { unique } from '../unique'; 7 8 9 + export const serviceExportedNamespace = () => '$OpenApiTs'; 10 + 11 + const toOperationParamType = (operation: Operation): FunctionParameter[] => { 12 + const config = getConfig(); 13 + const baseTypePath = `${serviceExportedNamespace()}['${operation.path}']['${operation.method.toLocaleLowerCase()}']['req']`; 14 + if (!operation.parameters.length) { 15 + return []; 16 + } 17 + 18 + if (config.useOptions) { 19 + const isOptional = operation.parameters.every(p => !p.isRequired); 20 + return [{ default: isOptional ? {} : undefined, name: 'data', type: baseTypePath }]; 21 + } 22 + 23 + return operation.parameters.map(p => { 24 + const typePath = `${baseTypePath}['${p.name}']`; 25 + return { 26 + default: p?.default, 27 + isRequired: modelIsRequired(p) === '', 28 + name: p.name, 29 + type: typePath, 30 + }; 31 + }); 32 + }; 33 + 34 + const toOperationReturnType = (operation: Operation) => { 35 + const config = getConfig(); 36 + const baseTypePath = `${serviceExportedNamespace()}['${operation.path}']['${operation.method.toLocaleLowerCase()}']['res']`; 37 + const results = operation.results.filter(result => result.code >= 200 && result.code < 300); 38 + // TODO: we should return nothing when results don't exist 39 + // can't remove this logic without removing request/name config 40 + // as it complicates things 41 + let returnType = compiler.typedef.basic('void'); 42 + if (results.length) { 43 + const types = results.map(result => `${baseTypePath}[${String(result.code)}]`); 44 + returnType = compiler.typedef.union(types); 45 + } 46 + if (config.useOptions && config.serviceResponse === 'response') { 47 + returnType = compiler.typedef.basic('ApiResult', [returnType]); 48 + } 49 + if (config.client === 'angular') { 50 + returnType = compiler.typedef.basic('Observable', [returnType]); 51 + } else { 52 + returnType = compiler.typedef.basic('CancelablePromise', [returnType]); 53 + } 54 + return returnType; 55 + }; 56 + 57 + const toOperationComment = (operation: Operation) => { 58 + const config = getConfig(); 59 + let params: string[] = []; 60 + if (!config.useOptions && operation.parameters.length) { 61 + params = operation.parameters.map(p => `@param ${p.name} ${p.description ? escapeComment(p.description) : ''}`); 62 + } 63 + const comment = [ 64 + operation.deprecated && '@deprecated', 65 + operation.summary && escapeComment(operation.summary), 66 + operation.description && escapeComment(operation.description), 67 + ...params, 68 + ...operation.results.map(r => `@returns ${r.type} ${r.description ? escapeComment(r.description) : ''}`), 69 + '@throws ApiError', 70 + ]; 71 + return comment; 72 + }; 73 + 74 + const toRequestOptions = (operation: Operation) => { 75 + const toObj = (parameters: OperationParameter[]) => 76 + parameters.reduce( 77 + (prev, curr) => { 78 + const key = curr.prop; 79 + const value = curr.name; 80 + if (key === value) { 81 + prev[key] = key; 82 + } else if (escapeName(key) === key) { 83 + prev[key] = value; 84 + } else { 85 + prev[`'${key}'`] = value; 86 + } 87 + return prev; 88 + }, 89 + {} as Record<string, unknown> 90 + ); 91 + 92 + const obj: Record<string, any> = { 93 + method: operation.method, 94 + url: operation.path, 95 + }; 96 + if (operation.parametersPath.length) { 97 + obj.path = toObj(operation.parametersPath); 98 + } 99 + if (operation.parametersCookie.length) { 100 + obj.cookies = toObj(operation.parametersCookie); 101 + } 102 + if (operation.parametersHeader.length) { 103 + obj.headers = toObj(operation.parametersHeader); 104 + } 105 + if (operation.parametersQuery.length) { 106 + obj.query = toObj(operation.parametersQuery); 107 + } 108 + if (operation.parametersForm.length) { 109 + obj.formData = toObj(operation.parametersForm); 110 + } 111 + if (operation.parametersBody) { 112 + if (operation.parametersBody.in === 'formData') { 113 + obj.formData = operation.parametersBody.name; 114 + } 115 + if (operation.parametersBody.in === 'body') { 116 + obj.body = operation.parametersBody.name; 117 + } 118 + } 119 + if (operation.parametersBody?.mediaType) { 120 + obj.mediaType = operation.parametersBody?.mediaType; 121 + } 122 + if (operation.responseHeader) { 123 + obj.responseHeader = operation.responseHeader; 124 + } 125 + if (operation.errors.length) { 126 + const errors: Record<number, string> = {}; 127 + operation.errors.forEach(err => { 128 + errors[err.code] = escapeDescription(err.description); 129 + }); 130 + obj.errors = errors; 131 + } 132 + return compiler.types.object({ 133 + identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'], 134 + obj, 135 + shorthand: true, 136 + }); 137 + }; 138 + 139 + export const toDestructuredData = (operation: Operation) => { 140 + const config = getConfig(); 141 + if (!config.useOptions || !operation.parameters.length) { 142 + return ''; 143 + } 144 + const obj: Record<string, unknown> = {}; 145 + operation.parameters.forEach(p => { 146 + obj[p.name] = p.name; 147 + }); 148 + const node = compiler.types.object({ identifiers: Object.keys(obj), obj, shorthand: true }); 149 + return `const ${compiler.utils.toString(node)} = data;`; 150 + }; 151 + 152 + const toOperationStatements = (operation: Operation) => { 153 + const config = getConfig(); 154 + const statements: any[] = []; 155 + // If using options we destructor the parameter 156 + if (config.useOptions && operation.parameters.length) { 157 + statements.push(compiler.utils.toNode(toDestructuredData(operation))); 158 + } 159 + 160 + const requestOptions = compiler.utils.toString(toRequestOptions(operation)); 161 + if (config.name) { 162 + statements.push(compiler.class.return({ args: [requestOptions], name: 'this.httpRequest.request' })); 163 + } else { 164 + if (config.client === 'angular') { 165 + statements.push( 166 + compiler.class.return({ args: ['OpenAPI', 'this.http', requestOptions], name: '__request' }) 167 + ); 168 + } else { 169 + statements.push(compiler.class.return({ args: ['OpenAPI', requestOptions], name: '__request' })); 170 + } 171 + } 172 + return statements; 173 + }; 174 + 175 + export const processService = (service: Service) => { 176 + const config = getConfig(); 177 + const members: ClassElement[] = service.operations.map(operation => { 178 + const node = compiler.class.method({ 179 + accessLevel: 'public', 180 + comment: toOperationComment(operation), 181 + isStatic: config.name === undefined && config.client !== 'angular', 182 + name: operation.name, 183 + parameters: toOperationParamType(operation), 184 + returnType: toOperationReturnType(operation), 185 + statements: toOperationStatements(operation), 186 + }); 187 + return node; 188 + }); 189 + 190 + // Push to front constructor if needed 191 + if (config.name) { 192 + members.unshift( 193 + compiler.class.constructor({ 194 + parameters: [{ accessLevel: 'public', isReadOnly: true, name: 'httpRequest', type: 'BaseHttpRequest' }], 195 + }) 196 + ); 197 + } else if (config.client === 'angular') { 198 + members.unshift( 199 + compiler.class.constructor({ 200 + parameters: [{ accessLevel: 'public', isReadOnly: true, name: 'http', type: 'HttpClient' }], 201 + }) 202 + ); 203 + } 204 + 205 + return compiler.class.create({ 206 + decorator: config.client === 'angular' ? { args: [{ providedIn: 'root' }], name: 'Injectable' } : undefined, 207 + members, 208 + name: `${service.name}${config.postfixServices}`, 209 + }); 210 + }; 211 + 8 212 /** 9 213 * Generate Services using the Handlebar template and write to disk. 10 214 * @param openApi {@link OpenApi} Dereferenced OpenAPI specification 11 215 * @param outputPath Directory to write the generated files to 12 216 * @param client Client containing models, schemas, and services 13 - * @param templates The loaded handlebar templates 14 217 */ 15 - export const writeServices = async ( 16 - openApi: OpenApi, 17 - outputPath: string, 18 - client: Client, 19 - templates: Templates 20 - ): Promise<void> => { 218 + export const writeServices = async (openApi: OpenApi, outputPath: string, client: Client): Promise<void> => { 21 219 const config = getConfig(); 22 220 23 221 const fileServices = new TypeScriptFile({ path: filePath(outputPath, 'services.ts') }); 24 222 25 223 let imports: string[] = []; 26 - let results: string[] = []; 27 224 28 225 for (const service of client.services) { 29 - const result = templates.exports.service({ 30 - $config: config, 31 - ...service, 32 - }); 226 + fileServices.add(processService(service)); 33 227 const exported = serviceExportedNamespace(); 34 228 imports = [...imports, exported]; 35 - results = [...results, result]; 36 229 } 37 230 38 231 // Import required packages and core files. ··· 61 254 // Import all models required by the services. 62 255 const models = imports.filter(unique).map(imp => ({ isTypeOnly: true, name: imp })); 63 256 fileServices.addNamedImport(models, generatedFileName('./models')); 64 - 65 - fileServices.add(...results); 66 257 67 258 if (config.exportServices) { 68 259 fileServices.write('\n\n');