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 #1312 from hey-api/refactor/context-events

refactor: plugins subscribe to context events

authored by

Lubos and committed by
GitHub
3e514d64 ecd90872

+1760 -901
+3
packages/openapi-ts/src/generate/output.ts
··· 2 2 3 3 import { compiler } from '../compiler'; 4 4 import type { IRContext } from '../ir/context'; 5 + import { parseIR } from '../ir/parser'; 5 6 import type { OpenApi } from '../openApi'; 6 7 import type { Client } from '../types/client'; 7 8 import type { Files } from '../types/utils'; ··· 117 118 plugin: plugin as never, 118 119 }); 119 120 } 121 + 122 + await parseIR({ context }); 120 123 121 124 const indexFile = context.createFile({ 122 125 id: '_index',
+83 -1
packages/openapi-ts/src/ir/context.ts
··· 4 4 import type { Config } from '../types/config'; 5 5 import type { Files } from '../types/utils'; 6 6 import { resolveRef } from '../utils/ref'; 7 - import type { IR } from './ir'; 7 + import type { 8 + IR, 9 + IROperationObject, 10 + IRParameterObject, 11 + IRPathItemObject, 12 + IRSchemaObject, 13 + } from './ir'; 8 14 9 15 interface ContextFile { 10 16 /** ··· 19 25 path: string; 20 26 } 21 27 28 + interface Events { 29 + /** 30 + * Called after parsing. 31 + */ 32 + after: () => void; 33 + /** 34 + * Called before parsing. 35 + */ 36 + before: () => void; 37 + operation: (args: { 38 + method: keyof IRPathItemObject; 39 + operation: IROperationObject; 40 + path: string; 41 + }) => void; 42 + parameter: (args: { 43 + $ref: string; 44 + name: string; 45 + parameter: IRParameterObject; 46 + }) => void; 47 + schema: (args: { 48 + $ref: string; 49 + name: string; 50 + schema: IRSchemaObject; 51 + }) => void; 52 + } 53 + 54 + type Listeners = { 55 + [T in keyof Events]?: Array<Events[T]>; 56 + }; 57 + 22 58 export class IRContext<Spec extends Record<string, any> = any> { 23 59 /** 24 60 * Configuration for parsing and generating the output. This ··· 38 74 */ 39 75 public spec: Spec; 40 76 77 + /** 78 + * A map of event listeners. 79 + */ 80 + private listeners: Listeners; 81 + 41 82 constructor({ config, spec }: { config: Config; spec: Spec }) { 42 83 this.config = config; 43 84 this.files = {}; 44 85 this.ir = {}; 86 + this.listeners = {}; 45 87 this.spec = spec; 88 + } 89 + 90 + /** 91 + * Notify all event listeners about `event`. 92 + */ 93 + public async broadcast<T extends keyof Events>( 94 + event: T, 95 + ...args: Parameters<Events[T]> 96 + ): Promise<void> { 97 + if (!this.listeners[event]) { 98 + return; 99 + } 100 + 101 + await Promise.all( 102 + this.listeners[event].map((callbackFn, index) => { 103 + try { 104 + // @ts-expect-error 105 + const response = callbackFn(...args); 106 + return Promise.resolve(response); 107 + } catch (error) { 108 + console.error( 109 + `🔥 Event broadcast: "${event}"\nindex: ${index}\narguments: ${JSON.stringify(args, null, 2)}`, 110 + ); 111 + throw error; 112 + } 113 + }), 114 + ); 46 115 } 47 116 48 117 /** ··· 90 159 $ref, 91 160 spec: this.spec, 92 161 }); 162 + } 163 + 164 + /** 165 + * Register a new `event` listener. 166 + */ 167 + public subscribe<T extends keyof Events>( 168 + event: T, 169 + callbackFn: Events[T], 170 + ): void { 171 + if (!this.listeners[event]) { 172 + this.listeners[event] = []; 173 + } 174 + this.listeners[event].push(callbackFn); 93 175 } 94 176 }
+1 -1
packages/openapi-ts/src/ir/ir.d.ts
··· 15 15 [path: `/${string}`]: IRPathItemObject; 16 16 } 17 17 18 - interface IRPathItemObject { 18 + export interface IRPathItemObject { 19 19 delete?: IROperationObject; 20 20 get?: IROperationObject; 21 21 head?: IROperationObject;
+36
packages/openapi-ts/src/ir/parser.ts
··· 1 + import type { IRContext } from './context'; 2 + import type { IRPathItemObject, IRPathsObject } from './ir'; 3 + 4 + /** 5 + * Traverse the parsed intermediate representation model and broadcast 6 + * various events to listeners. 7 + */ 8 + export const parseIR = async ({ context }: { context: IRContext }) => { 9 + await context.broadcast('before'); 10 + 11 + if (context.ir.components) { 12 + for (const name in context.ir.components.schemas) { 13 + const schema = context.ir.components.schemas[name]; 14 + const $ref = `#/components/schemas/${name}`; 15 + await context.broadcast('schema', { $ref, name, schema }); 16 + } 17 + 18 + for (const name in context.ir.components.parameters) { 19 + const parameter = context.ir.components.parameters[name]; 20 + const $ref = `#/components/parameters/${name}`; 21 + await context.broadcast('parameter', { $ref, name, parameter }); 22 + } 23 + } 24 + 25 + for (const path in context.ir.paths) { 26 + const pathItem = context.ir.paths[path as keyof IRPathsObject]; 27 + 28 + for (const _method in pathItem) { 29 + const method = _method as keyof IRPathItemObject; 30 + const operation = pathItem[method]!; 31 + await context.broadcast('operation', { method, operation, path }); 32 + } 33 + } 34 + 35 + await context.broadcast('after'); 36 + };
+164 -180
packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts
··· 7 7 clientOptionsTypeName, 8 8 } from '../../../generate/client'; 9 9 import type { IRContext } from '../../../ir/context'; 10 - import type { 11 - IROperationObject, 12 - IRPathItemObject, 13 - IRPathsObject, 14 - } from '../../../ir/ir'; 10 + import type { IROperationObject } from '../../../ir/ir'; 15 11 import { hasOperationDataRequired } from '../../../ir/operation'; 16 12 import { camelCase } from '../../../utils/camelCase'; 17 13 import { escapeComment } from '../../../utils/escape'; ··· 198 194 199 195 const services = new Map<string, Array<ts.MethodDeclaration>>(); 200 196 201 - for (const path in context.ir.paths) { 202 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 203 - 204 - for (const _method in pathItem) { 205 - const method = _method as keyof IRPathItemObject; 206 - const operation = pathItem[method]!; 207 - 208 - const identifierData = context.file({ id: 'types' })!.identifier({ 209 - $ref: operationIrRef({ id: operation.id, type: 'data' }), 210 - namespace: 'type', 197 + context.subscribe('operation', ({ operation, method, path }) => { 198 + const identifierData = context.file({ id: 'types' })!.identifier({ 199 + $ref: operationIrRef({ id: operation.id, type: 'data' }), 200 + namespace: 'type', 201 + }); 202 + if (identifierData.name) { 203 + file.import({ 204 + asType: true, 205 + module: typesModule, 206 + name: identifierData.name, 211 207 }); 212 - if (identifierData.name) { 213 - file.import({ 214 - asType: true, 215 - module: typesModule, 216 - name: identifierData.name, 217 - }); 218 - } 208 + } 219 209 220 - const identifierError = context.file({ id: 'types' })!.identifier({ 221 - $ref: operationIrRef({ id: operation.id, type: 'error' }), 222 - namespace: 'type', 210 + const identifierError = context.file({ id: 'types' })!.identifier({ 211 + $ref: operationIrRef({ id: operation.id, type: 'error' }), 212 + namespace: 'type', 213 + }); 214 + if (identifierError.name) { 215 + file.import({ 216 + asType: true, 217 + module: typesModule, 218 + name: identifierError.name, 223 219 }); 224 - if (identifierError.name) { 225 - file.import({ 226 - asType: true, 227 - module: typesModule, 228 - name: identifierError.name, 229 - }); 230 - } 220 + } 231 221 232 - const identifierResponse = context.file({ id: 'types' })!.identifier({ 233 - $ref: operationIrRef({ id: operation.id, type: 'response' }), 234 - namespace: 'type', 222 + const identifierResponse = context.file({ id: 'types' })!.identifier({ 223 + $ref: operationIrRef({ id: operation.id, type: 'response' }), 224 + namespace: 'type', 225 + }); 226 + if (identifierResponse.name) { 227 + file.import({ 228 + asType: true, 229 + module: typesModule, 230 + name: identifierResponse.name, 235 231 }); 236 - if (identifierResponse.name) { 237 - file.import({ 238 - asType: true, 239 - module: typesModule, 240 - name: identifierResponse.name, 241 - }); 242 - } 232 + } 243 233 244 - const node = compiler.methodDeclaration({ 245 - accessLevel: 'public', 246 - comment: [ 247 - operation.deprecated && '@deprecated', 248 - operation.summary && escapeComment(operation.summary), 249 - operation.description && escapeComment(operation.description), 250 - ], 251 - isStatic: true, 252 - name: serviceFunctionIdentifier({ 234 + const node = compiler.methodDeclaration({ 235 + accessLevel: 'public', 236 + comment: [ 237 + operation.deprecated && '@deprecated', 238 + operation.summary && escapeComment(operation.summary), 239 + operation.description && escapeComment(operation.description), 240 + ], 241 + isStatic: true, 242 + name: serviceFunctionIdentifier({ 243 + config: context.config, 244 + handleIllegal: false, 245 + id: operation.id, 246 + operation, 247 + }), 248 + parameters: [ 249 + { 250 + isRequired: hasOperationDataRequired(operation), 251 + name: 'options', 252 + type: operationOptionsType({ 253 + importedType: identifierData.name, 254 + throwOnError: 'ThrowOnError', 255 + }), 256 + }, 257 + ], 258 + returnType: undefined, 259 + statements: [ 260 + compiler.returnFunctionCall({ 261 + args: [ 262 + requestOptions({ 263 + context, 264 + operation, 265 + path, 266 + }), 267 + ], 268 + name: `(options?.client ?? client).${method}`, 269 + types: [ 270 + identifierResponse.name || 'unknown', 271 + identifierError.name || 'unknown', 272 + 'ThrowOnError', 273 + ], 274 + }), 275 + ], 276 + types: [ 277 + { 278 + default: false, 279 + extends: 'boolean', 280 + name: 'ThrowOnError', 281 + }, 282 + ], 283 + }); 284 + 285 + const uniqueTags = Array.from(new Set(operation.tags)); 286 + if (!uniqueTags.length) { 287 + uniqueTags.push('default'); 288 + } 289 + 290 + for (const tag of uniqueTags) { 291 + const serviceName = getServiceName(tag); 292 + const nodes = services.get(serviceName) ?? []; 293 + nodes.push(node); 294 + services.set(serviceName, nodes); 295 + } 296 + }); 297 + 298 + context.subscribe('after', () => { 299 + for (const [serviceName, nodes] of services) { 300 + const node = compiler.classDeclaration({ 301 + decorator: undefined, 302 + members: nodes, 303 + name: transformServiceName({ 253 304 config: context.config, 254 - handleIllegal: false, 255 - id: operation.id, 256 - operation, 305 + name: serviceName, 257 306 }), 307 + }); 308 + file.add(node); 309 + } 310 + }); 311 + }; 312 + 313 + const generateFlatServices = ({ context }: { context: IRContext }) => { 314 + const file = context.file({ id: servicesId })!; 315 + const typesModule = file.relativePathToFile({ context, id: 'types' }); 316 + 317 + context.subscribe('operation', ({ operation, method, path }) => { 318 + const identifierData = context.file({ id: 'types' })!.identifier({ 319 + $ref: operationIrRef({ id: operation.id, type: 'data' }), 320 + namespace: 'type', 321 + }); 322 + if (identifierData.name) { 323 + file.import({ 324 + asType: true, 325 + module: typesModule, 326 + name: identifierData.name, 327 + }); 328 + } 329 + 330 + const identifierError = context.file({ id: 'types' })!.identifier({ 331 + $ref: operationIrRef({ id: operation.id, type: 'error' }), 332 + namespace: 'type', 333 + }); 334 + if (identifierError.name) { 335 + file.import({ 336 + asType: true, 337 + module: typesModule, 338 + name: identifierError.name, 339 + }); 340 + } 341 + 342 + const identifierResponse = context.file({ id: 'types' })!.identifier({ 343 + $ref: operationIrRef({ id: operation.id, type: 'response' }), 344 + namespace: 'type', 345 + }); 346 + if (identifierResponse.name) { 347 + file.import({ 348 + asType: true, 349 + module: typesModule, 350 + name: identifierResponse.name, 351 + }); 352 + } 353 + 354 + const node = compiler.constVariable({ 355 + comment: [ 356 + operation.deprecated && '@deprecated', 357 + operation.summary && escapeComment(operation.summary), 358 + operation.description && escapeComment(operation.description), 359 + ], 360 + exportConst: true, 361 + expression: compiler.arrowFunction({ 258 362 parameters: [ 259 363 { 260 364 isRequired: hasOperationDataRequired(operation), ··· 290 394 name: 'ThrowOnError', 291 395 }, 292 396 ], 293 - }); 294 - 295 - const uniqueTags = Array.from(new Set(operation.tags)); 296 - if (!uniqueTags.length) { 297 - uniqueTags.push('default'); 298 - } 299 - 300 - for (const tag of uniqueTags) { 301 - const serviceName = getServiceName(tag); 302 - const nodes = services.get(serviceName) ?? []; 303 - nodes.push(node); 304 - services.set(serviceName, nodes); 305 - } 306 - } 307 - } 308 - 309 - for (const [serviceName, nodes] of services) { 310 - const node = compiler.classDeclaration({ 311 - decorator: undefined, 312 - members: nodes, 313 - name: transformServiceName({ 397 + }), 398 + name: serviceFunctionIdentifier({ 314 399 config: context.config, 315 - name: serviceName, 400 + handleIllegal: true, 401 + id: operation.id, 402 + operation, 316 403 }), 317 404 }); 318 405 file.add(node); 319 - } 320 - }; 321 - 322 - const generateFlatServices = ({ context }: { context: IRContext }) => { 323 - const file = context.file({ id: servicesId })!; 324 - const typesModule = file.relativePathToFile({ context, id: 'types' }); 325 - 326 - for (const path in context.ir.paths) { 327 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 328 - 329 - for (const _method in pathItem) { 330 - const method = _method as keyof IRPathItemObject; 331 - const operation = pathItem[method]!; 332 - 333 - const identifierData = context.file({ id: 'types' })!.identifier({ 334 - $ref: operationIrRef({ id: operation.id, type: 'data' }), 335 - namespace: 'type', 336 - }); 337 - if (identifierData.name) { 338 - file.import({ 339 - asType: true, 340 - module: typesModule, 341 - name: identifierData.name, 342 - }); 343 - } 344 - 345 - const identifierError = context.file({ id: 'types' })!.identifier({ 346 - $ref: operationIrRef({ id: operation.id, type: 'error' }), 347 - namespace: 'type', 348 - }); 349 - if (identifierError.name) { 350 - file.import({ 351 - asType: true, 352 - module: typesModule, 353 - name: identifierError.name, 354 - }); 355 - } 356 - 357 - const identifierResponse = context.file({ id: 'types' })!.identifier({ 358 - $ref: operationIrRef({ id: operation.id, type: 'response' }), 359 - namespace: 'type', 360 - }); 361 - if (identifierResponse.name) { 362 - file.import({ 363 - asType: true, 364 - module: typesModule, 365 - name: identifierResponse.name, 366 - }); 367 - } 368 - 369 - const node = compiler.constVariable({ 370 - comment: [ 371 - operation.deprecated && '@deprecated', 372 - operation.summary && escapeComment(operation.summary), 373 - operation.description && escapeComment(operation.description), 374 - ], 375 - exportConst: true, 376 - expression: compiler.arrowFunction({ 377 - parameters: [ 378 - { 379 - isRequired: hasOperationDataRequired(operation), 380 - name: 'options', 381 - type: operationOptionsType({ 382 - importedType: identifierData.name, 383 - throwOnError: 'ThrowOnError', 384 - }), 385 - }, 386 - ], 387 - returnType: undefined, 388 - statements: [ 389 - compiler.returnFunctionCall({ 390 - args: [ 391 - requestOptions({ 392 - context, 393 - operation, 394 - path, 395 - }), 396 - ], 397 - name: `(options?.client ?? client).${method}`, 398 - types: [ 399 - identifierResponse.name || 'unknown', 400 - identifierError.name || 'unknown', 401 - 'ThrowOnError', 402 - ], 403 - }), 404 - ], 405 - types: [ 406 - { 407 - default: false, 408 - extends: 'boolean', 409 - name: 'ThrowOnError', 410 - }, 411 - ], 412 - }), 413 - name: serviceFunctionIdentifier({ 414 - config: context.config, 415 - handleIllegal: true, 416 - id: operation.id, 417 - operation, 418 - }), 419 - }); 420 - file.add(node); 421 - } 422 - } 406 + }); 423 407 }; 424 408 425 409 export const handler: PluginHandler<Config> = ({ context, plugin }) => {
+72 -83
packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts
··· 2 2 3 3 import { compiler } from '../../../compiler'; 4 4 import type { IRContext } from '../../../ir/context'; 5 - import type { 6 - IRPathItemObject, 7 - IRPathsObject, 8 - IRSchemaObject, 9 - } from '../../../ir/ir'; 5 + import type { IRSchemaObject } from '../../../ir/ir'; 10 6 import { operationResponsesMap } from '../../../ir/operation'; 11 7 import { camelCase } from '../../../utils/camelCase'; 12 8 import { irRef } from '../../../utils/ref'; ··· 350 346 path: plugin.output, 351 347 }); 352 348 353 - for (const path in context.ir.paths) { 354 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 349 + context.subscribe('operation', ({ method, operation, path }) => { 350 + const { response } = operationResponsesMap(operation); 355 351 356 - for (const _method in pathItem) { 357 - const method = _method as keyof IRPathItemObject; 358 - const operation = pathItem[method]!; 352 + if (!response) { 353 + return; 354 + } 359 355 360 - const { response } = operationResponsesMap(operation); 361 - 362 - if (!response) { 363 - continue; 356 + if (response.items && response.items.length > 1) { 357 + if (context.config.debug) { 358 + console.warn( 359 + `❗️ Transformers warning: route ${`${method.toUpperCase()} ${path}`} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, 360 + ); 364 361 } 362 + return; 363 + } 365 364 366 - if (response.items && response.items.length > 1) { 367 - if (context.config.debug) { 368 - console.warn( 369 - `❗️ Transformers warning: route ${`${method.toUpperCase()} ${path}`} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, 370 - ); 371 - } 372 - continue; 373 - } 365 + const identifierResponse = context.file({ id: 'types' })!.identifier({ 366 + $ref: operationIrRef({ id: operation.id, type: 'response' }), 367 + namespace: 'type', 368 + }); 369 + if (!identifierResponse.name) { 370 + return; 371 + } 374 372 375 - const identifierResponse = context.file({ id: 'types' })!.identifier({ 376 - $ref: operationIrRef({ id: operation.id, type: 'response' }), 377 - namespace: 'type', 378 - }); 379 - if (!identifierResponse.name) { 380 - continue; 381 - } 373 + let identifierResponseTransformer = file.identifier({ 374 + $ref: operationTransformerIrRef({ id: operation.id, type: 'response' }), 375 + create: true, 376 + namespace: 'value', 377 + }); 378 + if (!identifierResponseTransformer.name) { 379 + return; 380 + } 382 381 383 - let identifierResponseTransformer = file.identifier({ 384 - $ref: operationTransformerIrRef({ id: operation.id, type: 'response' }), 385 - create: true, 386 - namespace: 'value', 382 + // TODO: parser - consider handling simple string response which is also a date 383 + const nodes = schemaResponseTransformerNodes({ 384 + context, 385 + schema: response, 386 + }); 387 + if (nodes.length) { 388 + file.import({ 389 + asType: true, 390 + module: file.relativePathToFile({ context, id: 'types' }), 391 + name: identifierResponse.name, 387 392 }); 388 - if (!identifierResponseTransformer.name) { 389 - continue; 390 - } 391 - 392 - // TODO: parser - consider handling simple string response which is also a date 393 - const nodes = schemaResponseTransformerNodes({ 394 - context, 395 - schema: response, 396 - }); 397 - if (nodes.length) { 398 - file.import({ 399 - asType: true, 400 - module: file.relativePathToFile({ context, id: 'types' }), 401 - name: identifierResponse.name, 402 - }); 403 - const responseTransformerNode = compiler.constVariable({ 404 - exportConst: true, 405 - expression: compiler.arrowFunction({ 406 - async: true, 407 - multiLine: true, 408 - parameters: [ 409 - { 410 - name: dataVariableName, 411 - // TODO: parser - add types, generate types without transformed dates 412 - type: compiler.keywordTypeNode({ keyword: 'any' }), 413 - }, 393 + const responseTransformerNode = compiler.constVariable({ 394 + exportConst: true, 395 + expression: compiler.arrowFunction({ 396 + async: true, 397 + multiLine: true, 398 + parameters: [ 399 + { 400 + name: dataVariableName, 401 + // TODO: parser - add types, generate types without transformed dates 402 + type: compiler.keywordTypeNode({ keyword: 'any' }), 403 + }, 404 + ], 405 + returnType: compiler.typeReferenceNode({ 406 + typeArguments: [ 407 + compiler.typeReferenceNode({ 408 + typeName: identifierResponse.name, 409 + }), 414 410 ], 415 - returnType: compiler.typeReferenceNode({ 416 - typeArguments: [ 417 - compiler.typeReferenceNode({ 418 - typeName: identifierResponse.name, 419 - }), 420 - ], 421 - typeName: 'Promise', 422 - }), 423 - statements: ensureStatements(nodes), 411 + typeName: 'Promise', 424 412 }), 425 - name: identifierResponseTransformer.name, 426 - }); 427 - file.add(responseTransformerNode); 428 - } else { 429 - // the created schema response transformer was empty, do not generate 430 - // it and prevent any future attempts 431 - identifierResponseTransformer = file.blockIdentifier({ 432 - $ref: operationTransformerIrRef({ 433 - id: operation.id, 434 - type: 'response', 435 - }), 436 - namespace: 'value', 437 - }); 438 - } 413 + statements: ensureStatements(nodes), 414 + }), 415 + name: identifierResponseTransformer.name, 416 + }); 417 + file.add(responseTransformerNode); 418 + } else { 419 + // the created schema response transformer was empty, do not generate 420 + // it and prevent any future attempts 421 + identifierResponseTransformer = file.blockIdentifier({ 422 + $ref: operationTransformerIrRef({ 423 + id: operation.id, 424 + type: 'response', 425 + }), 426 + namespace: 'value', 427 + }); 439 428 } 440 - } 429 + }); 441 430 };
+20 -58
packages/openapi-ts/src/plugins/@hey-api/types/plugin.ts
··· 6 6 import type { 7 7 IROperationObject, 8 8 IRParameterObject, 9 - IRPathItemObject, 10 - IRPathsObject, 11 9 IRSchemaObject, 12 10 } from '../../../ir/ir'; 13 11 import { operationResponsesMap } from '../../../ir/operation'; ··· 952 950 path: plugin.output, 953 951 }); 954 952 955 - if (context.ir.components) { 956 - for (const name in context.ir.components.schemas) { 957 - const schema = context.ir.components.schemas[name]; 958 - const $ref = `#/components/schemas/${name}`; 959 - 960 - try { 961 - schemaToType({ 962 - $ref, 963 - context, 964 - schema, 965 - }); 966 - } catch (error) { 967 - console.error( 968 - `🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(schema, null, 2)}`, 969 - ); 970 - throw error; 971 - } 972 - } 973 - 974 - for (const name in context.ir.components.parameters) { 975 - const parameter = context.ir.components.parameters[name]; 976 - const $ref = `#/components/parameters/${name}`; 977 - 978 - try { 979 - schemaToType({ 980 - $ref, 981 - context, 982 - schema: parameter.schema, 983 - }); 984 - } catch (error) { 985 - console.error( 986 - `🔥 Failed to process schema ${name}\n$ref: ${$ref}\nschema: ${JSON.stringify(parameter.schema, null, 2)}`, 987 - ); 988 - throw error; 989 - } 990 - } 991 - } 992 - 993 - for (const path in context.ir.paths) { 994 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 953 + context.subscribe('schema', ({ $ref, schema }) => { 954 + schemaToType({ 955 + $ref, 956 + context, 957 + schema, 958 + }); 959 + }); 995 960 996 - for (const _method in pathItem) { 997 - const method = _method as keyof IRPathItemObject; 998 - const operation = pathItem[method]!; 961 + context.subscribe('parameter', ({ $ref, parameter }) => { 962 + schemaToType({ 963 + $ref, 964 + context, 965 + schema: parameter.schema, 966 + }); 967 + }); 999 968 1000 - try { 1001 - operationToType({ 1002 - context, 1003 - operation, 1004 - }); 1005 - } catch (error) { 1006 - console.error( 1007 - `🔥 Failed to process operation ${method} ${path}\nschema: ${JSON.stringify(operation, null, 2)}`, 1008 - ); 1009 - throw error; 1010 - } 1011 - } 1012 - } 969 + context.subscribe('operation', ({ operation }) => { 970 + operationToType({ 971 + context, 972 + operation, 973 + }); 974 + }); 1013 975 };
+365 -378
packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts
··· 11 11 clientOptionsTypeName, 12 12 } from '../../../generate/client'; 13 13 import type { IRContext } from '../../../ir/context'; 14 - import type { 15 - IROperationObject, 16 - IRPathItemObject, 17 - IRPathsObject, 18 - } from '../../../ir/ir'; 14 + import type { IROperationObject } from '../../../ir/ir'; 19 15 import { 20 16 hasOperationDataRequired, 21 17 operationPagination, ··· 678 674 let hasMutations = false; 679 675 let hasQueries = false; 680 676 681 - for (const path in context.ir.paths) { 682 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 677 + context.subscribe('operation', ({ method, operation }) => { 678 + const queryFn = [ 679 + context.config.plugins['@hey-api/services']?.asClass && 680 + transformServiceName({ 681 + config: context.config, 682 + name: getServiceName(operation.tags?.[0] || 'default'), 683 + }), 684 + serviceFunctionIdentifier({ 685 + config: context.config, 686 + handleIllegal: !context.config.plugins['@hey-api/services']?.asClass, 687 + id: operation.id, 688 + operation, 689 + }), 690 + ] 691 + .filter(Boolean) 692 + .join('.'); 693 + let hasUsedQueryFn = false; 694 + 695 + const isRequired = hasOperationDataRequired(operation); 696 + 697 + // queries 698 + if ( 699 + plugin.queryOptions && 700 + (['get', 'post'] as (typeof method)[]).includes(method) 701 + ) { 702 + if (!hasQueries) { 703 + hasQueries = true; 704 + 705 + if (!hasCreateQueryKeyParamsFunction) { 706 + createQueryKeyType({ file }); 707 + createQueryKeyFunction({ file }); 708 + hasCreateQueryKeyParamsFunction = true; 709 + } 710 + 711 + file.import({ 712 + module: plugin.name, 713 + name: queryOptionsFn, 714 + }); 715 + } 716 + 717 + hasUsedQueryFn = true; 683 718 684 - for (const _method in pathItem) { 685 - const method = _method as keyof IRPathItemObject; 686 - const operation = pathItem[method]!; 719 + const typeData = useTypeData({ context, operation, plugin }); 687 720 688 - const queryFn = [ 689 - context.config.plugins['@hey-api/services']?.asClass && 690 - transformServiceName({ 691 - config: context.config, 692 - name: getServiceName(operation.tags?.[0] || 'default'), 721 + const queryKeyStatement = compiler.constVariable({ 722 + exportConst: true, 723 + expression: compiler.arrowFunction({ 724 + parameters: [ 725 + { 726 + isRequired, 727 + name: 'options', 728 + type: typeData, 729 + }, 730 + ], 731 + statements: createQueryKeyLiteral({ 732 + id: operation.id, 693 733 }), 694 - serviceFunctionIdentifier({ 695 - config: context.config, 696 - handleIllegal: !context.config.plugins['@hey-api/services']?.asClass, 697 - id: operation.id, 698 - operation, 734 + }), 735 + name: queryKeyFunctionIdentifier({ context, operation }), 736 + }); 737 + file.add(queryKeyStatement); 738 + 739 + const statement = compiler.constVariable({ 740 + // TODO: describe options, same as the actual function call 741 + comment: [], 742 + exportConst: true, 743 + expression: compiler.arrowFunction({ 744 + parameters: [ 745 + { 746 + isRequired, 747 + name: 'options', 748 + type: typeData, 749 + }, 750 + ], 751 + statements: [ 752 + compiler.returnFunctionCall({ 753 + args: [ 754 + compiler.objectExpression({ 755 + obj: [ 756 + { 757 + key: 'queryFn', 758 + value: compiler.arrowFunction({ 759 + async: true, 760 + multiLine: true, 761 + parameters: [ 762 + { 763 + destructure: [ 764 + { 765 + name: 'queryKey', 766 + }, 767 + { 768 + name: 'signal', 769 + }, 770 + ], 771 + }, 772 + ], 773 + statements: [ 774 + compiler.constVariable({ 775 + destructure: true, 776 + expression: compiler.awaitExpression({ 777 + expression: compiler.callExpression({ 778 + functionName: queryFn, 779 + parameters: [ 780 + compiler.objectExpression({ 781 + multiLine: true, 782 + obj: [ 783 + { 784 + spread: 'options', 785 + }, 786 + { 787 + spread: 'queryKey[0]', 788 + }, 789 + { 790 + key: 'signal', 791 + shorthand: true, 792 + value: compiler.identifier({ 793 + text: 'signal', 794 + }), 795 + }, 796 + { 797 + key: 'throwOnError', 798 + value: true, 799 + }, 800 + ], 801 + }), 802 + ], 803 + }), 804 + }), 805 + name: 'data', 806 + }), 807 + compiler.returnVariable({ 808 + expression: 'data', 809 + }), 810 + ], 811 + }), 812 + }, 813 + { 814 + key: 'queryKey', 815 + value: compiler.callExpression({ 816 + functionName: queryKeyFunctionIdentifier({ 817 + context, 818 + operation, 819 + }), 820 + parameters: ['options'], 821 + }), 822 + }, 823 + ], 824 + }), 825 + ], 826 + name: queryOptionsFn, 827 + }), 828 + ], 699 829 }), 700 - ] 701 - .filter(Boolean) 702 - .join('.'); 703 - let hasUsedQueryFn = false; 830 + name: queryOptionsFunctionIdentifier({ context, operation }), 831 + // TODO: add type error 832 + // TODO: AxiosError<PutSubmissionMetaError> 833 + }); 834 + file.add(statement); 835 + } 704 836 705 - const isRequired = hasOperationDataRequired(operation); 837 + // infinite queries 838 + if ( 839 + plugin.infiniteQueryOptions && 840 + (['get', 'post'] as (typeof method)[]).includes(method) 841 + ) { 842 + const pagination = operationPagination({ context, operation }); 706 843 707 - // queries 708 - if ( 709 - plugin.queryOptions && 710 - (['get', 'post'] as (typeof method)[]).includes(method) 711 - ) { 712 - if (!hasQueries) { 713 - hasQueries = true; 844 + if (pagination) { 845 + if (!hasInfiniteQueries) { 846 + hasInfiniteQueries = true; 714 847 715 848 if (!hasCreateQueryKeyParamsFunction) { 716 849 createQueryKeyType({ file }); ··· 718 851 hasCreateQueryKeyParamsFunction = true; 719 852 } 720 853 854 + if (!hasCreateInfiniteParamsFunction) { 855 + createInfiniteParamsFunction({ file }); 856 + hasCreateInfiniteParamsFunction = true; 857 + } 858 + 721 859 file.import({ 722 860 module: plugin.name, 723 - name: queryOptionsFn, 861 + name: infiniteQueryOptionsFn, 862 + }); 863 + 864 + typeInfiniteData = file.import({ 865 + asType: true, 866 + module: plugin.name, 867 + name: 'InfiniteData', 724 868 }); 725 869 } 726 870 727 871 hasUsedQueryFn = true; 728 872 729 873 const typeData = useTypeData({ context, operation, plugin }); 874 + const typeError = useTypeError({ context, operation, plugin }); 875 + const typeResponse = useTypeResponse({ context, operation, plugin }); 876 + 877 + const typeQueryKey = `${queryKeyName}<${typeData}>`; 878 + const typePageObjectParam = `Pick<${typeQueryKey}[0], 'body' | 'headers' | 'path' | 'query'>`; 879 + // TODO: parser - this is a bit clunky, need to compile type to string because 880 + // `compiler.returnFunctionCall()` accepts only strings, should be cleaned up 881 + const typePageParam = `${tsNodeToString({ 882 + node: schemaToType({ 883 + context, 884 + schema: pagination.schema, 885 + }), 886 + unescape: true, 887 + })} | ${typePageObjectParam}`; 730 888 731 889 const queryKeyStatement = compiler.constVariable({ 732 890 exportConst: true, ··· 738 896 type: typeData, 739 897 }, 740 898 ], 899 + returnType: typeQueryKey, 741 900 statements: createQueryKeyLiteral({ 742 901 id: operation.id, 902 + isInfinite: true, 743 903 }), 744 904 }), 745 - name: queryKeyFunctionIdentifier({ context, operation }), 905 + name: queryKeyFunctionIdentifier({ 906 + context, 907 + isInfinite: true, 908 + operation, 909 + }), 746 910 }); 747 911 file.add(queryKeyStatement); 748 912 ··· 762 926 compiler.returnFunctionCall({ 763 927 args: [ 764 928 compiler.objectExpression({ 929 + comments: [ 930 + { 931 + jsdoc: false, 932 + lines: ['@ts-ignore'], 933 + }, 934 + ], 765 935 obj: [ 766 936 { 767 937 key: 'queryFn', ··· 772 942 { 773 943 destructure: [ 774 944 { 945 + name: 'pageParam', 946 + }, 947 + { 775 948 name: 'queryKey', 776 949 }, 777 950 { ··· 782 955 ], 783 956 statements: [ 784 957 compiler.constVariable({ 958 + comment: [ 959 + { 960 + jsdoc: false, 961 + lines: ['@ts-ignore'], 962 + }, 963 + ], 964 + expression: compiler.conditionalExpression({ 965 + condition: compiler.binaryExpression({ 966 + left: compiler.typeOfExpression({ 967 + text: 'pageParam', 968 + }), 969 + operator: '===', 970 + right: compiler.ots.string('object'), 971 + }), 972 + whenFalse: compiler.objectExpression({ 973 + multiLine: true, 974 + obj: [ 975 + { 976 + key: pagination.in, 977 + value: compiler.objectExpression({ 978 + multiLine: true, 979 + obj: [ 980 + { 981 + key: pagination.name, 982 + value: compiler.identifier({ 983 + text: 'pageParam', 984 + }), 985 + }, 986 + ], 987 + }), 988 + }, 989 + ], 990 + }), 991 + whenTrue: compiler.identifier({ 992 + text: 'pageParam', 993 + }), 994 + }), 995 + name: 'page', 996 + typeName: typePageObjectParam, 997 + }), 998 + compiler.constVariable({ 999 + expression: compiler.callExpression({ 1000 + functionName: 'createInfiniteParams', 1001 + parameters: ['queryKey', 'page'], 1002 + }), 1003 + name: 'params', 1004 + }), 1005 + compiler.constVariable({ 785 1006 destructure: true, 786 1007 expression: compiler.awaitExpression({ 787 1008 expression: compiler.callExpression({ ··· 794 1015 spread: 'options', 795 1016 }, 796 1017 { 797 - spread: 'queryKey[0]', 1018 + spread: 'params', 798 1019 }, 799 1020 { 800 1021 key: 'signal', ··· 825 1046 value: compiler.callExpression({ 826 1047 functionName: queryKeyFunctionIdentifier({ 827 1048 context, 1049 + isInfinite: true, 828 1050 operation, 829 1051 }), 830 1052 parameters: ['options'], ··· 833 1055 ], 834 1056 }), 835 1057 ], 836 - name: queryOptionsFn, 1058 + name: infiniteQueryOptionsFn, 1059 + // TODO: better types syntax 1060 + types: [ 1061 + typeResponse, 1062 + typeError.name, 1063 + `${typeof typeInfiniteData === 'string' ? typeInfiniteData : typeInfiniteData.name}<${typeResponse}>`, 1064 + typeQueryKey, 1065 + typePageParam, 1066 + ], 837 1067 }), 838 1068 ], 839 1069 }), 840 - name: queryOptionsFunctionIdentifier({ context, operation }), 841 - // TODO: add type error 842 - // TODO: AxiosError<PutSubmissionMetaError> 1070 + name: infiniteQueryOptionsFunctionIdentifier({ 1071 + context, 1072 + operation, 1073 + }), 843 1074 }); 844 1075 file.add(statement); 845 1076 } 846 - 847 - // infinite queries 848 - if ( 849 - plugin.infiniteQueryOptions && 850 - (['get', 'post'] as (typeof method)[]).includes(method) 851 - ) { 852 - const pagination = operationPagination({ context, operation }); 853 - 854 - if (pagination) { 855 - if (!hasInfiniteQueries) { 856 - hasInfiniteQueries = true; 857 - 858 - if (!hasCreateQueryKeyParamsFunction) { 859 - createQueryKeyType({ file }); 860 - createQueryKeyFunction({ file }); 861 - hasCreateQueryKeyParamsFunction = true; 862 - } 863 - 864 - if (!hasCreateInfiniteParamsFunction) { 865 - createInfiniteParamsFunction({ file }); 866 - hasCreateInfiniteParamsFunction = true; 867 - } 1077 + } 868 1078 869 - file.import({ 870 - module: plugin.name, 871 - name: infiniteQueryOptionsFn, 872 - }); 1079 + // mutations 1080 + if ( 1081 + plugin.mutationOptions && 1082 + (['delete', 'patch', 'post', 'put'] as (typeof method)[]).includes(method) 1083 + ) { 1084 + if (!hasMutations) { 1085 + hasMutations = true; 873 1086 874 - typeInfiniteData = file.import({ 875 - asType: true, 876 - module: plugin.name, 877 - name: 'InfiniteData', 878 - }); 879 - } 1087 + file.import({ 1088 + asType: true, 1089 + module: plugin.name, 1090 + name: mutationsType, 1091 + }); 1092 + } 880 1093 881 - hasUsedQueryFn = true; 1094 + hasUsedQueryFn = true; 882 1095 883 - const typeData = useTypeData({ context, operation, plugin }); 884 - const typeError = useTypeError({ context, operation, plugin }); 885 - const typeResponse = useTypeResponse({ context, operation, plugin }); 1096 + const typeData = useTypeData({ context, operation, plugin }); 1097 + const typeError = useTypeError({ context, operation, plugin }); 1098 + const typeResponse = useTypeResponse({ context, operation, plugin }); 886 1099 887 - const typeQueryKey = `${queryKeyName}<${typeData}>`; 888 - const typePageObjectParam = `Pick<${typeQueryKey}[0], 'body' | 'headers' | 'path' | 'query'>`; 889 - // TODO: parser - this is a bit clunky, need to compile type to string because 890 - // `compiler.returnFunctionCall()` accepts only strings, should be cleaned up 891 - const typePageParam = `${tsNodeToString({ 892 - node: schemaToType({ 893 - context, 894 - schema: pagination.schema, 895 - }), 896 - unescape: true, 897 - })} | ${typePageObjectParam}`; 898 - 899 - const queryKeyStatement = compiler.constVariable({ 900 - exportConst: true, 901 - expression: compiler.arrowFunction({ 902 - parameters: [ 1100 + const expression = compiler.arrowFunction({ 1101 + parameters: [ 1102 + { 1103 + isRequired: false, 1104 + name: 'options', 1105 + type: `Partial<${typeData}>`, 1106 + }, 1107 + ], 1108 + statements: [ 1109 + compiler.constVariable({ 1110 + expression: compiler.objectExpression({ 1111 + obj: [ 903 1112 { 904 - isRequired, 905 - name: 'options', 906 - type: typeData, 907 - }, 908 - ], 909 - returnType: typeQueryKey, 910 - statements: createQueryKeyLiteral({ 911 - id: operation.id, 912 - isInfinite: true, 913 - }), 914 - }), 915 - name: queryKeyFunctionIdentifier({ 916 - context, 917 - isInfinite: true, 918 - operation, 919 - }), 920 - }); 921 - file.add(queryKeyStatement); 922 - 923 - const statement = compiler.constVariable({ 924 - // TODO: describe options, same as the actual function call 925 - comment: [], 926 - exportConst: true, 927 - expression: compiler.arrowFunction({ 928 - parameters: [ 929 - { 930 - isRequired, 931 - name: 'options', 932 - type: typeData, 933 - }, 934 - ], 935 - statements: [ 936 - compiler.returnFunctionCall({ 937 - args: [ 938 - compiler.objectExpression({ 939 - comments: [ 940 - { 941 - jsdoc: false, 942 - lines: ['@ts-ignore'], 943 - }, 944 - ], 945 - obj: [ 946 - { 947 - key: 'queryFn', 948 - value: compiler.arrowFunction({ 949 - async: true, 950 - multiLine: true, 1113 + key: 'mutationFn', 1114 + value: compiler.arrowFunction({ 1115 + async: true, 1116 + multiLine: true, 1117 + parameters: [ 1118 + { 1119 + name: 'localOptions', 1120 + }, 1121 + ], 1122 + statements: [ 1123 + compiler.constVariable({ 1124 + destructure: true, 1125 + expression: compiler.awaitExpression({ 1126 + expression: compiler.callExpression({ 1127 + functionName: queryFn, 951 1128 parameters: [ 952 - { 953 - destructure: [ 1129 + compiler.objectExpression({ 1130 + multiLine: true, 1131 + obj: [ 954 1132 { 955 - name: 'pageParam', 1133 + spread: 'options', 956 1134 }, 957 1135 { 958 - name: 'queryKey', 1136 + spread: 'localOptions', 959 1137 }, 960 1138 { 961 - name: 'signal', 962 - }, 963 - ], 964 - }, 965 - ], 966 - statements: [ 967 - compiler.constVariable({ 968 - comment: [ 969 - { 970 - jsdoc: false, 971 - lines: ['@ts-ignore'], 1139 + key: 'throwOnError', 1140 + value: true, 972 1141 }, 973 1142 ], 974 - expression: compiler.conditionalExpression({ 975 - condition: compiler.binaryExpression({ 976 - left: compiler.typeOfExpression({ 977 - text: 'pageParam', 978 - }), 979 - operator: '===', 980 - right: compiler.ots.string('object'), 981 - }), 982 - whenFalse: compiler.objectExpression({ 983 - multiLine: true, 984 - obj: [ 985 - { 986 - key: pagination.in, 987 - value: compiler.objectExpression({ 988 - multiLine: true, 989 - obj: [ 990 - { 991 - key: pagination.name, 992 - value: compiler.identifier({ 993 - text: 'pageParam', 994 - }), 995 - }, 996 - ], 997 - }), 998 - }, 999 - ], 1000 - }), 1001 - whenTrue: compiler.identifier({ 1002 - text: 'pageParam', 1003 - }), 1004 - }), 1005 - name: 'page', 1006 - typeName: typePageObjectParam, 1007 - }), 1008 - compiler.constVariable({ 1009 - expression: compiler.callExpression({ 1010 - functionName: 'createInfiniteParams', 1011 - parameters: ['queryKey', 'page'], 1012 - }), 1013 - name: 'params', 1014 - }), 1015 - compiler.constVariable({ 1016 - destructure: true, 1017 - expression: compiler.awaitExpression({ 1018 - expression: compiler.callExpression({ 1019 - functionName: queryFn, 1020 - parameters: [ 1021 - compiler.objectExpression({ 1022 - multiLine: true, 1023 - obj: [ 1024 - { 1025 - spread: 'options', 1026 - }, 1027 - { 1028 - spread: 'params', 1029 - }, 1030 - { 1031 - key: 'signal', 1032 - shorthand: true, 1033 - value: compiler.identifier({ 1034 - text: 'signal', 1035 - }), 1036 - }, 1037 - { 1038 - key: 'throwOnError', 1039 - value: true, 1040 - }, 1041 - ], 1042 - }), 1043 - ], 1044 - }), 1045 - }), 1046 - name: 'data', 1047 - }), 1048 - compiler.returnVariable({ 1049 - expression: 'data', 1050 1143 }), 1051 1144 ], 1052 1145 }), 1053 - }, 1054 - { 1055 - key: 'queryKey', 1056 - value: compiler.callExpression({ 1057 - functionName: queryKeyFunctionIdentifier({ 1058 - context, 1059 - isInfinite: true, 1060 - operation, 1061 - }), 1062 - parameters: ['options'], 1063 - }), 1064 - }, 1065 - ], 1066 - }), 1067 - ], 1068 - name: infiniteQueryOptionsFn, 1069 - // TODO: better types syntax 1070 - types: [ 1071 - typeResponse, 1072 - typeError.name, 1073 - `${typeof typeInfiniteData === 'string' ? typeInfiniteData : typeInfiniteData.name}<${typeResponse}>`, 1074 - typeQueryKey, 1075 - typePageParam, 1076 - ], 1077 - }), 1078 - ], 1079 - }), 1080 - name: infiniteQueryOptionsFunctionIdentifier({ 1081 - context, 1082 - operation, 1083 - }), 1084 - }); 1085 - file.add(statement); 1086 - } 1087 - } 1088 - 1089 - // mutations 1090 - if ( 1091 - plugin.mutationOptions && 1092 - (['delete', 'patch', 'post', 'put'] as (typeof method)[]).includes( 1093 - method, 1094 - ) 1095 - ) { 1096 - if (!hasMutations) { 1097 - hasMutations = true; 1098 - 1099 - file.import({ 1100 - asType: true, 1101 - module: plugin.name, 1102 - name: mutationsType, 1103 - }); 1104 - } 1105 - 1106 - hasUsedQueryFn = true; 1107 - 1108 - const typeData = useTypeData({ context, operation, plugin }); 1109 - const typeError = useTypeError({ context, operation, plugin }); 1110 - const typeResponse = useTypeResponse({ context, operation, plugin }); 1111 - 1112 - const expression = compiler.arrowFunction({ 1113 - parameters: [ 1114 - { 1115 - isRequired: false, 1116 - name: 'options', 1117 - type: `Partial<${typeData}>`, 1118 - }, 1119 - ], 1120 - statements: [ 1121 - compiler.constVariable({ 1122 - expression: compiler.objectExpression({ 1123 - obj: [ 1124 - { 1125 - key: 'mutationFn', 1126 - value: compiler.arrowFunction({ 1127 - async: true, 1128 - multiLine: true, 1129 - parameters: [ 1130 - { 1131 - name: 'localOptions', 1132 - }, 1133 - ], 1134 - statements: [ 1135 - compiler.constVariable({ 1136 - destructure: true, 1137 - expression: compiler.awaitExpression({ 1138 - expression: compiler.callExpression({ 1139 - functionName: queryFn, 1140 - parameters: [ 1141 - compiler.objectExpression({ 1142 - multiLine: true, 1143 - obj: [ 1144 - { 1145 - spread: 'options', 1146 - }, 1147 - { 1148 - spread: 'localOptions', 1149 - }, 1150 - { 1151 - key: 'throwOnError', 1152 - value: true, 1153 - }, 1154 - ], 1155 - }), 1156 - ], 1157 - }), 1158 - }), 1159 - name: 'data', 1160 1146 }), 1161 - compiler.returnVariable({ 1162 - expression: 'data', 1163 - }), 1164 - ], 1165 - }), 1166 - }, 1167 - ], 1168 - }), 1169 - name: mutationOptionsFn, 1170 - // TODO: better types syntax 1171 - typeName: `${mutationsType}<${typeResponse}, ${typeError.name}, ${typeData}>`, 1172 - }), 1173 - compiler.returnVariable({ 1174 - expression: mutationOptionsFn, 1147 + name: 'data', 1148 + }), 1149 + compiler.returnVariable({ 1150 + expression: 'data', 1151 + }), 1152 + ], 1153 + }), 1154 + }, 1155 + ], 1175 1156 }), 1176 - ], 1177 - }); 1178 - const statement = compiler.constVariable({ 1179 - // TODO: describe options, same as the actual function call 1180 - comment: [], 1181 - exportConst: true, 1182 - expression, 1183 - name: mutationOptionsFunctionIdentifier({ context, operation }), 1184 - }); 1185 - file.add(statement); 1186 - } 1157 + name: mutationOptionsFn, 1158 + // TODO: better types syntax 1159 + typeName: `${mutationsType}<${typeResponse}, ${typeError.name}, ${typeData}>`, 1160 + }), 1161 + compiler.returnVariable({ 1162 + expression: mutationOptionsFn, 1163 + }), 1164 + ], 1165 + }); 1166 + const statement = compiler.constVariable({ 1167 + // TODO: describe options, same as the actual function call 1168 + comment: [], 1169 + exportConst: true, 1170 + expression, 1171 + name: mutationOptionsFunctionIdentifier({ context, operation }), 1172 + }); 1173 + file.add(statement); 1174 + } 1187 1175 1188 - if (hasQueries || hasInfiniteQueries) { 1189 - file.import({ 1190 - module: context 1191 - .file({ id: plugin.name })! 1192 - .relativePathToFile({ context, id: 'services' }), 1193 - name: 'client', 1194 - }); 1195 - } 1176 + if (hasQueries || hasInfiniteQueries) { 1177 + file.import({ 1178 + module: context 1179 + .file({ id: plugin.name })! 1180 + .relativePathToFile({ context, id: 'services' }), 1181 + name: 'client', 1182 + }); 1183 + } 1196 1184 1197 - if (hasUsedQueryFn) { 1198 - file.import({ 1199 - module: context 1200 - .file({ id: plugin.name })! 1201 - .relativePathToFile({ context, id: 'services' }), 1202 - name: queryFn.split('.')[0], 1203 - }); 1204 - } 1185 + if (hasUsedQueryFn) { 1186 + file.import({ 1187 + module: context 1188 + .file({ id: plugin.name })! 1189 + .relativePathToFile({ context, id: 'services' }), 1190 + name: queryFn.split('.')[0], 1191 + }); 1205 1192 } 1206 - } 1193 + }); 1207 1194 };
+16 -22
packages/openapi-ts/src/plugins/fastify/plugin.ts
··· 2 2 3 3 import { compiler, type Property } from '../../compiler'; 4 4 import type { IRContext } from '../../ir/context'; 5 - import type { 6 - IROperationObject, 7 - IRPathItemObject, 8 - IRPathsObject, 9 - } from '../../ir/ir'; 5 + import type { IROperationObject } from '../../ir/ir'; 10 6 import { operationResponsesMap } from '../../ir/operation'; 11 7 import { hasParameterGroupObjectRequired } from '../../ir/parameter'; 12 8 import { operationIrRef } from '../@hey-api/services/plugin'; ··· 207 203 208 204 const routeHandlers: Array<Property> = []; 209 205 210 - for (const path in context.ir.paths) { 211 - const pathItem = context.ir.paths[path as keyof IRPathsObject]; 206 + context.subscribe('operation', ({ operation }) => { 207 + const routeHandler = operationToRouteHandler({ context, operation }); 208 + if (routeHandler) { 209 + routeHandlers.push(routeHandler); 210 + } 211 + }); 212 212 213 - for (const _method in pathItem) { 214 - const method = _method as keyof IRPathItemObject; 215 - const operation = pathItem[method]!; 213 + context.subscribe('after', () => { 214 + const identifier = file.identifier({ 215 + $ref: 'RouteHandlers', 216 + create: true, 217 + namespace: 'type', 218 + }); 216 219 217 - const routeHandler = operationToRouteHandler({ context, operation }); 218 - if (routeHandler) { 219 - routeHandlers.push(routeHandler); 220 - } 220 + if (!identifier.name) { 221 + return; 221 222 } 222 - } 223 223 224 - const identifier = file.identifier({ 225 - $ref: 'RouteHandlers', 226 - create: true, 227 - namespace: 'type', 228 - }); 229 - if (identifier.name) { 230 224 if (routeHandlers.length) { 231 225 file.import({ 232 226 asType: true, ··· 245 239 }), 246 240 }), 247 241 ); 248 - } 242 + }); 249 243 };
+2 -1
packages/openapi-ts/src/plugins/zod/config.ts
··· 1 1 import type { DefineConfig, PluginConfig } from '../types'; 2 + import { handler } from './plugin'; 2 3 import { handlerLegacy } from './plugin-legacy'; 3 4 import type { Config } from './types'; 4 5 5 6 export const defaultConfig: PluginConfig<Config> = { 6 - _handler: () => {}, 7 + _handler: handler, 7 8 _handlerLegacy: handlerLegacy, 8 9 name: 'zod', 9 10 output: 'zod',
+13 -102
packages/openapi-ts/src/plugins/zod/plugin-legacy.ts
··· 1 1 import { compiler } from '../../compiler'; 2 2 import type { TypeScriptFile } from '../../generate/files'; 3 - import type { Client, Model } from '../../types/client'; 3 + import type { Model } from '../../types/client'; 4 4 import type { PluginLegacyHandler } from '../types'; 5 5 import type { Config } from './types'; 6 6 7 - interface TypesProps { 8 - client: Client; 7 + const processArray = ({ 8 + file, 9 + model, 10 + }: { 9 11 file: TypeScriptFile; 10 12 model: Model; 11 - onRemoveNode?: VoidFunction; 12 - } 13 - 14 - const processArray = ({ file, model }: TypesProps) => { 13 + }) => { 15 14 const identifier = file.identifier({ 16 15 $ref: model.meta?.$ref || '', 17 16 create: true, ··· 36 35 }), 37 36 ], 38 37 }); 39 - 40 - if (model.base === 'string' || model.base === 'boolean') { 41 - const statement = compiler.constVariable({ 42 - exportConst: true, 43 - expression: zArrayExpression, 44 - name: identifier.name || '', 45 - }); 46 - file.add(statement); 47 - return; 48 - } 49 38 50 39 if (model.base === 'number') { 51 40 let expression = zArrayExpression; ··· 109 98 file.add(statement); 110 99 }; 111 100 112 - const processGeneric = ({ file, model }: TypesProps) => { 113 - const identifier = file.identifier({ 114 - $ref: model.meta?.$ref || '', 115 - create: true, 116 - namespace: 'value', 117 - }); 118 - 119 - if (!identifier.created) { 120 - return; 121 - } 122 - 123 - if (model.base === 'string') { 124 - const statement = compiler.constVariable({ 125 - exportConst: true, 126 - expression: compiler.callExpression({ 127 - functionName: compiler.propertyAccessExpression({ 128 - expression: 'z', 129 - name: 'string', 130 - }), 131 - }), 132 - name: identifier.name || '', 133 - }); 134 - file.add(statement); 135 - return; 136 - } 137 - 138 - if (model.base === 'boolean') { 139 - // console.warn(model) 140 - const statement = compiler.constVariable({ 141 - exportConst: true, 142 - expression: compiler.callExpression({ 143 - functionName: compiler.propertyAccessExpression({ 144 - expression: 'z', 145 - name: 'boolean', 146 - }), 147 - }), 148 - name: identifier.name || '', 149 - }); 150 - file.add(statement); 151 - return; 152 - } 153 - 154 - // console.warn(model.base) 155 - const statement = compiler.constVariable({ 156 - exportConst: true, 157 - expression: compiler.callExpression({ 158 - functionName: compiler.propertyAccessExpression({ 159 - expression: 'z', 160 - name: 'object', 161 - }), 162 - parameters: [ 163 - compiler.objectExpression({ 164 - multiLine: true, 165 - obj: [], 166 - }), 167 - ], 168 - }), 169 - name: identifier.name || '', 170 - }); 171 - file.add(statement); 172 - }; 173 - 174 - const processModel = (props: TypesProps) => { 175 - switch (props.model.export) { 176 - case 'all-of': 177 - case 'any-of': 178 - case 'one-of': 179 - case 'interface': 180 - // return processComposition(props); 181 - return; 182 - case 'array': 183 - return processArray(props); 184 - case 'enum': 185 - // return processEnum(props); 186 - return; 187 - default: 188 - return processGeneric(props); 189 - } 190 - }; 191 - 192 101 export const handlerLegacy: PluginLegacyHandler<Config> = ({ 193 102 client, 194 103 files, ··· 202 111 }); 203 112 204 113 for (const model of client.models) { 205 - processModel({ 206 - client, 207 - file, 208 - model, 209 - }); 114 + switch (model.export) { 115 + case 'array': 116 + return processArray({ 117 + file, 118 + model, 119 + }); 120 + } 210 121 } 211 122 };
+569
packages/openapi-ts/src/plugins/zod/plugin.ts
··· 1 + import type ts from 'typescript'; 2 + 3 + import { compiler } from '../../compiler'; 4 + import type { IRContext } from '../../ir/context'; 5 + import type { IRSchemaObject } from '../../ir/ir'; 6 + import { deduplicateSchema } from '../../ir/schema'; 7 + import { isRefOpenApiComponent } from '../../utils/ref'; 8 + import type { PluginHandler } from '../types'; 9 + import type { Config } from './types'; 10 + 11 + interface SchemaWithType<T extends Required<IRSchemaObject>['type']> 12 + extends Omit<IRSchemaObject, 'type'> { 13 + type: Extract<Required<IRSchemaObject>['type'], T>; 14 + } 15 + 16 + const zodId = 'zod'; 17 + 18 + const arrayTypeToZodSchema = ({ 19 + context, 20 + namespace, 21 + schema, 22 + }: { 23 + context: IRContext; 24 + namespace: Array<ts.Statement>; 25 + schema: SchemaWithType<'array'>; 26 + }) => { 27 + if (!schema.items) { 28 + const expression = compiler.callExpression({ 29 + functionName: compiler.propertyAccessExpression({ 30 + expression: 'z', 31 + name: 'array', 32 + }), 33 + parameters: [ 34 + unknownTypeToZodSchema({ 35 + context, 36 + namespace, 37 + schema: { 38 + type: 'unknown', 39 + }, 40 + }), 41 + ], 42 + }); 43 + return expression; 44 + } 45 + 46 + schema = deduplicateSchema({ schema }); 47 + 48 + // at least one item is guaranteed 49 + const itemExpressions = schema.items!.map((item) => 50 + schemaToZodSchema({ 51 + context, 52 + namespace, 53 + schema: item, 54 + }), 55 + ); 56 + 57 + if (itemExpressions.length === 1) { 58 + const expression = compiler.callExpression({ 59 + functionName: compiler.propertyAccessExpression({ 60 + expression: 'z', 61 + name: 'array', 62 + }), 63 + parameters: itemExpressions, 64 + }); 65 + return expression; 66 + } 67 + 68 + if (schema.logicalOperator === 'and') { 69 + // TODO: parser - handle intersection 70 + // return compiler.typeArrayNode( 71 + // compiler.typeIntersectionNode({ types: itemExpressions }), 72 + // ); 73 + } 74 + 75 + // TODO: parser - handle union 76 + // return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemExpressions })); 77 + 78 + const expression = compiler.callExpression({ 79 + functionName: compiler.propertyAccessExpression({ 80 + expression: 'z', 81 + name: 'array', 82 + }), 83 + parameters: [ 84 + unknownTypeToZodSchema({ 85 + context, 86 + namespace, 87 + schema: { 88 + type: 'unknown', 89 + }, 90 + }), 91 + ], 92 + }); 93 + return expression; 94 + }; 95 + 96 + const booleanTypeToZodSchema = ({ 97 + schema, 98 + }: { 99 + context: IRContext; 100 + namespace: Array<ts.Statement>; 101 + schema: SchemaWithType<'boolean'>; 102 + }) => { 103 + if (schema.const !== undefined) { 104 + // TODO: parser - add constant 105 + // return compiler.literalTypeNode({ 106 + // literal: compiler.ots.boolean(schema.const as boolean), 107 + // }); 108 + } 109 + 110 + const expression = compiler.callExpression({ 111 + functionName: compiler.propertyAccessExpression({ 112 + expression: 'z', 113 + name: 'boolean', 114 + }), 115 + }); 116 + return expression; 117 + }; 118 + 119 + const neverTypeToZodSchema = ({ 120 + schema, 121 + }: { 122 + context: IRContext; 123 + namespace: Array<ts.Statement>; 124 + schema: SchemaWithType<'never'>; 125 + }) => { 126 + const expression = compiler.callExpression({ 127 + functionName: compiler.propertyAccessExpression({ 128 + expression: 'z', 129 + name: schema.type, 130 + }), 131 + }); 132 + return expression; 133 + }; 134 + 135 + const nullTypeToZodSchema = ({ 136 + schema, 137 + }: { 138 + context: IRContext; 139 + namespace: Array<ts.Statement>; 140 + schema: SchemaWithType<'null'>; 141 + }) => { 142 + const expression = compiler.callExpression({ 143 + functionName: compiler.propertyAccessExpression({ 144 + expression: 'z', 145 + name: schema.type, 146 + }), 147 + }); 148 + return expression; 149 + }; 150 + 151 + const numberTypeToZodSchema = ({ 152 + schema, 153 + }: { 154 + context: IRContext; 155 + namespace: Array<ts.Statement>; 156 + schema: SchemaWithType<'number'>; 157 + }) => { 158 + // TODO: parser - handle min/max length 159 + 160 + if (schema.const !== undefined) { 161 + // TODO: parser - add constant 162 + // return compiler.literalTypeNode({ 163 + // literal: compiler.ots.number(schema.const as number), 164 + // }); 165 + } 166 + 167 + const expression = compiler.callExpression({ 168 + functionName: compiler.propertyAccessExpression({ 169 + expression: 'z', 170 + name: 'number', 171 + }), 172 + }); 173 + return expression; 174 + }; 175 + 176 + const objectTypeToZodSchema = ({ 177 + // context, 178 + // namespace, 179 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 180 + schema, 181 + }: { 182 + context: IRContext; 183 + namespace: Array<ts.Statement>; 184 + schema: SchemaWithType<'object'>; 185 + }) => { 186 + // let indexProperty: Property | undefined; 187 + // const schemaProperties: Array<Property> = []; 188 + // let indexPropertyItems: Array<IRSchemaObject> = []; 189 + // const required = schema.required ?? []; 190 + // let hasOptionalProperties = false; 191 + 192 + // for (const name in schema.properties) { 193 + // const property = schema.properties[name]; 194 + // const isRequired = required.includes(name); 195 + // digitsRegExp.lastIndex = 0; 196 + // schemaProperties.push({ 197 + // comment: parseSchemaJsDoc({ schema: property }), 198 + // isReadOnly: property.accessScope === 'read', 199 + // isRequired, 200 + // name: digitsRegExp.test(name) 201 + // ? ts.factory.createNumericLiteral(name) 202 + // : name, 203 + // type: schemaToZodSchema({ 204 + // $ref: `${irRef}${name}`, 205 + // context, 206 + // namespace, 207 + // schema: property, 208 + // }), 209 + // }); 210 + // // indexPropertyItems.push(property); 211 + // if (!isRequired) { 212 + // hasOptionalProperties = true; 213 + // } 214 + // } 215 + 216 + // if ( 217 + // schema.additionalProperties && 218 + // (schema.additionalProperties.type !== 'never' || !indexPropertyItems.length) 219 + // ) { 220 + // if (schema.additionalProperties.type === 'never') { 221 + // indexPropertyItems = [schema.additionalProperties]; 222 + // } else { 223 + // indexPropertyItems.unshift(schema.additionalProperties); 224 + // } 225 + 226 + // if (hasOptionalProperties) { 227 + // indexPropertyItems.push({ 228 + // type: 'undefined', 229 + // }); 230 + // } 231 + 232 + // indexProperty = { 233 + // isRequired: true, 234 + // name: 'key', 235 + // type: schemaToZodSchema({ 236 + // context, 237 + // namespace, 238 + // schema: 239 + // indexPropertyItems.length === 1 240 + // ? indexPropertyItems[0] 241 + // : { 242 + // items: indexPropertyItems, 243 + // logicalOperator: 'or', 244 + // }, 245 + // }), 246 + // }; 247 + // } 248 + 249 + // return compiler.typeInterfaceNode({ 250 + // indexProperty, 251 + // properties: schemaProperties, 252 + // useLegacyResolution: false, 253 + // }); 254 + const expression = compiler.callExpression({ 255 + functionName: compiler.propertyAccessExpression({ 256 + expression: 'z', 257 + name: 'object', 258 + }), 259 + parameters: [ 260 + // TODO: parser - handle parameters 261 + compiler.objectExpression({ 262 + multiLine: true, 263 + obj: [], 264 + }), 265 + ], 266 + }); 267 + return expression; 268 + }; 269 + 270 + const stringTypeToZodSchema = ({ 271 + schema, 272 + }: { 273 + context: IRContext; 274 + namespace: Array<ts.Statement>; 275 + schema: SchemaWithType<'string'>; 276 + }) => { 277 + if (schema.const !== undefined) { 278 + // TODO: parser - add constant 279 + // return compiler.literalTypeNode({ 280 + // literal: compiler.stringLiteral({ text: schema.const as string }), 281 + // }); 282 + } 283 + 284 + if (schema.format) { 285 + // TODO: parser - add format 286 + // if (schema.format === 'binary') { 287 + // return compiler.typeUnionNode({ 288 + // types: [ 289 + // compiler.typeReferenceNode({ 290 + // typeName: 'Blob', 291 + // }), 292 + // compiler.typeReferenceNode({ 293 + // typeName: 'File', 294 + // }), 295 + // ], 296 + // }); 297 + // } 298 + // if (schema.format === 'date-time' || schema.format === 'date') { 299 + // // TODO: parser - add ability to skip type transformers 300 + // if (context.config.plugins['@hey-api/transformers']?.dates) { 301 + // return compiler.typeReferenceNode({ typeName: 'Date' }); 302 + // } 303 + // } 304 + } 305 + 306 + const expression = compiler.callExpression({ 307 + functionName: compiler.propertyAccessExpression({ 308 + expression: 'z', 309 + name: 'string', 310 + }), 311 + }); 312 + return expression; 313 + }; 314 + 315 + const undefinedTypeToZodSchema = ({ 316 + schema, 317 + }: { 318 + context: IRContext; 319 + namespace: Array<ts.Statement>; 320 + schema: SchemaWithType<'undefined'>; 321 + }) => { 322 + const expression = compiler.callExpression({ 323 + functionName: compiler.propertyAccessExpression({ 324 + expression: 'z', 325 + name: schema.type, 326 + }), 327 + }); 328 + return expression; 329 + }; 330 + 331 + const unknownTypeToZodSchema = ({ 332 + schema, 333 + }: { 334 + context: IRContext; 335 + namespace: Array<ts.Statement>; 336 + schema: SchemaWithType<'unknown'>; 337 + }) => { 338 + const expression = compiler.callExpression({ 339 + functionName: compiler.propertyAccessExpression({ 340 + expression: 'z', 341 + name: schema.type, 342 + }), 343 + }); 344 + return expression; 345 + }; 346 + 347 + const voidTypeToZodSchema = ({ 348 + schema, 349 + }: { 350 + context: IRContext; 351 + namespace: Array<ts.Statement>; 352 + schema: SchemaWithType<'void'>; 353 + }) => { 354 + const expression = compiler.callExpression({ 355 + functionName: compiler.propertyAccessExpression({ 356 + expression: 'z', 357 + name: schema.type, 358 + }), 359 + }); 360 + return expression; 361 + }; 362 + 363 + const schemaTypeToZodSchema = ({ 364 + // $ref, 365 + context, 366 + namespace, 367 + schema, 368 + }: { 369 + $ref?: string; 370 + context: IRContext; 371 + namespace: Array<ts.Statement>; 372 + schema: IRSchemaObject; 373 + // @ts-expect-error 374 + }): ts.Expression => { 375 + switch (schema.type as Required<IRSchemaObject>['type']) { 376 + case 'array': 377 + return arrayTypeToZodSchema({ 378 + context, 379 + namespace, 380 + schema: schema as SchemaWithType<'array'>, 381 + }); 382 + case 'boolean': 383 + return booleanTypeToZodSchema({ 384 + context, 385 + namespace, 386 + schema: schema as SchemaWithType<'boolean'>, 387 + }); 388 + case 'enum': 389 + // TODO: parser - handle enum 390 + // return enumTypeToIdentifier({ 391 + // $ref, 392 + // context, 393 + // namespace, 394 + // schema: schema as SchemaWithType<'enum'>, 395 + // }); 396 + break; 397 + case 'never': 398 + return neverTypeToZodSchema({ 399 + context, 400 + namespace, 401 + schema: schema as SchemaWithType<'never'>, 402 + }); 403 + case 'null': 404 + return nullTypeToZodSchema({ 405 + context, 406 + namespace, 407 + schema: schema as SchemaWithType<'null'>, 408 + }); 409 + case 'number': 410 + return numberTypeToZodSchema({ 411 + context, 412 + namespace, 413 + schema: schema as SchemaWithType<'number'>, 414 + }); 415 + case 'object': 416 + return objectTypeToZodSchema({ 417 + context, 418 + namespace, 419 + schema: schema as SchemaWithType<'object'>, 420 + }); 421 + case 'string': 422 + return stringTypeToZodSchema({ 423 + context, 424 + namespace, 425 + schema: schema as SchemaWithType<'string'>, 426 + }); 427 + case 'tuple': 428 + // TODO: parser - handle tuple 429 + // return tupleTypeToIdentifier({ 430 + // context, 431 + // namespace, 432 + // schema: schema as SchemaWithType<'tuple'>, 433 + // }); 434 + break; 435 + case 'undefined': 436 + return undefinedTypeToZodSchema({ 437 + context, 438 + namespace, 439 + schema: schema as SchemaWithType<'undefined'>, 440 + }); 441 + case 'unknown': 442 + return unknownTypeToZodSchema({ 443 + context, 444 + namespace, 445 + schema: schema as SchemaWithType<'unknown'>, 446 + }); 447 + case 'void': 448 + return voidTypeToZodSchema({ 449 + context, 450 + namespace, 451 + schema: schema as SchemaWithType<'void'>, 452 + }); 453 + } 454 + }; 455 + 456 + const schemaToZodSchema = ({ 457 + $ref, 458 + context, 459 + // TODO: parser - remove namespace, it's a type plugin construct 460 + namespace = [], 461 + schema, 462 + }: { 463 + $ref?: string; 464 + context: IRContext; 465 + namespace?: Array<ts.Statement>; 466 + schema: IRSchemaObject; 467 + }): ts.Expression => { 468 + const file = context.file({ id: zodId })!; 469 + 470 + let expression: ts.Expression | undefined; 471 + 472 + if (schema.$ref) { 473 + // if $ref hasn't been processed yet, inline it to avoid the 474 + // "Block-scoped variable used before its declaration." error 475 + // this could be (maybe?) fixed by reshuffling the generation order 476 + const identifier = file.identifier({ 477 + $ref: schema.$ref, 478 + namespace: 'value', 479 + }); 480 + if (identifier.name) { 481 + expression = compiler.identifier({ text: identifier.name || '' }); 482 + } else { 483 + const ref = context.resolveIrRef<IRSchemaObject>(schema.$ref); 484 + expression = schemaToZodSchema({ 485 + context, 486 + schema: ref, 487 + }); 488 + } 489 + } else if (schema.type) { 490 + expression = schemaTypeToZodSchema({ 491 + $ref, 492 + context, 493 + namespace, 494 + schema, 495 + }); 496 + } else if (schema.items) { 497 + // TODO: parser - handle items 498 + // schema = deduplicateSchema({ schema }); 499 + // if (schema.items) { 500 + // const itemTypes = schema.items.map((item) => 501 + // schemaToZodSchema({ 502 + // context, 503 + // namespace, 504 + // schema: item, 505 + // }), 506 + // ); 507 + // expression = 508 + // schema.logicalOperator === 'and' 509 + // ? compiler.typeIntersectionNode({ types: itemTypes }) 510 + // : compiler.typeUnionNode({ types: itemTypes }); 511 + // } else { 512 + // expression = schemaToZodSchema({ 513 + // context, 514 + // namespace, 515 + // schema, 516 + // }); 517 + // } 518 + } else { 519 + // catch-all fallback for failed schemas 520 + expression = schemaTypeToZodSchema({ 521 + context, 522 + namespace, 523 + schema: { 524 + type: 'unknown', 525 + }, 526 + }); 527 + } 528 + 529 + // emit nodes only if $ref points to a reusable component 530 + if ($ref && isRefOpenApiComponent($ref) && expression) { 531 + // enum handler emits its own artifacts 532 + if (schema.type !== 'enum') { 533 + const identifier = file.identifier({ 534 + $ref, 535 + create: true, 536 + namespace: 'value', 537 + }); 538 + const statement = compiler.constVariable({ 539 + exportConst: true, 540 + expression, 541 + name: identifier.name || '', 542 + }); 543 + file.add(statement); 544 + } 545 + } 546 + 547 + // @ts-expect-error 548 + return expression; 549 + }; 550 + 551 + export const handler: PluginHandler<Config> = ({ context, plugin }) => { 552 + const file = context.createFile({ 553 + id: zodId, 554 + path: plugin.output, 555 + }); 556 + 557 + file.import({ 558 + module: 'zod', 559 + name: 'z', 560 + }); 561 + 562 + context.subscribe('schema', ({ $ref, schema }) => { 563 + schemaToZodSchema({ 564 + $ref, 565 + context, 566 + schema, 567 + }); 568 + }); 569 + };
+203
packages/openapi-ts/test/__snapshots__/3.0.x/plugins/zod/default/zod.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { z } from 'zod'; 4 + 5 + export const _400 = z.string(); 6 + 7 + export const camelCaseCommentWithBreaks = z.number(); 8 + 9 + export const CommentWithBreaks = z.number(); 10 + 11 + export const CommentWithBackticks = z.number(); 12 + 13 + export const CommentWithBackticksAndQuotes = z.number(); 14 + 15 + export const CommentWithSlashes = z.number(); 16 + 17 + export const CommentWithExpressionPlaceholders = z.number(); 18 + 19 + export const CommentWithQuotes = z.number(); 20 + 21 + export const CommentWithReservedCharacters = z.number(); 22 + 23 + export const SimpleInteger = z.number(); 24 + 25 + export const SimpleBoolean = z.boolean(); 26 + 27 + export const SimpleString = z.string(); 28 + 29 + export const NonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); 30 + 31 + export const SimpleFile = z.string(); 32 + 33 + export const SimpleReference = z.object({}); 34 + 35 + export const EnumFromDescription = z.number(); 36 + 37 + export const ArrayWithNumbers = z.array(z.number()); 38 + 39 + export const ArrayWithBooleans = z.array(z.boolean()); 40 + 41 + export const ArrayWithStrings = z.array(z.string()); 42 + 43 + export const ArrayWithReferences = z.array(z.object({})); 44 + 45 + export const ArrayWithArray = z.array(z.array(z.object({}))); 46 + 47 + export const ArrayWithProperties = z.array(z.object({})); 48 + 49 + export const ArrayWithAnyOfProperties = z.array(z.unknown()); 50 + 51 + export const AnyOfAnyAndNull = z.object({}); 52 + 53 + export const AnyOfArrays = z.object({}); 54 + 55 + export const DictionaryWithString = z.object({}); 56 + 57 + export const DictionaryWithPropertiesAndAdditionalProperties = z.object({}); 58 + 59 + export const DictionaryWithReference = z.object({}); 60 + 61 + export const DictionaryWithArray = z.object({}); 62 + 63 + export const DictionaryWithDictionary = z.object({}); 64 + 65 + export const DictionaryWithProperties = z.object({}); 66 + 67 + export const ModelWithInteger = z.object({}); 68 + 69 + export const ModelWithBoolean = z.object({}); 70 + 71 + export const ModelWithString = z.object({}); 72 + 73 + export const ModelWithStringError = z.object({}); 74 + 75 + export const Model_From_Zendesk = z.string(); 76 + 77 + export const ModelWithNullableString = z.object({}); 78 + 79 + export const ModelWithEnum = z.object({}); 80 + 81 + export const ModelWithEnumWithHyphen = z.object({}); 82 + 83 + export const ModelWithEnumFromDescription = z.object({}); 84 + 85 + export const ModelWithNestedEnums = z.object({}); 86 + 87 + export const ModelWithReference = z.object({}); 88 + 89 + export const ModelWithArrayReadOnlyAndWriteOnly = z.object({}); 90 + 91 + export const ModelWithArray = z.object({}); 92 + 93 + export const ModelWithDictionary = z.object({}); 94 + 95 + export const DeprecatedModel = z.object({}); 96 + 97 + export const ModelWithCircularReference = z.object({}); 98 + 99 + export const CompositionWithOneOf = z.object({}); 100 + 101 + export const CompositionWithOneOfAnonymous = z.object({}); 102 + 103 + export const ModelCircle = z.object({}); 104 + 105 + export const ModelSquare = z.object({}); 106 + 107 + export const CompositionWithAnyOf = z.object({}); 108 + 109 + export const CompositionWithAnyOfAnonymous = z.object({}); 110 + 111 + export const CompositionWithNestedAnyAndTypeNull = z.object({}); 112 + 113 + export const CompositionWithNestedAnyOfAndNull = z.object({}); 114 + 115 + export const CompositionWithOneOfAndNullable = z.object({}); 116 + 117 + export const CompositionWithOneOfAndSimpleDictionary = z.object({}); 118 + 119 + export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({}); 120 + 121 + export const CompositionWithOneOfAndComplexArrayDictionary = z.object({}); 122 + 123 + export const CompositionWithAllOfAndNullable = z.object({}); 124 + 125 + export const CompositionWithAnyOfAndNullable = z.object({}); 126 + 127 + export const CompositionBaseModel = z.object({}); 128 + 129 + export const ModelWithProperties = z.object({}); 130 + 131 + export const ModelWithNestedProperties = z.object({}); 132 + 133 + export const ModelWithDuplicateProperties = z.object({}); 134 + 135 + export const ModelWithOrderedProperties = z.object({}); 136 + 137 + export const ModelWithDuplicateImports = z.object({}); 138 + 139 + export const ModelWithPattern = z.object({}); 140 + 141 + export const File = z.object({}); 142 + 143 + export const _default = z.object({}); 144 + 145 + export const Pageable = z.object({}); 146 + 147 + export const FreeFormObjectWithoutAdditionalProperties = z.object({}); 148 + 149 + export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); 150 + 151 + export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); 152 + 153 + export const ModelWithConst = z.object({}); 154 + 155 + export const ModelWithAdditionalPropertiesEqTrue = z.object({}); 156 + 157 + export const NestedAnyOfArraysNullable = z.object({}); 158 + 159 + export const CharactersInDescription = z.string(); 160 + 161 + export const ModelWithNullableObject = z.object({}); 162 + 163 + export const ModelWithNestedArrayEnumsData = z.object({}); 164 + 165 + export const ModelWithNestedArrayEnums = z.object({}); 166 + 167 + export const ModelWithNestedCompositionEnums = z.object({}); 168 + 169 + export const ModelWithReadOnlyAndWriteOnly = z.object({}); 170 + 171 + export const ModelWithPrefixItemsConstantSizeArray = z.array(z.unknown()); 172 + 173 + export const ModelWithNumericEnumUnion = z.object({}); 174 + 175 + export const ModelWithBackticksInDescription = z.object({}); 176 + 177 + export const ParameterSimpleParameterUnused = z.string(); 178 + 179 + export const PostServiceWithEmptyTagResponse = z.string(); 180 + 181 + export const PostServiceWithEmptyTagResponse2 = z.string(); 182 + 183 + export const DeleteFooData = z.string(); 184 + 185 + export const DeleteFooData2 = z.string(); 186 + 187 + export const _import = z.string(); 188 + 189 + export const SchemaWithFormRestrictedKeys = z.object({}); 190 + 191 + export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({}); 192 + 193 + export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({}); 194 + 195 + export const AdditionalPropertiesUnknownIssue = z.object({}); 196 + 197 + export const AdditionalPropertiesUnknownIssue2 = z.object({}); 198 + 199 + export const AdditionalPropertiesIntegerIssue = z.object({}); 200 + 201 + export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({}); 202 + 203 + export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({});
+203
packages/openapi-ts/test/__snapshots__/3.1.x/plugins/zod/default/zod.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { z } from 'zod'; 4 + 5 + export const _400 = z.string(); 6 + 7 + export const camelCaseCommentWithBreaks = z.number(); 8 + 9 + export const CommentWithBreaks = z.number(); 10 + 11 + export const CommentWithBackticks = z.number(); 12 + 13 + export const CommentWithBackticksAndQuotes = z.number(); 14 + 15 + export const CommentWithSlashes = z.number(); 16 + 17 + export const CommentWithExpressionPlaceholders = z.number(); 18 + 19 + export const CommentWithQuotes = z.number(); 20 + 21 + export const CommentWithReservedCharacters = z.number(); 22 + 23 + export const SimpleInteger = z.number(); 24 + 25 + export const SimpleBoolean = z.boolean(); 26 + 27 + export const SimpleString = z.string(); 28 + 29 + export const NonAsciiStringæøåÆØÅöôêÊ字符串 = z.string(); 30 + 31 + export const SimpleFile = z.string(); 32 + 33 + export const SimpleReference = z.object({}); 34 + 35 + export const EnumFromDescription = z.number(); 36 + 37 + export const ArrayWithNumbers = z.array(z.number()); 38 + 39 + export const ArrayWithBooleans = z.array(z.boolean()); 40 + 41 + export const ArrayWithStrings = z.array(z.string()); 42 + 43 + export const ArrayWithReferences = z.array(z.object({})); 44 + 45 + export const ArrayWithArray = z.array(z.array(z.object({}))); 46 + 47 + export const ArrayWithProperties = z.array(z.object({})); 48 + 49 + export const ArrayWithAnyOfProperties = z.array(z.unknown()); 50 + 51 + export const AnyOfAnyAndNull = z.object({}); 52 + 53 + export const AnyOfArrays = z.object({}); 54 + 55 + export const DictionaryWithString = z.object({}); 56 + 57 + export const DictionaryWithPropertiesAndAdditionalProperties = z.object({}); 58 + 59 + export const DictionaryWithReference = z.object({}); 60 + 61 + export const DictionaryWithArray = z.object({}); 62 + 63 + export const DictionaryWithDictionary = z.object({}); 64 + 65 + export const DictionaryWithProperties = z.object({}); 66 + 67 + export const ModelWithInteger = z.object({}); 68 + 69 + export const ModelWithBoolean = z.object({}); 70 + 71 + export const ModelWithString = z.object({}); 72 + 73 + export const ModelWithStringError = z.object({}); 74 + 75 + export const Model_From_Zendesk = z.string(); 76 + 77 + export const ModelWithNullableString = z.object({}); 78 + 79 + export const ModelWithEnum = z.object({}); 80 + 81 + export const ModelWithEnumWithHyphen = z.object({}); 82 + 83 + export const ModelWithEnumFromDescription = z.object({}); 84 + 85 + export const ModelWithNestedEnums = z.object({}); 86 + 87 + export const ModelWithReference = z.object({}); 88 + 89 + export const ModelWithArrayReadOnlyAndWriteOnly = z.object({}); 90 + 91 + export const ModelWithArray = z.object({}); 92 + 93 + export const ModelWithDictionary = z.object({}); 94 + 95 + export const DeprecatedModel = z.object({}); 96 + 97 + export const ModelWithCircularReference = z.object({}); 98 + 99 + export const CompositionWithOneOf = z.object({}); 100 + 101 + export const CompositionWithOneOfAnonymous = z.object({}); 102 + 103 + export const ModelCircle = z.object({}); 104 + 105 + export const ModelSquare = z.object({}); 106 + 107 + export const CompositionWithAnyOf = z.object({}); 108 + 109 + export const CompositionWithAnyOfAnonymous = z.object({}); 110 + 111 + export const CompositionWithNestedAnyAndTypeNull = z.object({}); 112 + 113 + export const ConstValue = z.string(); 114 + 115 + export const CompositionWithNestedAnyOfAndNull = z.object({}); 116 + 117 + export const CompositionWithOneOfAndNullable = z.object({}); 118 + 119 + export const CompositionWithOneOfAndSimpleDictionary = z.object({}); 120 + 121 + export const CompositionWithOneOfAndSimpleArrayDictionary = z.object({}); 122 + 123 + export const CompositionWithOneOfAndComplexArrayDictionary = z.object({}); 124 + 125 + export const CompositionWithAllOfAndNullable = z.object({}); 126 + 127 + export const CompositionWithAnyOfAndNullable = z.object({}); 128 + 129 + export const CompositionBaseModel = z.object({}); 130 + 131 + export const ModelWithProperties = z.object({}); 132 + 133 + export const ModelWithNestedProperties = z.object({}); 134 + 135 + export const ModelWithDuplicateProperties = z.object({}); 136 + 137 + export const ModelWithOrderedProperties = z.object({}); 138 + 139 + export const ModelWithDuplicateImports = z.object({}); 140 + 141 + export const ModelWithPattern = z.object({}); 142 + 143 + export const File = z.object({}); 144 + 145 + export const _default = z.object({}); 146 + 147 + export const Pageable = z.object({}); 148 + 149 + export const FreeFormObjectWithoutAdditionalProperties = z.object({}); 150 + 151 + export const FreeFormObjectWithAdditionalPropertiesEqTrue = z.object({}); 152 + 153 + export const FreeFormObjectWithAdditionalPropertiesEqEmptyObject = z.object({}); 154 + 155 + export const ModelWithConst = z.object({}); 156 + 157 + export const ModelWithAdditionalPropertiesEqTrue = z.object({}); 158 + 159 + export const NestedAnyOfArraysNullable = z.object({}); 160 + 161 + export const CharactersInDescription = z.string(); 162 + 163 + export const ModelWithNullableObject = z.object({}); 164 + 165 + export const ModelWithNestedArrayEnumsData = z.object({}); 166 + 167 + export const ModelWithNestedArrayEnums = z.object({}); 168 + 169 + export const ModelWithNestedCompositionEnums = z.object({}); 170 + 171 + export const ModelWithReadOnlyAndWriteOnly = z.object({}); 172 + 173 + export const ModelWithNumericEnumUnion = z.object({}); 174 + 175 + export const ModelWithBackticksInDescription = z.object({}); 176 + 177 + export const ParameterSimpleParameterUnused = z.string(); 178 + 179 + export const PostServiceWithEmptyTagResponse = z.string(); 180 + 181 + export const PostServiceWithEmptyTagResponse2 = z.string(); 182 + 183 + export const DeleteFooData = z.string(); 184 + 185 + export const DeleteFooData2 = z.string(); 186 + 187 + export const _import = z.string(); 188 + 189 + export const SchemaWithFormRestrictedKeys = z.object({}); 190 + 191 + export const io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = z.object({}); 192 + 193 + export const io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = z.object({}); 194 + 195 + export const AdditionalPropertiesUnknownIssue = z.object({}); 196 + 197 + export const AdditionalPropertiesUnknownIssue2 = z.object({}); 198 + 199 + export const AdditionalPropertiesIntegerIssue = z.object({}); 200 + 201 + export const Generic_Schema_Duplicate_Issue_1_System_Boolean_ = z.object({}); 202 + 203 + export const Generic_Schema_Duplicate_Issue_1_System_String_ = z.object({});
+8
packages/openapi-ts/test/plugins.spec.ts
··· 209 209 }), 210 210 description: 'generate Fastify types with Fastify plugin', 211 211 }, 212 + { 213 + config: createConfig({ 214 + output: 'default', 215 + // @ts-expect-error 216 + plugins: ['zod'], 217 + }), 218 + description: 'generate Zod schemas with Zod plugin', 219 + }, 212 220 ]; 213 221 214 222 it.each(scenarios)('$description', async ({ config }) => {
+2 -4
packages/openapi-ts/test/sample.cjs
··· 13 13 input: { 14 14 // include: 15 15 // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', 16 - path: './test/spec/3.1.x/const.json', 16 + path: './test/spec/3.1.x/full.json', 17 17 // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 18 18 }, 19 19 // name: 'foo', ··· 48 48 }, 49 49 { 50 50 // name: '@tanstack/react-query', 51 - }, 52 - { 53 - // name: 'zod', 54 51 // name: 'fastify', 52 + name: 'zod', 55 53 }, 56 54 ], 57 55 // useOptions: false,
-71
packages/openapi-ts/test/spec/tanstack.json
··· 1 - { 2 - "openapi": "3.0.1", 3 - "info": { 4 - "title": "OpenAPI definition", 5 - "version": "v0" 6 - }, 7 - "paths": { 8 - "/api/search": { 9 - "post": { 10 - "operationId": "search", 11 - "requestBody": { 12 - "content": { 13 - "application/json": { 14 - "schema": { 15 - "$ref": "#/components/schemas/SearchRequest" 16 - } 17 - } 18 - } 19 - } 20 - } 21 - } 22 - }, 23 - "components": { 24 - "schemas": { 25 - "SearchParamsRequest": { 26 - "required": ["contractIds", "contractNumbers"], 27 - "type": "object", 28 - "properties": { 29 - "contractIds": { 30 - "type": "array", 31 - "items": { 32 - "type": "string", 33 - "format": "uuid" 34 - } 35 - }, 36 - "contractNumbers": { 37 - "type": "array", 38 - "items": { 39 - "type": "string" 40 - } 41 - } 42 - } 43 - }, 44 - "SearchRequest": { 45 - "required": ["searchParameters"], 46 - "type": "object", 47 - "properties": { 48 - "searchParameters": { 49 - "$ref": "#/components/schemas/SearchParamsRequest" 50 - }, 51 - "pageParameters": { 52 - "$ref": "#/components/schemas/PageParameters" 53 - } 54 - } 55 - }, 56 - "PageParameters": { 57 - "type": "object", 58 - "properties": { 59 - "offset": { 60 - "type": "integer", 61 - "format": "int32" 62 - }, 63 - "limit": { 64 - "type": "integer", 65 - "format": "int32" 66 - } 67 - } 68 - } 69 - } 70 - } 71 - }