Skip to content

Commit 0f775e5

Browse files
authored
refactor: replace hardcode sendEchoMessage method in py websocket client (#1572)
Co-authored-by: Adi-204 <adiboghawala@gmail.com>
1 parent 7e1db1e commit 0f775e5

File tree

16 files changed

+399
-126
lines changed

16 files changed

+399
-126
lines changed

packages/helpers/src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { getMessageExamples, getOperationMessages } = require('./operations');
22
const { getServerUrl, getServer } = require('./servers');
3-
const { getClientName, listFiles, getInfo } = require('./utils');
3+
const { getClientName, listFiles, getInfo, toSnakeCase } = require('./utils');
44
const { getQueryParams } = require('./bindings');
55

66
module.exports = {
@@ -11,5 +11,6 @@ module.exports = {
1111
getQueryParams,
1212
getOperationMessages,
1313
getMessageExamples,
14-
getInfo
14+
getInfo,
15+
toSnakeCase
1516
};

packages/helpers/src/utils.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,24 @@ const getInfo = (asyncapi) => {
5757
return info;
5858
};
5959

60+
/**
61+
* Convert a camelCase or PascalCase string to snake_case.
62+
* If the string is already in snake_case, it will be returned unchanged.
63+
*
64+
* @param {string} camelStr - The string to convert to snake_case
65+
* @returns {string} The converted snake_case string
66+
*/
67+
const toSnakeCase = (inputStr) => {
68+
return inputStr
69+
.replace(/\W+/g, ' ')
70+
.split(/ |\B(?=[A-Z])/)
71+
.map((word) => word.toLowerCase())
72+
.join('_');
73+
};
74+
6075
module.exports = {
6176
getClientName,
6277
listFiles,
63-
getInfo
78+
getInfo,
79+
toSnakeCase
6480
};
65-

packages/helpers/test/__fixtures__/asyncapi-websocket-query.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ operations:
137137
- $ref: '#/channels/marketDataV1/messages/oneExample'
138138
- $ref: '#/channels/marketDataV1/messages/multipleExamples'
139139

140+
PascalCaseOperation:
141+
action: receive
142+
channel:
143+
$ref: '#/channels/marketDataV1'
144+
messages:
145+
- $ref: '#/channels/marketDataV1/messages/noExamples'
146+
147+
operation_with_snake_case:
148+
action: send
149+
channel:
150+
$ref: '#/channels/marketDataV1'
151+
messages:
152+
- $ref: '#/channels/marketDataV1/messages/noExamples'
153+
140154
components:
141155
parameters:
142156
symbol:
@@ -146,4 +160,4 @@ components:
146160
enum:
147161
- btcusd
148162
- ethbtc
149-
- ethusd
163+
- ethusd

packages/helpers/test/utils.test.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const path = require('path');
22
const { Parser, fromFile } = require('@asyncapi/parser');
3-
const { getClientName, getInfo } = require('@asyncapi/generator-helpers');
3+
const { getClientName, getInfo, toSnakeCase } = require('@asyncapi/generator-helpers');
44

55
const parser = new Parser();
66
const asyncapi_v3_path = path.resolve(__dirname, './__fixtures__/asyncapi-websocket-query.yml');
@@ -80,4 +80,41 @@ describe('getInfo integration test with AsyncAPI', () => {
8080
getInfo(null);
8181
}).toThrow('Make sure you pass AsyncAPI document as an argument.');
8282
});
83+
});
84+
85+
describe('toSnakeCase integration test with AsyncAPI', () => {
86+
let parsedAsyncAPIDocument, operations;
87+
88+
beforeAll(async () => {
89+
const parseResult = await fromFile(parser, asyncapi_v3_path).parse();
90+
parsedAsyncAPIDocument = parseResult.document;
91+
operations = parsedAsyncAPIDocument.operations();
92+
});
93+
94+
it('should convert PascalCase operation names to snake_case format', () => {
95+
const operation = operations.get('PascalCaseOperation');
96+
const actualOperationId = toSnakeCase(operation.id());
97+
const expectedOperationId = 'pascal_case_operation';
98+
expect(actualOperationId).toBe(expectedOperationId);
99+
});
100+
101+
it('should leave already snake_case operation names unchanged', () => {
102+
const operation = operations.get('operation_with_snake_case');
103+
const actualOperationId = toSnakeCase(operation.id());
104+
const expectedOperationId = 'operation_with_snake_case';
105+
expect(actualOperationId).toBe(expectedOperationId);
106+
});
107+
108+
it('should convert camelCase operation names to snake_case format', () => {
109+
const operation = operations.get('noSummaryNoDescriptionOperations');
110+
const actualOperationId = toSnakeCase(operation.id());
111+
const expectedOperationId = 'no_summary_no_description_operations';
112+
expect(actualOperationId).toBe(expectedOperationId);
113+
});
114+
115+
it('should return empty string', () => {
116+
const actualOperationId = toSnakeCase('');
117+
const expectedOperationId = '';
118+
expect(actualOperationId).toBe(expectedOperationId);
119+
});
83120
});

packages/templates/clients/websocket/javascript/test/components/__snapshots__/AvailableOperations.test.js.snap

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,15 @@ client.mixedMessageExamples(false);
9797
**Example:**
9898
\`\`\`javascript
9999
client.mixedMessageExamples(123);
100-
\`\`\`"
100+
\`\`\`
101+
102+
103+
104+
#### \`PascalCaseOperation(payload)\`
105+
106+
107+
108+
109+
110+
#### \`operation_with_snake_case(payload)\`"
101111
`;

packages/templates/clients/websocket/javascript/test/components/__snapshots__/SendOperation.test.js.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,34 @@ exports[`Testing of SendOperation function render websockets with send operation
110110
throw new Error('WebSocket connection not established. Call connect() first.');
111111
}
112112
GeminiMarketDataWebsocketAPI.mixedMessageExamples(message, this.websocket);
113+
}
114+
115+
116+
/**
117+
* Sends a operation_with_snake_case message over the WebSocket connection.
118+
*
119+
* @param {Object} message - The message payload to send. Should match the schema defined in the AsyncAPI document.
120+
* @param {WebSocket} [socket] - The WebSocket connection to use. If not provided, the client's own connection will be used.
121+
* @throws {TypeError} If message cannot be stringified to JSON
122+
* @throws {Error} If WebSocket connection is not in OPEN state
123+
*/
124+
static operation_with_snake_case(message, socket) {
125+
try {
126+
socket.send(JSON.stringify(message));
127+
} catch (error) {
128+
console.error('Error sending operation_with_snake_case message:', error);
129+
}
130+
}
131+
/**
132+
* Instance method version of operation_with_snake_case that uses the client's own WebSocket connection.
133+
* @param {Object} message - The message payload to send
134+
* @throws {Error} If WebSocket connection is not established
135+
*/
136+
operation_with_snake_case(message){
137+
if(!this.websocket){
138+
throw new Error('WebSocket connection not established. Call connect() first.');
139+
}
140+
GeminiMarketDataWebsocketAPI.operation_with_snake_case(message, this.websocket);
113141
}"
114142
`;
115143

packages/templates/clients/websocket/python/components/ClientClass.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { Connect } from './Connect';
44
import { RegisterMessageHandler } from './RegisterMessageHandler';
55
import { RegisterErrorHandler } from './RegisterErrorHandler';
66
import { HandleMessage } from './HandleMessage';
7-
import { SendEchoMessage } from './SendEchoMessage';
7+
import { SendOperation } from './SendOperation';
8+
import { Send } from './Send';
89
import { CloseConnection } from './CloseConnection';
910
import { RegisterOutgoingProcessor } from './RegisterOutgoingProcessor';
1011
import { HandleError } from './HandleError';
1112

12-
export function ClientClass({ clientName, serverUrl, title, queryParams }) {
13+
export function ClientClass({ clientName, serverUrl, title, queryParams, operations }) {
14+
const sendOperations = operations.filterBySend();
1315
return (
1416
<Text>
1517
<Text newLines={2}>
@@ -22,7 +24,8 @@ export function ClientClass({ clientName, serverUrl, title, queryParams }) {
2224
<RegisterOutgoingProcessor />
2325
<HandleMessage />
2426
<HandleError />
25-
<SendEchoMessage />
27+
<SendOperation sendOperations={sendOperations} clientName={clientName} />
28+
<Send sendOperations={sendOperations} />
2629
<CloseConnection />
2730
</Text>
2831
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Text } from '@asyncapi/generator-react-sdk';
2+
3+
export function Send({ sendOperations }) {
4+
if (!sendOperations || sendOperations.length === 0) {
5+
return null;
6+
}
7+
8+
return (
9+
<Text newLines={2} indent={2}>
10+
{
11+
`
12+
@staticmethod
13+
async def _send(message, socket):
14+
"""
15+
Internal helper to handle the actual sending logic.
16+
17+
Args:
18+
message (dict or str): The message to send.
19+
socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
20+
21+
Notes:
22+
If message is a dictionary, it will be automatically converted to JSON.
23+
"""
24+
try:
25+
if isinstance(message, dict):
26+
message = json.dumps(message)
27+
await socket.send(message)
28+
except Exception as e:
29+
print("Error sending:", e)`
30+
}
31+
</Text>
32+
);
33+
}

packages/templates/clients/websocket/python/components/SendEchoMessage.js

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Text } from '@asyncapi/generator-react-sdk';
2+
import { toSnakeCase } from '@asyncapi/generator-helpers';
3+
4+
export function SendOperation({ sendOperations, clientName }) {
5+
if (!sendOperations || sendOperations.length === 0) {
6+
return null;
7+
}
8+
9+
return (
10+
<>
11+
{
12+
sendOperations.map((operation) => {
13+
const methodName = toSnakeCase(operation.id());
14+
const staticMethodName = `${methodName}_static`;
15+
16+
return (
17+
<Text newLines={2} indent={2}>
18+
{`async def ${methodName}(self, message):
19+
"""
20+
Send a ${methodName} message using the WebSocket connection attached to this instance.
21+
22+
Args:
23+
message (dict or str): The message to send. Will be serialized to JSON if it's a dictionary.
24+
25+
Raises:
26+
Exception: If sending fails or the socket is not connected.
27+
"""
28+
await self._send(message, self.ws_app)
29+
30+
@staticmethod
31+
async def ${staticMethodName}(message, socket):
32+
"""
33+
Send a ${methodName} message using a provided WebSocket connection, without needing an instance.
34+
35+
Args:
36+
message (dict or str): The message to send.
37+
socket (websockets.WebSocketCommonProtocol): The WebSocket to send through.
38+
39+
Raises:
40+
Exception: If sending fails or the socket is not connected.
41+
"""
42+
await ${clientName}._send(message, socket)
43+
`}
44+
</Text>
45+
);
46+
})
47+
}
48+
</>
49+
);
50+
}

0 commit comments

Comments
 (0)