···11+---
22+'@hey-api/openapi-ts': minor
33+---
44+55+feat(parser): replace `plugin.subscribe()` with `plugin.forEach()`
66+77+### Added `plugin.forEach()` method
88+99+This method replaces the `.subscribe()` method. Additionally, `.forEach()` is executed immediately, which means we don't need the `before` and `after` events – simply move your code before and after the `.forEach()` block.
1010+1111+```ts
1212+plugin.forEach('operation', 'schema', (event) => {
1313+ // do something with event
1414+});
1515+```
+16
docs/openapi-ts/migrating.md
···27272828This config option is deprecated and will be removed.
29293030+## v0.75.0
3131+3232+### Added `plugin.forEach()` method
3333+3434+This method replaces the `.subscribe()` method. Additionally, `.forEach()` is executed immediately, which means we don't need the `before` and `after` events – simply move your code before and after the `.forEach()` block.
3535+3636+```ts
3737+plugin.subscribe('operation', (event) => { // [!code --]
3838+ // do something with event // [!code --]
3939+}); // [!code --]
4040+plugin.subscribe('schema', (event) => { // [!code --]
4141+plugin.forEach('operation', 'schema', (event) => { // [!code ++]
4242+ // do something with event
4343+});
4444+```
4545+3046## v0.74.0
31473248### Single Zod schema per request
+6-14
docs/openapi-ts/plugins/custom.md
···118118 path: plugin.output,
119119 });
120120121121- plugin.subscribe('before', () => {
122122- // do something before parsing the input
123123- });
124124-125125- plugin.subscribe('operation', ({ operation }) => {
126126- // do something with the operation model
127127- });
128128-129129- plugin.subscribe('schema', ({ operation }) => {
130130- // do something with the schema model
131131- });
132132-133133- plugin.subscribe('after', () => {
134134- // do something after parsing the input
121121+ plugin.forEach('operation', 'schema', (event) => {
122122+ if (event.type === 'operation') {
123123+ // do something with the operation model
124124+ } else if (event.type === 'schema') {
125125+ // do something with the schema model
126126+ }
135127 });
136128137129 // we're using the TypeScript Compiler API
···33import ts from 'typescript';
4455import { compiler } from '../compiler';
66-import { parseIR } from '../ir/parser';
76import type { IR } from '../ir/types';
87import { getClientPlugin } from '../plugins/@hey-api/client-core/utils';
98import { generateClientBundle } from './client';
···3332 }
34333534 for (const plugin of context.registerPlugins()) {
3636- plugin.run();
3535+ await plugin.run();
3736 }
3838-3939- await parseIR({ context });
40374138 if (!context.config.dryRun) {
4239 const indexFile = context.createFile({
+9-106
packages/openapi-ts/src/ir/context.ts
···11import path from 'node:path';
2233-import { HeyApiError } from '../error';
43import { TypeScriptFile } from '../generate/files';
54import { PluginInstance } from '../plugins/shared/utils/instance';
65import type { Config, StringCase } from '../types/config';
···4241 path: string;
4342}
44434545-export interface Events {
4646- /**
4747- * Called after parsing.
4848- */
4949- after: () => void;
5050- /**
5151- * Called before parsing.
5252- */
5353- before: () => void;
5454- operation: (args: {
5555- method: keyof IR.PathItemObject;
5656- operation: IR.OperationObject;
5757- path: string;
5858- }) => void;
5959- parameter: (args: {
6060- $ref: string;
6161- name: string;
6262- parameter: IR.ParameterObject;
6363- }) => void;
6464- requestBody: (args: {
6565- $ref: string;
6666- name: string;
6767- requestBody: IR.RequestBodyObject;
6868- }) => void;
6969- schema: (args: {
7070- $ref: string;
7171- name: string;
7272- schema: IR.SchemaObject;
7373- }) => void;
7474- server: (args: { server: IR.ServerObject }) => void;
7575-}
7676-7777-type ListenerWithMeta<T extends keyof Events> = {
7878- callbackFn: Events[T];
7979- pluginName: string;
8080-};
8181-8282-type Listeners = {
8383- [T in keyof Events]?: Array<ListenerWithMeta<T>>;
8484-};
8585-8644export class IRContext<Spec extends Record<string, any> = any> {
8745 /**
8846 * Configuration for parsing and generating the output. This
···9250 /**
9351 * A map of files that will be generated from `spec`.
9452 */
9595- public files: Files;
5353+ public files: Files = {};
9654 /**
9755 * Intermediate representation model obtained from `spec`.
9856 */
9999- public ir: IR.Model;
5757+ public ir: IR.Model = {};
10058 /**
10159 * A map of registered plugin instances, keyed by plugin name. Plugins are
10260 * registered through the `registerPlugin` method and can be accessed by
···10765 * Resolved specification from `input`.
10866 */
10967 public spec: Spec;
110110-111111- /**
112112- * A map of event listeners.
113113- */
114114- private listeners: Listeners;
1156811669 constructor({ config, spec }: { config: Config; spec: Spec }) {
11770 this.config = config;
118118- this.files = {};
119119- this.ir = {};
120120- this.listeners = {};
12171 this.spec = spec;
12272 }
1237312474 /**
125125- * Notify all event listeners about `event`.
126126- */
127127- public async broadcast<T extends keyof Events>(
128128- event: T,
129129- ...args: Parameters<Events[T]>
130130- ): Promise<void> {
131131- const eventListeners = this.listeners[event];
132132-133133- if (eventListeners) {
134134- for (const listener of eventListeners) {
135135- try {
136136- await listener.callbackFn(
137137- // @ts-expect-error
138138- ...args,
139139- );
140140- } catch (error) {
141141- const originalError =
142142- error instanceof Error ? error : new Error(String(error));
143143- throw new HeyApiError({
144144- args,
145145- error: originalError,
146146- event,
147147- name: 'BroadcastError',
148148- pluginName: listener.pluginName,
149149- });
150150- }
151151- }
152152- }
153153- }
154154-155155- /**
15675 * Create and return a new TypeScript file. Also set the current file context
15776 * to the newly created file.
15877 */
···216135 }
217136218137 /**
219219- * Generator that iterates through plugin order and registers each plugin.
220220- * Yields the registered plugin instance for each plugin name.
138138+ * Registers all plugins in the order specified by the configuration and returns
139139+ * an array of the registered PluginInstance objects. Each plugin is instantiated
140140+ * and added to the context's plugins map.
141141+ *
142142+ * @returns {ReadonlyArray<PluginInstance>} An array of registered plugin instances in order.
221143 */
222222- public *registerPlugins(): Generator<PluginInstance> {
223223- for (const name of this.config.pluginOrder) {
224224- yield this.registerPlugin(name);
225225- }
144144+ public registerPlugins(): ReadonlyArray<PluginInstance> {
145145+ return this.config.pluginOrder.map((name) => this.registerPlugin(name));
226146 }
227147228148 // TODO: parser - works the same as resolveRef, but for IR schemas.
···243163 return resolveRef<T>({
244164 $ref,
245165 spec: this.spec,
246246- });
247247- }
248248-249249- /**
250250- * Register a new `event` listener.
251251- */
252252- public subscribe<T extends keyof Events>(
253253- event: T,
254254- callbackFn: Events[T],
255255- pluginName: string,
256256- ): void {
257257- if (!this.listeners[event]) {
258258- this.listeners[event] = [];
259259- }
260260- this.listeners[event].push({
261261- callbackFn,
262262- pluginName: pluginName ?? '',
263166 });
264167 }
265168}
-47
packages/openapi-ts/src/ir/parser.ts
···11-import type { IR } from './types';
22-33-/**
44- * Traverse the parsed intermediate representation model and broadcast
55- * various events to listeners.
66- */
77-export const parseIR = async ({ context }: { context: IR.Context }) => {
88- await context.broadcast('before');
99-1010- if (context.ir.servers) {
1111- for (const server of context.ir.servers) {
1212- await context.broadcast('server', { server });
1313- }
1414- }
1515-1616- if (context.ir.components) {
1717- for (const name in context.ir.components.schemas) {
1818- const schema = context.ir.components.schemas[name]!;
1919- const $ref = `#/components/schemas/${name}`;
2020- await context.broadcast('schema', { $ref, name, schema });
2121- }
2222-2323- for (const name in context.ir.components.parameters) {
2424- const parameter = context.ir.components.parameters[name]!;
2525- const $ref = `#/components/parameters/${name}`;
2626- await context.broadcast('parameter', { $ref, name, parameter });
2727- }
2828-2929- for (const name in context.ir.components.requestBodies) {
3030- const requestBody = context.ir.components.requestBodies[name]!;
3131- const $ref = `#/components/requestBodies/${name}`;
3232- await context.broadcast('requestBody', { $ref, name, requestBody });
3333- }
3434- }
3535-3636- for (const path in context.ir.paths) {
3737- const pathItem = context.ir.paths[path as keyof IR.PathsObject];
3838-3939- for (const _method in pathItem) {
4040- const method = _method as keyof IR.PathItemObject;
4141- const operation = pathItem[method]!;
4242- await context.broadcast('operation', { method, operation, path });
4343- }
4444- }
4545-4646- await context.broadcast('after');
4747-};
+1-2
packages/openapi-ts/src/ir/types.d.ts
···33 SecuritySchemeObject,
44 ServerObject,
55} from '../openApi/3.1.x/types/spec';
66-import type { ContextFile as CtxFile, Events, IRContext } from './context';
66+import type { ContextFile as CtxFile, IRContext } from './context';
77import type { IRMediaType } from './mediaType';
8899interface IRBodyObject {
···217217 export type BodyObject = IRBodyObject;
218218 export type ComponentsObject = IRComponentsObject;
219219 export type Context<Spec extends Record<string, any> = any> = IRContext<Spec>;
220220- export type ContextEvents = Events;
221220 export type ContextFile = CtxFile;
222221 export type Model = IRModel;
223222 export type OperationObject = IROperationObject;