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.

chore: port asClass functionality

Lubos 24db8ca7 5613ac0c

+139 -420
+2 -2
dev/openapi-ts.config.ts
··· 54 54 // 'sdk-instance.yaml', 55 55 // 'sdk-method-class-conflict.yaml', 56 56 // 'sdk-nested-classes.yaml', 57 - // 'sdk-nested-conflict.yaml', 57 + 'sdk-nested-conflict.yaml', 58 58 // 'string-with-format.yaml', 59 59 // 'transformers.json', 60 60 // 'transformers-recursive.json', ··· 274 274 // }, 275 275 }, 276 276 { 277 - // asClass: true, 277 + asClass: true, 278 278 // auth: false, 279 279 // classNameBuilder: '{{name}}', 280 280 classNameBuilder: '{{name}}Service',
+56 -34
packages/openapi-ts/src/plugins/@hey-api/sdk/model/class.ts
··· 1 + import type { SymbolMeta } from '@hey-api/codegen-core'; 2 + 1 3 import type { IR } from '~/ir/types'; 2 4 import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; 3 5 import { ··· 66 68 } 67 69 68 70 /** 71 + * Gets the full path of this class in the hierarchy. 72 + * 73 + * @returns An array of class names from the root to this class 74 + */ 75 + getPath(): ReadonlyArray<string> { 76 + const path: Array<string> = []; 77 + // eslint-disable-next-line @typescript-eslint/no-this-alias 78 + let cursor: SdkClassModel | undefined = this; 79 + while (cursor) { 80 + path.unshift(cursor.name); 81 + cursor = cursor.parent; 82 + } 83 + return path; 84 + } 85 + 86 + /** 69 87 * Inserts an operation into the class tree. 70 88 * 71 89 * Parses the operation ID and creates the class hierarchy. ··· 107 125 meta: { 108 126 category: 'utility', 109 127 resource: 'class', 110 - resourceId: this.name, 128 + resourceId: this.getPath().join('.'), 111 129 tool: 'sdk', 112 130 }, 113 131 }, 114 132 ); 133 + const metaClient: SymbolMeta = { 134 + category: 'utility', 135 + resource: 'class', 136 + resourceId: 'HeyApiClient', 137 + tool: 'sdk', 138 + }; 115 139 const node = $.class(symbolClass) 116 140 .export() 117 - .extends( 118 - plugin.referenceSymbol({ 119 - category: 'utility', 120 - resource: 'class', 121 - resourceId: 'HeyApiClient', 122 - tool: 'sdk', 123 - }), 141 + .$if(plugin.config.instance, (c) => 142 + c.extends(plugin.referenceSymbol(metaClient)), 124 143 ) 125 144 .$if(isAngularClient && this.isRoot, (c) => 126 145 c.decorator( ··· 132 151 ), 133 152 ); 134 153 135 - if (this.isRoot) { 136 - const symbolClient = plugin.symbol('HeyApiClient', { 137 - meta: { 138 - category: 'utility', 139 - resource: 'class', 140 - resourceId: 'HeyApiClient', 141 - tool: 'sdk', 142 - }, 143 - }); 154 + if (this.isRoot && plugin.config.instance) { 155 + const symbolClient = plugin.symbol('HeyApiClient', { meta: metaClient }); 144 156 const clientNode = createClientClass({ plugin, symbol: symbolClient }); 145 157 dependencies.push(clientNode); 146 158 const symbolRegistry = plugin.symbol('HeyApiRegistry', { ··· 272 284 const refChild = plugin.referenceSymbol({ 273 285 category: 'utility', 274 286 resource: 'class', 275 - resourceId: child.name, 287 + resourceId: child.getPath().join('.'), 276 288 tool: 'sdk', 277 289 }); 278 290 const memberName = toCase(refChild.name, 'camelCase'); 279 - const privateName = plugin.symbol(`_${memberName}`); 280 - const getterName = plugin.symbol(memberName); 281 - node.field(privateName, (f) => f.private().optional().type(refChild)); 282 - node.do( 283 - $.getter(getterName, (g) => 284 - g.returns(refChild).do( 285 - $('this') 286 - .attr(privateName) 287 - .nullishAssign( 288 - $.new(refChild).args( 289 - $.object().prop('client', $('this').attr('client')), 290 - ), 291 - ) 292 - .return(), 291 + if (plugin.config.instance) { 292 + const privateName = plugin.symbol(`_${memberName}`); 293 + const getterName = plugin.symbol(memberName); 294 + node.field(privateName, (f) => f.private().optional().type(refChild)); 295 + node.do( 296 + $.getter(getterName, (g) => 297 + g.returns(refChild).do( 298 + $('this') 299 + .attr(privateName) 300 + .nullishAssign( 301 + $.new(refChild).args( 302 + $.object().prop('client', $('this').attr('client')), 303 + ), 304 + ) 305 + .return(), 306 + ), 293 307 ), 294 - ), 295 - ); 308 + ); 309 + } else { 310 + node.do( 311 + plugin.isSymbolRegistered(refChild.id) 312 + ? $.field(memberName, (f) => f.static().assign($(refChild))) 313 + : $.getter(memberName, (g) => 314 + g.public().static().do($.return(refChild)), 315 + ), 316 + ); 317 + } 296 318 } 297 319 298 320 return { dependencies, node };
+59
packages/openapi-ts/src/plugins/@hey-api/sdk/model/structure.ts
··· 1 + import type { IR } from '~/ir/types'; 2 + 3 + import type { HeyApiSdkPlugin } from '../types'; 4 + import { SdkClassModel } from './class'; 5 + 6 + export class SdkStructureModel { 7 + /** Name of the SDK. If empty, we fallback to operation tags. */ 8 + private _name: string; 9 + 10 + /** Root classes mapped by their names. */ 11 + roots: Map<string, SdkClassModel> = new Map(); 12 + 13 + constructor(name: string) { 14 + this._name = name; 15 + } 16 + 17 + /** 18 + * Inserts an operation into the structure. 19 + * 20 + * Parses the operation ID and organizes it into classes based on tags. 21 + */ 22 + insert( 23 + operation: IR.OperationObject, 24 + plugin: HeyApiSdkPlugin['Instance'], 25 + ): void { 26 + const roots = this._name ? [this._name] : (operation.tags ?? ['default']); 27 + 28 + for (const name of roots) { 29 + const model = this.root(name); 30 + model.insert(operation, plugin); 31 + } 32 + } 33 + 34 + /** 35 + * Gets or creates a root class by name. 36 + * 37 + * If the root doesn't exist, it's created automatically. 38 + * 39 + * @param name - The name of the root class 40 + * @returns The root class instance 41 + */ 42 + root(name: string): SdkClassModel { 43 + if (!this.roots.has(name)) { 44 + this.roots.set(name, new SdkClassModel(name)); 45 + } 46 + return this.roots.get(name)!; 47 + } 48 + 49 + /** 50 + * Recursively walks the structure. 51 + * 52 + * Yields all classes in the structure. 53 + */ 54 + *walk(): Generator<SdkClassModel> { 55 + for (const model of this.roots.values()) { 56 + yield* model.walk(); 57 + } 58 + } 59 + }
+20 -384
packages/openapi-ts/src/plugins/@hey-api/sdk/shared/class.ts
··· 1 1 import type { Symbol } from '@hey-api/codegen-core'; 2 2 3 - import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; 4 - import { 5 - createOperationComment, 6 - isOperationOptionsRequired, 7 - } from '~/plugins/shared/utils/operation'; 8 - import type { TsDsl } from '~/ts-dsl'; 9 3 import { $ } from '~/ts-dsl'; 10 - import { toCase } from '~/utils/to-case'; 11 4 12 - import { SdkClassModel } from '../model/class'; 5 + import { SdkStructureModel } from '../model/structure'; 13 6 import type { HeyApiSdkPlugin } from '../types'; 14 - import { nuxtTypeComposable, nuxtTypeDefault } from './constants'; 15 - import { 16 - operationClasses, 17 - operationParameters, 18 - operationStatements, 19 - } from './operation'; 20 - 21 - type SdkClassEntry = { 22 - /** 23 - * Name of the class. 24 - */ 25 - className: string; 26 - /** 27 - * Class names for child classes located inside this class. 28 - */ 29 - classes: Set<string>; 30 - /** 31 - * Symbol ID for the class. 32 - */ 33 - id: number; 34 - /** 35 - * Track unique added method nodes. 36 - */ 37 - methods: Set<string>; 38 - /** 39 - * List of class nodes containing methods. 40 - */ 41 - nodes: Array<TsDsl>; 42 - /** 43 - * Is this a root class? 44 - */ 45 - root: boolean; 46 - }; 47 7 48 8 export const registryName = '__registry'; 49 9 ··· 155 115 }: { 156 116 plugin: HeyApiSdkPlugin['Instance']; 157 117 }): void => { 158 - const client = getClientPlugin(plugin.context.config); 159 - const isAngularClient = client.name === '@hey-api/client-angular'; 160 - const isNuxtClient = client.name === '@hey-api/client-nuxt'; 161 - const sdkClasses = new Map<string, SdkClassEntry>(); 162 - /** 163 - * Track unique added classes. 164 - */ 165 - const generatedClasses = new Set<string>(); 166 - 167 - const sdkModel = plugin.config.instance 168 - ? new SdkClassModel(plugin.config.instance) 169 - : undefined; 118 + const structure = new SdkStructureModel(plugin.config.instance); 170 119 171 120 plugin.forEach( 172 121 'operation', 173 122 ({ operation }) => { 174 - if (sdkModel) { 175 - sdkModel.insert(operation, plugin); 176 - } else { 177 - const isRequiredOptions = isOperationOptionsRequired({ 178 - context: plugin.context, 179 - operation, 180 - }); 181 - const symbolResponse = isNuxtClient 182 - ? plugin.querySymbol({ 183 - category: 'type', 184 - resource: 'operation', 185 - resourceId: operation.id, 186 - role: 'response', 187 - }) 188 - : undefined; 189 - 190 - const classes = operationClasses({ operation, plugin }); 191 - 192 - for (const entry of classes.values()) { 193 - entry.path.forEach((currentClassName, index) => { 194 - const symbolCurrentClass = plugin.referenceSymbol({ 195 - category: 'utility', 196 - resource: 'class', 197 - resourceId: currentClassName, 198 - tool: 'sdk', 199 - }); 200 - if (!sdkClasses.has(symbolCurrentClass.meta!.resourceId!)) { 201 - sdkClasses.set(symbolCurrentClass.meta!.resourceId!, { 202 - className: symbolCurrentClass.meta!.resourceId!, 203 - classes: new Set(), 204 - id: symbolCurrentClass.id, 205 - methods: new Set(), 206 - nodes: [], 207 - root: !index, 208 - }); 209 - } 210 - 211 - const parentClassName = entry.path[index - 1]; 212 - if (parentClassName) { 213 - const symbolParentClass = plugin.referenceSymbol({ 214 - category: 'utility', 215 - resource: 'class', 216 - resourceId: parentClassName, 217 - tool: 'sdk', 218 - }); 219 - if ( 220 - symbolParentClass.meta?.resourceId !== 221 - symbolCurrentClass.meta?.resourceId 222 - ) { 223 - const parentClass = sdkClasses.get( 224 - symbolParentClass.meta!.resourceId!, 225 - )!; 226 - parentClass.classes.add(symbolCurrentClass.meta!.resourceId!); 227 - sdkClasses.set( 228 - symbolParentClass.meta!.resourceId!, 229 - parentClass, 230 - ); 231 - } 232 - } 233 - 234 - const isLast = entry.path.length === index + 1; 235 - // add methods only to the last class 236 - if (!isLast) { 237 - return; 238 - } 239 - 240 - const currentClass = sdkClasses.get( 241 - symbolCurrentClass.meta!.resourceId!, 242 - )!; 243 - 244 - const methodName = entry.methodName; 245 - if (currentClass.methods.has(methodName)) return; 246 - currentClass.methods.add(methodName); 247 - 248 - const opParameters = operationParameters({ 249 - isRequiredOptions, 250 - operation, 251 - plugin, 252 - }); 253 - const statements = operationStatements({ 254 - isRequiredOptions, 255 - opParameters, 256 - operation, 257 - plugin, 258 - }); 259 - const functionNode = $.method(methodName, (m) => 260 - m 261 - .$if(createOperationComment(operation), (m, v) => m.doc(v)) 262 - .public() 263 - .static(!isAngularClient && !plugin.config.instance) 264 - .$if( 265 - isNuxtClient, 266 - (m) => 267 - m 268 - .generic(nuxtTypeComposable, (t) => 269 - t 270 - .extends( 271 - plugin.referenceSymbol({ 272 - category: 'external', 273 - resource: 'client.Composable', 274 - }), 275 - ) 276 - .default($.type.literal('$fetch')), 277 - ) 278 - .generic(nuxtTypeDefault, (t) => 279 - t.$if(symbolResponse, (t, s) => 280 - t.extends(s).default(s), 281 - ), 282 - ), 283 - (m) => 284 - m.generic('ThrowOnError', (t) => 285 - t 286 - .extends('boolean') 287 - .default( 288 - ('throwOnError' in client.config 289 - ? client.config.throwOnError 290 - : false) ?? false, 291 - ), 292 - ), 293 - ) 294 - .params(...opParameters.parameters) 295 - .do(...statements), 296 - ); 297 - 298 - if (!currentClass.nodes.length) { 299 - currentClass.nodes.push(functionNode); 300 - } else { 301 - currentClass.nodes.push($.newline(), functionNode); 302 - } 303 - 304 - sdkClasses.set(symbolCurrentClass.meta!.resourceId!, currentClass); 305 - }); 306 - } 307 - } 123 + structure.insert(operation, plugin); 308 124 }, 309 - { 310 - order: 'declarations', 311 - }, 125 + { order: 'declarations' }, 312 126 ); 313 127 314 - if (!sdkModel) { 315 - const clientIndex = plugin.config.instance ? plugin.node(null) : undefined; 316 - const symbolClient = 317 - clientIndex !== undefined 318 - ? plugin.symbol('HeyApiClient', { 319 - meta: { 320 - category: 'utility', 321 - resource: 'class', 322 - resourceId: 'HeyApiClient', 323 - tool: 'sdk', 324 - }, 325 - }) 326 - : undefined; 327 - const registryIndex = plugin.config.instance 328 - ? plugin.node(null) 329 - : undefined; 330 - 331 - const generateClass = (currentClass: SdkClassEntry) => { 332 - const resourceId = currentClass.className; 333 - 334 - if (generatedClasses.has(resourceId)) return; 335 - generatedClasses.add(resourceId); 336 - 337 - if (clientIndex !== undefined && symbolClient && !symbolClient.node) { 338 - const node = createClientClass({ plugin, symbol: symbolClient }); 339 - plugin.node(node, clientIndex); 340 - } 341 - 342 - for (const childClassName of currentClass.classes) { 343 - const childClass = sdkClasses.get(childClassName)!; 344 - generateClass(childClass); 345 - 346 - const refChildClass = plugin.referenceSymbol({ 347 - category: 'utility', 348 - resource: 'class', 349 - resourceId: childClass.className, 350 - tool: 'sdk', 351 - }); 352 - 353 - const originalMemberName = toCase( 354 - refChildClass.meta!.resourceId!, 355 - 'camelCase', 356 - ); 357 - // avoid collisions with existing method names 358 - let memberName = originalMemberName; 359 - if (currentClass.methods.has(memberName)) { 360 - let index = 2; 361 - let attempt = `${memberName}${index}`; 362 - while (currentClass.methods.has(attempt)) { 363 - attempt = `${memberName}${index++}`; 364 - } 365 - memberName = attempt; 366 - } 367 - currentClass.methods.add(memberName); 368 - 369 - if (currentClass.nodes.length > 0) { 370 - currentClass.nodes.push($.newline()); 371 - } 372 - 373 - if (plugin.config.instance) { 374 - const privateName = plugin.symbol(`_${memberName}`); 375 - const privateNode = $.field(privateName, (f) => 376 - f.private().optional().type(refChildClass), 377 - ); 378 - currentClass.nodes.push(privateNode); 379 - const getterNode = $.getter(memberName, (g) => 380 - g.returns(refChildClass).do( 381 - $('this') 382 - .attr(privateName) 383 - .nullishAssign( 384 - $.new(refChildClass).args( 385 - $.object().prop('client', $('this').attr('client')), 386 - ), 387 - ) 388 - .return(), 389 - ), 390 - ); 391 - currentClass.nodes.push(getterNode); 392 - } else { 393 - const subClassReferenceNode = plugin.isSymbolRegistered( 394 - refChildClass.id, 395 - ) 396 - ? $.field(memberName, (f) => f.static().assign($(refChildClass))) 397 - : $.getter(memberName, (g) => 398 - g.public().static().do($.return(refChildClass)), 399 - ); 400 - currentClass.nodes.push(subClassReferenceNode); 401 - } 402 - } 403 - 404 - const symbol = plugin.symbol(resourceId, { 405 - meta: { 406 - category: 'utility', 407 - resource: 'class', 408 - resourceId, 409 - tool: 'sdk', 410 - }, 411 - }); 412 - 413 - if (currentClass.root && registryIndex !== undefined) { 414 - const symClient = plugin.getSymbol({ category: 'client' }); 415 - const isClientRequired = !plugin.config.client || !symClient; 416 - const symbolClient = plugin.referenceSymbol({ 417 - category: 'external', 418 - resource: 'client.Client', 419 - }); 420 - const ctor = $.init((i) => 421 - i 422 - .param('args', (p) => 423 - p.required(isClientRequired).type( 424 - $.type 425 - .object() 426 - .prop('client', (p) => 427 - p.required(isClientRequired).type(symbolClient), 428 - ) 429 - .prop('key', (p) => p.optional().type('string')), 430 - ), 431 - ) 432 - .do( 433 - $('super').call('args'), 434 - $(symbol) 435 - .attr(registryName) 436 - .attr('set') 437 - .call('this', $('args').attr('key').required(isClientRequired)), 438 - ), 439 - ); 128 + const allDependencies: Array<ReturnType<typeof $.class>> = []; 129 + const allNodes: Array<ReturnType<typeof $.class>> = []; 440 130 441 - if (!currentClass.nodes.length) { 442 - currentClass.nodes.unshift(ctor); 443 - } else { 444 - currentClass.nodes.unshift(ctor, $.newline()); 445 - } 131 + for (const model of structure.walk()) { 132 + const { dependencies, node } = model.toNode(plugin); 133 + allDependencies.push(...dependencies); 134 + allNodes.push(node); 135 + } 446 136 447 - const symbolRegistry = plugin.symbol('HeyApiRegistry', { 448 - meta: { 449 - category: 'utility', 450 - resource: 'class', 451 - resourceId: 'HeyApiRegistry', 452 - tool: 'sdk', 453 - }, 454 - }); 455 - const node = createRegistryClass({ 456 - plugin, 457 - sdkSymbol: symbol, 458 - symbol: symbolRegistry, 459 - }); 460 - plugin.node(node, registryIndex); 461 - const registryNode = $.field(registryName, (f) => 462 - f 463 - .public() 464 - .static() 465 - .readonly() 466 - .assign($.new(symbolRegistry).generic(symbol)), 467 - ); 468 - currentClass.nodes.unshift(registryNode, $.newline()); 469 - } 137 + const uniqueDependencies = new Map<number, ReturnType<typeof $.class>>(); 138 + for (const dep of allDependencies) { 139 + if (dep.symbol) uniqueDependencies.set(dep.symbol.id, dep); 140 + } 141 + for (const dep of uniqueDependencies.values()) { 142 + plugin.node(dep); 143 + } 470 144 471 - const node = $.class(symbol) 472 - .export() 473 - .extends(symbolClient) 474 - .$if(isAngularClient && currentClass.root, (c) => 475 - c.decorator( 476 - plugin.referenceSymbol({ 477 - category: 'external', 478 - resource: '@angular/core.Injectable', 479 - }), 480 - $.object().prop('providedIn', $.literal('root')), 481 - ), 482 - ) 483 - .do(...currentClass.nodes); 484 - plugin.node(node); 485 - }; 486 - 487 - for (const sdkClass of sdkClasses.values()) { 488 - generateClass(sdkClass); 489 - } 490 - } else { 491 - const allDependencies: Array<ReturnType<typeof $.class>> = []; 492 - const allNodes: Array<ReturnType<typeof $.class>> = []; 493 - 494 - for (const model of sdkModel.walk()) { 495 - const { dependencies, node } = model.toNode(plugin); 496 - allDependencies.push(...dependencies); 497 - allNodes.push(node); 498 - } 499 - 500 - const uniqueDeps = new Map<number, ReturnType<typeof $.class>>(); 501 - for (const dep of allDependencies) { 502 - if (dep.symbol) uniqueDeps.set(dep.symbol.id, dep); 503 - } 504 - for (const dep of uniqueDeps.values()) { 505 - plugin.node(dep); 506 - } 507 - 508 - for (const node of allNodes) { 509 - plugin.node(node); 510 - } 145 + for (const node of allNodes) { 146 + plugin.node(node); 511 147 } 512 148 };
+2
packages/openapi-ts/src/plugins/@hey-api/sdk/shared/operation.ts
··· 66 66 67 67 /** 68 68 * Returns a list of classes where this operation appears in the generated SDK. 69 + * 70 + * @deprecated 69 71 */ 70 72 export const operationClasses = ({ 71 73 operation,