diff --git a/package-lock.json b/package-lock.json index 25938fe8c3..69d5a260d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ }, "apps/generator": { "name": "@asyncapi/generator", - "version": "3.2.0", + "version": "3.2.1", "license": "Apache-2.0", "dependencies": { "@asyncapi/generator-components": "*", @@ -14543,7 +14543,7 @@ }, "packages/components": { "name": "@asyncapi/generator-components", - "version": "0.5.0", + "version": "0.6.0", "license": "Apache-2.0", "dependencies": { "@asyncapi/generator-helpers": "*", @@ -14838,7 +14838,7 @@ "name": "core-template-client-websocket-dart", "license": "Apache-2.0", "dependencies": { - "@asyncapi/generator-components": "0.5.0", + "@asyncapi/generator-components": "0.6.0", "@asyncapi/generator-helpers": "1.1.0", "@asyncapi/generator-react-sdk": "*" }, @@ -14858,7 +14858,7 @@ "name": "core-template-client-websocket-java-quarkus", "license": "Apache-2.0", "dependencies": { - "@asyncapi/generator-components": "0.5.0", + "@asyncapi/generator-components": "0.6.0", "@asyncapi/generator-helpers": "1.1.0", "@asyncapi/generator-react-sdk": "^1.1.2" }, @@ -14880,7 +14880,7 @@ "name": "core-template-client-websocket-javascript", "license": "Apache-2.0", "dependencies": { - "@asyncapi/generator-components": "0.5.0", + "@asyncapi/generator-components": "0.6.0", "@asyncapi/generator-helpers": "1.1.0", "@asyncapi/generator-react-sdk": "*", "@asyncapi/keeper": "0.5.0", @@ -14902,7 +14902,7 @@ "name": "core-template-client-websocket-python", "license": "Apache-2.0", "dependencies": { - "@asyncapi/generator-components": "0.5.0", + "@asyncapi/generator-components": "0.6.0", "@asyncapi/generator-helpers": "1.1.0", "@asyncapi/generator-react-sdk": "*" }, diff --git a/packages/helpers/src/bindings.js b/packages/helpers/src/bindings.js index 963aa2a0e9..730bb29125 100644 --- a/packages/helpers/src/bindings.js +++ b/packages/helpers/src/bindings.js @@ -1,52 +1,130 @@ /** - * Extracts default query parameters from a channel’s WebSocket binding. + * Extracts query parameters from all channels with WebSocket bindings. * - * @param {Map} channels - A Map representing all AsyncAPI channels. - * @returns {Map} A Map whose keys are parameter names and whose values are their defaults (or `''`). + * @param {Object} channels - An AsyncAPI channels collection object with isEmpty() and all() methods. + * @returns {Map>} A Map where keys are channel names and values are Maps of parameter names to their defaults (or `''`). * * @example - * // Suppose channel.bindings().get("ws").values() returns: - * // { query: { properties: { foo: { default: 'bar' }, baz: {} } } } - * const params = getQueryParams(channel); - * console.log(params.get('foo')); // → 'bar' - * console.log(params.get('baz')); // → '' + * // Suppose you have multiple channels with WS bindings: + * // channels: + * // chat: + * // bindings: + * // ws: + * // query: + * // properties: + * // token: { default: 'auth123' } + * // roomId: { } + * // notifications: + * // bindings: + * // ws: + * // query: + * // properties: + * // userId: { default: 'user456' } + * // token: { } + * + * const allParams = getQueryParamsForAllChannels(channels); + * + * // Returns a Map like: + * // { + * // 'chat' => { 'token' => 'auth123', 'roomId' => '' }, + * // 'notifications' => { 'userId' => 'user456', 'token' => '' } + * // } + * + * const chatParams = allParams.get('chat'); + * console.log(chatParams.get('token')); // → 'auth123' + * console.log(chatParams.get('roomId')); // → '' + * + * const notifParams = allParams.get('notifications'); + * console.log(notifParams.get('userId')); // → 'user456' + * console.log(notifParams.get('token')); // → '' */ -function getQueryParams(channels) { - // current implementation assumes there is always one channel - // at the moment only WebSocket binding support query params and the use case for WebSocket is that there is always one channel per AsyncAPI document - const channel = !channels.isEmpty() && channels.all().entries().next().value[1]; - - const queryMap = new Map(); +function getQueryParamsForAllChannels(channels) { + const allChannelsParams = new Map(); - const bindings = channel?.bindings?.(); - const hasWsBinding = bindings?.has('ws'); - - if (!hasWsBinding) { - return null; + if (channels.isEmpty()) { + return allChannelsParams; } - const wsBinding = bindings.get('ws'); - const query = wsBinding.value()?.query; - //we do not throw error, as user do not have to use query params, we just exit with null as it doesn't make sense to continue with query building - if (!query) { - return null; + for (const channel of channels.all()) { + const channelName = channel.id(); + const bindings = channel?.bindings?.(); + if (!bindings?.has('ws')) { + continue; + } + + const wsBinding = bindings.get('ws'); + const query = wsBinding.value()?.query; + if (!query) { + continue; + } + + const properties = query.properties; + if (!properties || typeof properties !== 'object' || Object.keys(properties).length === 0) { + continue; + } + + const channelParams = new Map(); + for (const [key, schema] of Object.entries(properties)) { + const value = schema.default ?? ''; + channelParams.set(key, String(value)); + } + + allChannelsParams.set(channelName, channelParams); } + + return allChannelsParams; +} + +/** + * Extracts default query parameters from the first channel's WebSocket binding. + * (Maintained for backward compatibility) + * + * @param {Map} channels - A Map representing all AsyncAPI channels. + * @returns {Map|null} A Map of parameter names to defaults (or `''`), or null if no WS binding found. + * + * @example + * // Suppose you have multiple channels with WS bindings: + * // channels: + * // chat: + * // bindings: + * // ws: + * // query: + * // properties: + * // token: { default: 'auth123' } + * // roomId: { } + * // notifications: + * // bindings: + * // ws: + * // query: + * // properties: + * // userId: { default: 'user456' } + * // token: { } + * + * const params = getQueryParams(channels); + * + * // Returns only the first channel's params (e.g., 'chat'): + * // { 'token' => 'auth123', 'roomId' => '' } + * + * console.log(params.get('token')); // → 'auth123' + * console.log(params.get('roomId')); // → '' + * + * // Note: To access query parameters for all channels, use getQueryParamsForAllChannels() + * const allParams = getQueryParamsForAllChannels(channels); + * const notifParams = allParams.get('notifications'); + * console.log(notifParams.get('userId')); // → 'user456' + */ +function getQueryParams(channels) { + const allChannelsParams = getQueryParamsForAllChannels(channels); - // Drill into the JSON Schema properties - const properties = query.properties; - if (!properties || typeof properties !== 'object') { + if (allChannelsParams.size === 0) { return null; } - - // Populate the map, preserving defaults - for (const [key, schema] of Object.entries(properties)) { - const value = schema.default ?? ''; - queryMap.set(key, String(value)); - } - return queryMap; + // Return the first channel's params for backward compatibility + return allChannelsParams.values().next().value; } module.exports = { - getQueryParams -}; \ No newline at end of file + getQueryParams, + getQueryParamsForAllChannels +}; diff --git a/packages/helpers/src/index.js b/packages/helpers/src/index.js index 60dc978edf..151be0a5d5 100644 --- a/packages/helpers/src/index.js +++ b/packages/helpers/src/index.js @@ -2,7 +2,7 @@ const { getMessageExamples, getOperationMessages } = require('./operations'); const { getServerUrl, getServer, getServerHost, getServerProtocol } = require('./servers'); const { getClientName, getInfo, toSnakeCase, toCamelCase, getTitle, lowerFirst, upperFirst } = require('./utils'); const { getMessageDiscriminatorData, getMessageDiscriminatorsFromOperations } = require('./discriminators'); -const { getQueryParams } = require('./bindings'); +const { getQueryParams, getQueryParamsForAllChannels } = require('./bindings'); const { cleanTestResultPaths, verifyDirectoryStructure, getDirElementsRecursive, buildParams, listFiles, hasNestedConfig} = require('./testing'); const { JavaModelsPresets } = require('./ModelsPresets'); @@ -14,6 +14,7 @@ module.exports = { getServerProtocol, listFiles, getQueryParams, + getQueryParamsForAllChannels, getOperationMessages, getMessageExamples, getTitle, diff --git a/packages/helpers/test/bindings.test.js b/packages/helpers/test/bindings.test.js index 87745eb4ee..715931aab0 100644 --- a/packages/helpers/test/bindings.test.js +++ b/packages/helpers/test/bindings.test.js @@ -1,6 +1,6 @@ const path = require('path'); const { Parser, fromFile } = require('@asyncapi/parser'); -const { getQueryParams } = require('@asyncapi/generator-helpers'); +const { getQueryParams, getQueryParamsForAllChannels } = require('@asyncapi/generator-helpers'); const parser = new Parser(); const asyncapi_v3_path = path.resolve(__dirname, './__fixtures__/asyncapi-websocket-query.yml'); @@ -77,4 +77,90 @@ describe('getQueryParams integration test with AsyncAPI', () => { const params = getQueryParamsForChannels(['wsBindingEmptyQuery']); expect(params).toBeNull(); }); +}); + +describe('getQueryParamsForAllChannels integration test with AsyncAPI', () => { + let parsedAsyncAPIDocument; + + beforeAll(async () => { + const parseResult = await fromFile(parser, asyncapi_v3_path).parse(); + parsedAsyncAPIDocument = parseResult.document; + }); + + /** + * Filters document channels by name and returns extracted WS query params for all matching channels. + * + * @param {string[]} channelNames - Channel IDs to include. + * @returns {Map>} Map of channel name -> query param defaults. + * Returns an empty Map when no matching channels with WS query properties are found. + */ + const getFilteredAllChannelsParams = (channelNames) => { + const channels = parsedAsyncAPIDocument.channels(); + const filteredMap = new Map(); + for (const channel of channels.all()) { + if (channelNames.includes(channel.id())) { + filteredMap.set(channel.id(), channel); + } + } + return getQueryParamsForAllChannels({ + isEmpty: () => filteredMap.size === 0, + all: () => [...filteredMap.values()] + }); + }; + + it('should extract query parameters from marketDataV1 channel', () => { + const allParams = getFilteredAllChannelsParams(['marketDataV1']); + + expect(allParams.size).toBe(1); + expect(allParams.has('marketDataV1')).toBe(true); + + const params = allParams.get('marketDataV1'); + expect(params).toBeInstanceOf(Map); + expect(params.get('heartbeat')).toBe('false'); + expect(params.get('top_of_book')).toBe('false'); + expect(params.get('bids')).toBe('true'); + expect(params.get('offers')).toBe(''); + }); + + it('should return empty map for channel without WebSocket binding', () => { + const allParams = getFilteredAllChannelsParams(['marketDataV1NoBinding']); + + expect(allParams.size).toBe(0); + }); + + it('should return empty map for empty channels', () => { + const emptyChannels = { + isEmpty: () => true, + all: () => [] + }; + const allParams = getQueryParamsForAllChannels(emptyChannels); + + expect(allParams.size).toBe(0); + }); + + it('should return empty map for channel with empty binding', () => { + const allParams = getFilteredAllChannelsParams(['emptyChannel']); + + expect(allParams.size).toBe(0); + }); + + it('should return empty map if WebSocket binding exists but has no query parameters', () => { + const allParams = getFilteredAllChannelsParams(['wsBindingNoQuery']); + + expect(allParams.size).toBe(0); + }); + + it('should return empty map if WebSocket binding query exists but has no properties', () => { + const allParams = getFilteredAllChannelsParams(['wsBindingEmptyQuery']); + + expect(allParams.size).toBe(0); + }); + + it('should skip channels without WS bindings and only include those with WS bindings', () => { + const allParams = getFilteredAllChannelsParams(['marketDataV1', 'marketDataV1NoBinding']); + + expect(allParams.size).toBe(1); + expect(allParams.has('marketDataV1')).toBe(true); + expect(allParams.has('marketDataV1NoBinding')).toBe(false); + }); }); \ No newline at end of file