diff --git a/.changeset/feat-websocket-js-query-params.md b/.changeset/feat-websocket-js-query-params.md new file mode 100644 index 0000000000..612d358655 --- /dev/null +++ b/.changeset/feat-websocket-js-query-params.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/generator": minor +--- + +Add query parameter support to JavaScript WebSocket client template. The template now extracts query parameters from AsyncAPI channel bindings and generates constructor parameters to automatically append them to the WebSocket URL using URLSearchParams. diff --git a/packages/templates/clients/websocket/javascript/components/ClientClass.js b/packages/templates/clients/websocket/javascript/components/ClientClass.js index 70eee1fe75..85368469dd 100644 --- a/packages/templates/clients/websocket/javascript/components/ClientClass.js +++ b/packages/templates/clients/websocket/javascript/components/ClientClass.js @@ -4,13 +4,13 @@ import { CloseConnection, RegisterMessageHandler, RegisterErrorHandler, SendOper import { ModuleExport } from './ModuleExport'; import { CompileOperationSchemas } from './CompileOperationSchemas'; -export function ClientClass({ clientName, serverUrl, title, sendOperations }) { +export function ClientClass({ clientName, serverUrl, title, sendOperations, query }) { return ( {`class ${clientName} {`} - + operation.id()); const sendOperationsArray = JSON.stringify(sendOperationsId); + const queryParamsArray = query && Array.from(query.entries()); + + const getConstructorSignature = () => { + if (!queryParamsArray || queryParamsArray.length === 0) { + return 'constructor(url)'; + } + const queryParamNames = queryParamsArray.map(([paramName]) => paramName).join(', '); + return `constructor(url, ${queryParamNames})`; + }; + + const getQueryParamsDocumentation = () => { + if (!queryParamsArray || queryParamsArray.length === 0) { + return ''; + } + return queryParamsArray.map(([paramName]) => `\n * @param {string} ${paramName} - Query parameter for the WebSocket URL`).join(''); + }; + + const getQueryParamsInitialization = () => { + if (!queryParamsArray || queryParamsArray.length === 0) { + return ''; + } + return queryParamsArray.map(([paramName]) => `\n if (${paramName}) params['${paramName}'] = ${paramName};`).join(''); + }; return ( { `/* * Constructor to initialize the WebSocket client - * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document.${getQueryParamsDocumentation()} */ -constructor(url) { - this.url = url || '${serverUrl}'; +${getConstructorSignature()} {${ + query ? ` + const params = {};${getQueryParamsInitialization()} + const qs = new URLSearchParams(params).toString(); + this.url = qs ? \`\${url || '${serverUrl}'}?\${qs}\` : (url || '${serverUrl}');` : ` + this.url = url || '${serverUrl}';` + } this.websocket = null; this.messageHandlers = []; this.errorHandlers = []; diff --git a/packages/templates/clients/websocket/javascript/template/client.js.js b/packages/templates/clients/websocket/javascript/template/client.js.js index 207fb33dc7..21a3a2f9a4 100644 --- a/packages/templates/clients/websocket/javascript/template/client.js.js +++ b/packages/templates/clients/websocket/javascript/template/client.js.js @@ -1,5 +1,5 @@ import { File } from '@asyncapi/generator-react-sdk'; -import { getClientName, getServerUrl, getServer, getInfo, getTitle } from '@asyncapi/generator-helpers'; +import { getClientName, getServerUrl, getServer, getInfo, getTitle, getQueryParams } from '@asyncapi/generator-helpers'; import { FileHeaderInfo, DependencyProvider } from '@asyncapi/generator-components'; import { ClientClass } from '../components/ClientClass'; @@ -10,6 +10,7 @@ export default function ({ asyncapi, params }) { const clientName = getClientName(asyncapi, params.appendClientSuffix, params.customClientName); const serverUrl = getServerUrl(server); const sendOperations = asyncapi.operations().filterBySend(); + const queryParams = getQueryParams(asyncapi.channels()); const asyncapiFilepath = `${params.asyncapiFileDir}/asyncapi.yaml`; return ( @@ -22,7 +23,7 @@ export default function ({ asyncapi, params }) { language="javascript" additionalDependencies={['const path = require(\'path\');', `const asyncapiFilepath = path.resolve(__dirname, '${asyncapiFilepath}');`]} /> - + ); } diff --git a/packages/templates/clients/websocket/javascript/test/components/Constructor.test.js b/packages/templates/clients/websocket/javascript/test/components/Constructor.test.js new file mode 100644 index 0000000000..14a1459db0 --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/Constructor.test.js @@ -0,0 +1,49 @@ +import path from 'path'; +import { render } from '@asyncapi/generator-react-sdk'; +import { Parser, fromFile } from '@asyncapi/parser'; +import { getServer, getServerUrl, getQueryParams } from '@asyncapi/generator-helpers'; +import { Constructor } from '../../components/Constructor.js'; + +const parser = new Parser(); +const asyncapiFilePath = path.resolve(__dirname, '../../../../../../helpers/test/__fixtures__/asyncapi-websocket-query.yml'); + +describe('Constructor component (integration with AsyncAPI document)', () => { + let servers; + let parsedAsyncAPIDocument; + let sendOperations; + + beforeAll(async () => { + const parseResult = await fromFile(parser, asyncapiFilePath).parse(); + parsedAsyncAPIDocument = parseResult.document; + servers = parsedAsyncAPIDocument.servers(); + sendOperations = parsedAsyncAPIDocument.operations().filterBySend(); + }); + + test('renders with only serverUrl (withHostDuplicatingProtocol)', () => { + const server = getServer(servers, 'withHostDuplicatingProtocol'); + const serverUrl = getServerUrl(server); + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with neither serverUrl nor query', () => { + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with undefined query', () => { + const server = getServer(servers, 'withVariables'); + const serverUrl = getServerUrl(server); + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); + + test('renders with serverUrl and query parameters', () => { + const server = getServer(servers, 'withVariables'); + const serverUrl = getServerUrl(server); + const channels = parsedAsyncAPIDocument.channels(); + const queryParams = getQueryParams(channels); + const result = render(); + expect(result.trim()).toMatchSnapshot(); + }); +}); diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/AvailableOperations.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/AvailableOperations.test.js.snap deleted file mode 100644 index 1cf4a77a02..0000000000 --- a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/AvailableOperations.test.js.snap +++ /dev/null @@ -1,111 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Testing AvailableOperations component Must render all available operations 1`] = ` -"### Available Operations - -#### \`noMessage(payload)\` -Operation with no messages - - - - -#### \`noMessageExamples(payload)\` -Message without any examples - - - - -#### \`noSummaryOperations(payload)\` - - -This is a description without a summary - - - -#### \`oneMessageExample(payload)\` -One message with one example - - - -**Example:** -\`\`\`javascript -client.oneMessageExample(\\"test\\"); -\`\`\` - - - -#### \`noSummaryNoDescriptionOperations(payload)\` - - - - -**Example:** -\`\`\`javascript -client.noSummaryNoDescriptionOperations(\\"test\\"); -\`\`\` - - - -#### \`multipleExamples(payload)\` -Multiple messages with examples - - - -**Example:** -\`\`\`javascript -client.multipleExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.multipleExamples(false); -\`\`\` - - -**Example:** -\`\`\`javascript -client.multipleExamples(123); -\`\`\` - - - -#### \`mixedMessageExamples(payload)\` -Mixed message example coverage - -An example of a send operation with multiple message with examples - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(false); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(123); -\`\`\` - - - -#### \`PascalCaseOperation(payload)\` - - - - - -#### \`operation_with_snake_case(payload)\`" -`; diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/Constructor.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/Constructor.test.js.snap new file mode 100644 index 0000000000..a9f7e35cac --- /dev/null +++ b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/Constructor.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Constructor component (integration with AsyncAPI document) renders with neither serverUrl nor query 1`] = ` +"/* + * Constructor to initialize the WebSocket client + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + */ + constructor(url) { + this.url = url || 'null'; + this.websocket = null; + this.messageHandlers = []; + this.errorHandlers = []; + this.compiledSchemas = {}; + this.schemasCompiled = false; + this.sendOperationsId = [\\"noMessage\\",\\"noSummaryOperations\\",\\"noSummaryNoDescriptionOperations\\",\\"mixedMessageExamples\\",\\"operation_with_snake_case\\"]; + }" +`; + +exports[`Constructor component (integration with AsyncAPI document) renders with only serverUrl (withHostDuplicatingProtocol) 1`] = ` +"/* + * Constructor to initialize the WebSocket client + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + */ + constructor(url) { + this.url = url || 'wss://api.gemini.com'; + this.websocket = null; + this.messageHandlers = []; + this.errorHandlers = []; + this.compiledSchemas = {}; + this.schemasCompiled = false; + this.sendOperationsId = [\\"noMessage\\",\\"noSummaryOperations\\",\\"noSummaryNoDescriptionOperations\\",\\"mixedMessageExamples\\",\\"operation_with_snake_case\\"]; + }" +`; + +exports[`Constructor component (integration with AsyncAPI document) renders with serverUrl and query parameters 1`] = ` +"/* + * Constructor to initialize the WebSocket client + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + * @param {string} heartbeat - Query parameter for the WebSocket URL + * @param {string} top_of_book - Query parameter for the WebSocket URL + * @param {string} bids - Query parameter for the WebSocket URL + * @param {string} offers - Query parameter for the WebSocket URL + */ + constructor(url, heartbeat, top_of_book, bids, offers) { + const params = {}; + if (heartbeat) params['heartbeat'] = heartbeat; + if (top_of_book) params['top_of_book'] = top_of_book; + if (bids) params['bids'] = bids; + if (offers) params['offers'] = offers; + const qs = new URLSearchParams(params).toString(); + this.url = qs ? \`\${url || 'wss://api.gemini.com/v1/marketdata/{symbol}'}?\${qs}\` : (url || 'wss://api.gemini.com/v1/marketdata/{symbol}'); + this.websocket = null; + this.messageHandlers = []; + this.errorHandlers = []; + this.compiledSchemas = {}; + this.schemasCompiled = false; + this.sendOperationsId = [\\"noMessage\\",\\"noSummaryOperations\\",\\"noSummaryNoDescriptionOperations\\",\\"mixedMessageExamples\\",\\"operation_with_snake_case\\"]; + }" +`; + +exports[`Constructor component (integration with AsyncAPI document) renders with undefined query 1`] = ` +"/* + * Constructor to initialize the WebSocket client + * @param {string} url - The WebSocket server URL. Use it if the server URL is different from the default one taken from the AsyncAPI document. + */ + constructor(url) { + this.url = url || 'wss://api.gemini.com/v1/marketdata/{symbol}'; + this.websocket = null; + this.messageHandlers = []; + this.errorHandlers = []; + this.compiledSchemas = {}; + this.schemasCompiled = false; + this.sendOperationsId = [\\"noMessage\\",\\"noSummaryOperations\\",\\"noSummaryNoDescriptionOperations\\",\\"mixedMessageExamples\\",\\"operation_with_snake_case\\"]; + }" +`; diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/MessageExamples.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/MessageExamples.test.js.snap deleted file mode 100644 index b163eab120..0000000000 --- a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/MessageExamples.test.js.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Testing MessageExamples component Test operation with multiple examples 1`] = ` -"**Example:** -\`\`\`javascript -client.multipleExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.multipleExamples(false); -\`\`\` - - -**Example:** -\`\`\`javascript -client.multipleExamples(123); -\`\`\`" -`; - -exports[`Testing MessageExamples component Test operation with multiple messages and multiple examples 1`] = ` -"**Example:** -\`\`\`javascript -client.mixedMessageExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(\\"test\\"); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(false); -\`\`\` - - -**Example:** -\`\`\`javascript -client.mixedMessageExamples(123); -\`\`\`" -`; - -exports[`Testing MessageExamples component Test operation with no message examples 1`] = `""`; - -exports[`Testing MessageExamples component Test operation with no messages 1`] = `""`; - -exports[`Testing MessageExamples component Test operation with one example 1`] = ` -"**Example:** -\`\`\`javascript -client.oneMessageExample(\\"test\\"); -\`\`\`" -`; diff --git a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/OperationHeader.test.js.snap b/packages/templates/clients/websocket/javascript/test/components/__snapshots__/OperationHeader.test.js.snap deleted file mode 100644 index 87aff8bbd0..0000000000 --- a/packages/templates/clients/websocket/javascript/test/components/__snapshots__/OperationHeader.test.js.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Testing of OperationHeader function render operation header with description only correctly 1`] = ` -"#### \`noSummaryOperations(payload)\` - - -This is a description without a summary" -`; - -exports[`Testing of OperationHeader function render operation header with no summary or description correctly 1`] = `"#### \`noSummaryNoDescriptionOperations(payload)\`"`; - -exports[`Testing of OperationHeader function render operation header with summary and description correctly 1`] = ` -"#### \`mixedMessageExamples(payload)\` -Mixed message example coverage - -An example of a send operation with multiple message with examples" -`; - -exports[`Testing of OperationHeader function render operation header with summary only correctly 1`] = ` -"#### \`noMessage(payload)\` -Operation with no messages" -`;